Skip to main content

fleuron/
fonts.rs

1//! The font registry: font bytes in, `font_id`s out.
2//!
3//! Both the shaper and every painter resolve glyphs through this one
4//! table, which is what makes preview equal export. The registry is
5//! the sole owner of font bytes; nothing else in the engine keeps a
6//! `FontRef` lifetime alive.
7
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use harfrust::{
12    BufferClusterLevel, Feature, FontRef as HarfFontRef, Language, ShaperData, ShaperInstance, Tag,
13    UnicodeBuffer, script,
14};
15use serde::{Deserialize, Serialize};
16use skrifa::MetadataProvider;
17use skrifa::attribute::Style as SlopeStyle;
18use skrifa::instance::{Location, LocationRef, Size};
19use skrifa::metrics::{GlyphMetrics, Metrics};
20use skrifa::prelude::GlyphId;
21use skrifa::raw::TableProvider;
22use skrifa::string::StringId;
23
24/// A font entering the engine: raw bytes and the identity the style
25/// tree matched against.
26///
27/// Bytes are opaque here — layout never decodes images or fonts
28/// twice; decoding happens once, at registration.
29#[derive(Debug, Clone)]
30pub struct FontSource {
31    /// The full font file (TTF/OTF). Registry-owned; callers hand
32    /// over a copy.
33    pub bytes: Vec<u8>,
34    /// Family name for matching (lowercase, e.g. "eb garamond").
35    pub family: String,
36    /// Face name (name id 4), e.g. "EB Garamond Regular".
37    pub name: String,
38    /// Style name (name id 2), e.g. "Regular".
39    pub style: String,
40    /// What a stylesheet declared this face to be, overriding what
41    /// the file says about itself. A declared identity also pins
42    /// registration to the file's default instance: a sheet naming
43    /// one slope and one weight is not describing a variable
44    /// family's five.
45    pub declared: Option<FaceAttributes>,
46}
47
48impl FontSource {
49    /// Builds a source from a font file, reading identity from its
50    /// name table.
51    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, FontError> {
52        let font = skrifa::FontRef::new(&bytes).map_err(|_| FontError::Parse)?;
53        let family = name_string(&font, StringId::TYPOGRAPHIC_FAMILY_NAME)
54            .or_else(|| name_string(&font, StringId::FAMILY_NAME))
55            .ok_or(FontError::MissingName)?;
56        let name = name_string(&font, StringId::FULL_NAME).unwrap_or_else(|| family.clone());
57        let style =
58            name_string(&font, StringId::SUBFAMILY_NAME).unwrap_or_else(|| "Regular".into());
59        Ok(FontSource {
60            bytes,
61            family: family.to_lowercase(),
62            name,
63            style,
64            declared: None,
65        })
66    }
67}
68
69/// The slope and weight a face is matched at: the two axes of CSS
70/// font matching the engine supports.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72pub struct FaceAttributes {
73    /// True for both italic and oblique cuts; the engine draws no
74    /// distinction a book needs.
75    pub italic: bool,
76    /// Weight on the CSS 1–1000 scale.
77    pub weight: u16,
78}
79
80impl FaceAttributes {
81    /// Upright, regular: what a face is when nothing says otherwise.
82    pub const REGULAR: FaceAttributes = FaceAttributes {
83        italic: false,
84        weight: 400,
85    };
86}
87
88/// One axis of a variable face, pinned: the tag and the user-space
89/// coordinate this face sits at.
90///
91/// A face at its family's default location has none — there is
92/// nothing to pin, and an instanced subset of the default is just
93/// the default.
94#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
95pub struct AxisSetting {
96    /// The four-byte OpenType axis tag, e.g. `wght`.
97    #[serde(with = "tag")]
98    pub tag: [u8; 4],
99    /// The coordinate in the axis's own units.
100    pub value: f32,
101}
102
103/// An axis tag as the four characters it is written as, rather than
104/// the four numbers it is stored as. A painter passes it straight to
105/// `font-variation-settings`.
106mod tag {
107    use serde::de::{Error, Unexpected};
108    use serde::{Deserialize, Deserializer, Serializer};
109
110    pub fn serialize<S: Serializer>(tag: &[u8; 4], serializer: S) -> Result<S::Ok, S::Error> {
111        serializer.serialize_str(&String::from_utf8_lossy(tag))
112    }
113
114    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<[u8; 4], D::Error> {
115        let text = String::deserialize(deserializer)?;
116        text.as_bytes()
117            .try_into()
118            .map_err(|_| D::Error::invalid_value(Unexpected::Str(&text), &"four bytes"))
119    }
120}
121
122/// The face a request for a family, slope and weight resolved to,
123/// and what that face actually is.
124///
125/// The two differ when the family has no cut at the slope or weight
126/// asked for; the caller decides whether that is worth a diagnostic.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct FaceMatch {
129    /// The registry id to shape and paint with.
130    pub id: u16,
131    /// What that face is, which need not be what was asked for.
132    pub attributes: FaceAttributes,
133}
134
135/// A generic family keyword, as in CSS.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
137#[serde(rename_all = "snake_case")]
138pub enum GenericFamily {
139    /// `serif`
140    Serif,
141    /// `sans-serif`
142    SansSerif,
143    /// `monospace`
144    Monospace,
145}
146
147impl GenericFamily {
148    /// The keyword as it appears in stylesheets.
149    pub fn keyword(self) -> &'static str {
150        match self {
151            GenericFamily::Serif => "serif",
152            GenericFamily::SansSerif => "sans-serif",
153            GenericFamily::Monospace => "monospace",
154        }
155    }
156
157    /// Parses a stylesheet keyword, case-insensitively; `None` means
158    /// the value isn't a generic.
159    pub fn parse(keyword: &str) -> Option<Self> {
160        match keyword.to_ascii_lowercase().as_str() {
161            "serif" => Some(GenericFamily::Serif),
162            "sans-serif" | "sans" => Some(GenericFamily::SansSerif),
163            "monospace" | "mono" => Some(GenericFamily::Monospace),
164            _ => None,
165        }
166    }
167}
168
169/// One shaped glyph: its id, its advance, and the byte offset of its
170/// cluster in the shaped string — the bridge between shaping output
171/// and text-anchored break opportunities.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct ShapedGlyph {
174    /// Glyph id in the face that shaped it.
175    pub id: u32,
176    /// Horizontal advance in font units.
177    pub x_advance: u32,
178    /// Byte index into the shaped string where this glyph's cluster
179    /// begins.
180    pub cluster: u32,
181}
182
183/// One OpenType feature the run asks the face for: the tag and the
184/// value, where 0 turns the feature off and 1 turns it on. A feature
185/// with alternates takes the number of the alternate.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
187pub struct FeatureSetting {
188    /// The four-byte OpenType feature tag, e.g. `onum`.
189    #[serde(with = "tag")]
190    pub tag: [u8; 4],
191    /// What the feature is set to.
192    pub value: u32,
193}
194
195impl FeatureSetting {
196    /// A setting of `tag` at `value`.
197    pub const fn new(tag: [u8; 4], value: u32) -> FeatureSetting {
198        FeatureSetting { tag, value }
199    }
200}
201
202/// What one run asks the face for beyond the default set: the
203/// engine's own `smcp`, and the features the sheet asked for.
204#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
205pub struct FeatureSet {
206    /// `smcp`: the face's own small capitals, from
207    /// `font-variant-caps`.
208    pub small_caps: bool,
209    /// What the sheet asked for, from `font-feature-settings` and the
210    /// `font-variant` longhands, in the order the shaper reads them.
211    /// A later setting of one tag wins over an earlier one.
212    pub settings: Vec<FeatureSetting>,
213}
214
215/// The OpenType features a run is shaped with, beyond the ones the
216/// shaper turns on for every run.
217///
218/// These travel on the run. A painter that draws glyphs has them
219/// already; one that draws characters has to ask the face for the
220/// same features, or it draws different glyphs at the positions the
221/// engine measured.
222///
223/// A book is set in a handful of these and holds hundreds of
224/// thousands of runs, so the runs share one set rather than each
225/// carrying a list of its own.
226#[derive(Debug, Clone, Default, PartialEq, Eq)]
227pub struct Features(Option<Arc<FeatureSet>>);
228
229impl Serialize for Features {
230    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
231        self.0.as_deref().serialize(serializer)
232    }
233}
234
235impl<'de> Deserialize<'de> for Features {
236    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Features, D::Error> {
237        let set = Option::<FeatureSet>::deserialize(deserializer)?;
238        Ok(Features(set.map(Arc::new)))
239    }
240}
241
242impl Features {
243    /// Nothing beyond the default set.
244    pub const NONE: Features = Features(None);
245
246    /// The features of one run: `smcp` where the face draws the small
247    /// capitals, and whatever the sheet asked for.
248    pub fn new(small_caps: bool, settings: Vec<FeatureSetting>) -> Features {
249        match (small_caps, settings.is_empty()) {
250            (false, true) => Features::NONE,
251            (true, true) => Features(Some(small_caps_only())),
252            _ => Features(Some(Arc::new(FeatureSet {
253                small_caps,
254                settings,
255            }))),
256        }
257    }
258
259    /// Whether the run is set in the face's own small capitals.
260    pub fn small_caps(&self) -> bool {
261        self.0.as_ref().is_some_and(|set| set.small_caps)
262    }
263
264    /// What the sheet asked for, in the order the shaper reads it.
265    pub fn settings(&self) -> &[FeatureSetting] {
266        self.0.as_ref().map_or(&[], |set| &set.settings)
267    }
268}
269
270/// The set a run of small capitals asks for, shared by every such
271/// run: a chapter opening in small capitals is one set, not one per
272/// line.
273fn small_caps_only() -> Arc<FeatureSet> {
274    static SET: std::sync::OnceLock<Arc<FeatureSet>> = std::sync::OnceLock::new();
275    SET.get_or_init(|| {
276        Arc::new(FeatureSet {
277            small_caps: true,
278            settings: Vec::new(),
279        })
280    })
281    .clone()
282}
283
284/// A font's identity in the engine's output.
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct FontRefEntry {
287    /// Family for matching (lowercase).
288    pub family: String,
289    /// Face name (name id 4).
290    pub name: String,
291    /// Style name (name id 2).
292    pub style: String,
293    /// The slope and weight this face answers for.
294    pub attributes: FaceAttributes,
295    /// Where on its file's axes this face sits, in user space. Empty
296    /// for a static face and for a variable one at its default
297    /// location. A painter that instances the file itself pins these
298    /// axes, and draws the cut the run was shaped at rather than the
299    /// file's default.
300    pub variations: Vec<AxisSetting>,
301}
302
303/// Font metrics in font units.
304///
305/// Ascender/descender/line gap follow the OS/2 typographic values
306/// when present, hhea otherwise. Descender is negative, per the
307/// tables.
308#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
309pub struct FontMetricsTable {
310    /// Font design units per em; everything else here is in them.
311    pub units_per_em: u16,
312    /// Height above the baseline.
313    pub ascender: i16,
314    /// Depth below the baseline, negative.
315    pub descender: i16,
316    /// Leading the face asks for between lines.
317    pub line_gap: i16,
318    /// Height of a capital above the baseline. Zero when the face
319    /// declares none, which is what a drop cap has to fall back from.
320    pub cap_height: i16,
321    /// Where the face puts an underline, from its `post` table.
322    /// `None` when the face declares none.
323    pub underline: Option<Rule>,
324    /// Where it puts a strikethrough, from its `OS/2` table. `None`
325    /// when the face declares none.
326    pub strikeout: Option<Rule>,
327}
328
329/// Where a face puts one rule across its text, in font units.
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
331pub struct Rule {
332    /// The top of the rule, from the baseline, positive above it. An
333    /// underline is below the baseline, so its offset is negative.
334    pub offset: i16,
335    /// How thick the rule is.
336    pub thickness: i16,
337}
338
339/// One registered face: bytes plus everything decoded from them.
340///
341/// A variable file registers one face per named instance, so bytes
342/// and the shaper's per-file tables are shared and only the location
343/// differs.
344struct Face {
345    bytes: Arc<Vec<u8>>,
346    identity: FontRefEntry,
347    metrics: FontMetricsTable,
348    /// Decoded once, at registration; the shaper reads through this.
349    shaper_data: Arc<ShaperData>,
350    /// Where on the file's axes this face sits, normalized. Default
351    /// for a static file. The same location in user space travels on
352    /// the identity, for painters that instance the file themselves.
353    location: Location,
354    /// The shaper's view of `location`; `None` at the default, where
355    /// there is nothing to vary.
356    instance: Option<ShaperInstance>,
357    /// Whether the file draws small capitals of its own.
358    small_caps: bool,
359    /// The feature tags the file carries, sorted. A tag outside this
360    /// is one the face has nothing for, which a sheet asking for it
361    /// is told.
362    features: Vec<[u8; 4]>,
363}
364
365/// The registry: assigns `font_id`s, hands shaper access and metrics
366/// to the pipeline, and maps generic families to bundled faces.
367///
368/// Ids are dense and sequential from 0 — an id is an index.
369#[derive(Default)]
370pub struct FontRegistry {
371    faces: Vec<Face>,
372    by_family: HashMap<String, Vec<u16>>,
373    generics: HashMap<GenericFamily, u16>,
374}
375
376impl FontRegistry {
377    /// An empty registry.
378    pub fn new() -> Self {
379        Self::default()
380    }
381
382    /// Registers a font file and returns the ids of the faces it
383    /// yielded, in the order the file names them.
384    ///
385    /// A variable file yields one face per named instance: the cuts
386    /// the family says it has. Matching is then a lookup over faces
387    /// with fixed slopes and weights, which is what lets the cascade
388    /// resolve a face without a registry it may write to.
389    ///
390    /// Decodes metrics eagerly: a font that can't be parsed fails
391    /// here, once, rather than at first shape.
392    pub fn add(&mut self, mut source: FontSource) -> Result<Vec<u16>, FontError> {
393        let bytes = Arc::new(std::mem::take(&mut source.bytes));
394        let font = skrifa::FontRef::new(&bytes).map_err(|_| FontError::Parse)?;
395        let shaper_data = Arc::new(ShaperData::new(&font));
396        let harf = HarfFontRef::new(&bytes).map_err(|_| FontError::Parse)?;
397
398        let mut ids = Vec::new();
399        for cut in cuts(&font, &source) {
400            let id = self.faces.len() as u16;
401            let instance = (!cut.variations.is_empty())
402                .then(|| ShaperInstance::from_coords(&harf, cut.location.coords().iter().copied()));
403            self.by_family
404                .entry(source.family.clone())
405                .or_default()
406                .push(id);
407            self.faces.push(Face {
408                bytes: bytes.clone(),
409                identity: FontRefEntry {
410                    family: source.family.clone(),
411                    name: cut.name,
412                    style: cut.style,
413                    attributes: cut.attributes,
414                    variations: cut.variations,
415                },
416                metrics: read_metrics(&font, &cut.location),
417                small_caps: draws_small_caps(&harf, &shaper_data, instance.as_ref()),
418                features: feature_tags(&font),
419                shaper_data: shaper_data.clone(),
420                location: cut.location,
421                instance,
422            });
423            ids.push(id);
424        }
425        Ok(ids)
426    }
427
428    /// Drops every face from id `len` on, and every family and
429    /// generic that named one.
430    pub(crate) fn truncate(&mut self, len: usize) {
431        self.faces.truncate(len);
432        for ids in self.by_family.values_mut() {
433            ids.retain(|id| usize::from(*id) < len);
434        }
435        self.by_family.retain(|_, ids| !ids.is_empty());
436        self.generics.retain(|_, id| usize::from(*id) < len);
437    }
438
439    /// Number of registered faces. Ids are `0..len`.
440    pub fn len(&self) -> usize {
441        self.faces.len()
442    }
443
444    /// True when no face is registered.
445    pub fn is_empty(&self) -> bool {
446        self.faces.is_empty()
447    }
448
449    /// Binds a generic keyword to a registered family's first face.
450    /// The keyword must not already be bound.
451    pub fn map_generic(&mut self, generic: GenericFamily, family: &str) -> Option<u16> {
452        let id = self.by_family.get(family)?.first().copied()?;
453        if self.generics.insert(generic, id).is_some() {
454            panic!("generic family {generic:?} already mapped");
455        }
456        Some(id)
457    }
458
459    /// The id a generic keyword resolves to.
460    pub fn generic(&self, generic: GenericFamily) -> Option<u16> {
461        self.generics.get(&generic).copied()
462    }
463
464    /// The id of the first face whose family matches (case-insensitive
465    /// compare against the lowercased registry key).
466    pub fn by_family(&self, family: &str) -> Option<u16> {
467        self.by_family
468            .get(&family.to_lowercase())
469            .and_then(|ids| ids.first().copied())
470    }
471
472    /// The face of `family` that best answers a slope and weight.
473    ///
474    /// Matching follows CSS: slope decides first — a family with an
475    /// italic cut never answers an italic request with the upright —
476    /// and weight is then chosen by the desired-weight rules, which
477    /// look up before they look down in the text range and away from
478    /// it elsewhere. The match reports what it actually found, so a
479    /// caller can say when the family had nothing at the slope asked
480    /// for.
481    pub fn select(&self, family: &str, want: FaceAttributes) -> Option<FaceMatch> {
482        let ids = self.by_family.get(&family.to_lowercase())?;
483        let attributes = |id: &u16| self.faces[*id as usize].identity.attributes;
484        // A family with nothing at the slope asked for answers with
485        // everything it has; one that has it answers only with that.
486        let has_slope = ids.iter().any(|id| attributes(id).italic == want.italic);
487        let id = ids
488            .iter()
489            .filter(|id| !has_slope || attributes(id).italic == want.italic)
490            .min_by_key(|id| (weight_rank(attributes(id).weight, want.weight), **id))
491            .copied()?;
492        Some(FaceMatch {
493            id,
494            attributes: attributes(&id),
495        })
496    }
497
498    /// Identity of a face, for the output font table.
499    pub fn font_ref(&self, id: u16) -> Option<&FontRefEntry> {
500        self.faces.get(id as usize).map(|face| &face.identity)
501    }
502
503    /// Metrics of a face, in font units.
504    pub fn metrics(&self, id: u16) -> Option<FontMetricsTable> {
505        self.faces.get(id as usize).map(|face| face.metrics)
506    }
507
508    /// Whether a face draws small capitals of its own. A face that
509    /// does not has to have them synthesized from its capitals.
510    pub fn has_small_caps(&self, id: u16) -> bool {
511        self.faces
512            .get(id as usize)
513            .is_some_and(|face| face.small_caps)
514    }
515
516    /// Whether a face carries one feature. A sheet that asks for a
517    /// feature the face has nothing for gets the run set without it.
518    pub fn has_feature(&self, id: u16, tag: [u8; 4]) -> bool {
519        self.faces
520            .get(id as usize)
521            .is_some_and(|face| face.features.binary_search(&tag).is_ok())
522    }
523
524    /// The raw bytes of a face, for embedding at write time.
525    ///
526    /// Shared: the faces a variable file yielded are one file, and a
527    /// painter embeds one copy of it however many cuts it draws.
528    pub fn bytes(&self, id: u16) -> Option<Arc<Vec<u8>>> {
529        self.faces.get(id as usize).map(|face| face.bytes.clone())
530    }
531
532    /// Where on its file's axes a face sits, in user space. Empty
533    /// for a static face and for a variable one at its default
534    /// location.
535    pub fn variations(&self, id: u16) -> Option<&[AxisSetting]> {
536        self.faces
537            .get(id as usize)
538            .map(|face| face.identity.variations.as_slice())
539    }
540
541    /// Advance width of one glyph, in font units.
542    pub fn advance_width(&self, id: u16, glyph: u32) -> Option<u16> {
543        let face = self.faces.get(id as usize)?;
544        let font = HarfFontRef::new(&face.bytes).ok()?;
545        let glyph_metrics =
546            GlyphMetrics::new(&font, Size::unscaled(), LocationRef::from(&face.location));
547        glyph_metrics
548            .advance_width(GlyphId::new(glyph))
549            .map(|w| w.round() as u16)
550    }
551
552    /// The advance widths of a run of glyphs, in font units.
553    ///
554    /// Shaped output is glyph ids; measuring them must not
555    /// re-decode the font per glyph, so this batches.
556    pub fn advance_widths(&self, id: u16, glyphs: &[u32]) -> Option<Vec<u16>> {
557        let face = self.faces.get(id as usize)?;
558        let font = HarfFontRef::new(&face.bytes).ok()?;
559        let glyph_metrics =
560            GlyphMetrics::new(&font, Size::unscaled(), LocationRef::from(&face.location));
561        Some(
562            glyphs
563                .iter()
564                .map(|g| {
565                    glyph_metrics
566                        .advance_width(GlyphId::new(*g))
567                        .map(|w| w.round() as u16)
568                        .unwrap_or(0)
569                })
570                .collect(),
571        )
572    }
573
574    /// Maps one character to its nominal glyph, in font units.
575    pub fn char_glyph(&self, id: u16, ch: char) -> Option<u32> {
576        let face = self.faces.get(id as usize)?;
577        let font = HarfFontRef::new(&face.bytes).ok()?;
578        let charmap = font.charmap();
579        charmap.map(ch).map(|g| g.to_u32())
580    }
581
582    /// Shapes a string with a registered face, in font units.
583    ///
584    /// Returns per-glyph `ShapedGlyph` — the raw material of line
585    /// layout. Clusters index the shaped string, so callers can map
586    /// text offsets (break opportunities) back to glyphs; offsets data
587    /// stays in the shaper's buffer.
588    pub fn shape(&self, id: u16, text: &str) -> Option<Vec<ShapedGlyph>> {
589        self.shape_with(id, text, &Features::NONE)
590    }
591
592    /// The same, with the features a style asked for turned on.
593    pub fn shape_with(&self, id: u16, text: &str, features: &Features) -> Option<Vec<ShapedGlyph>> {
594        let face = self.faces.get(id as usize)?;
595        let font = HarfFontRef::new(&face.bytes).ok()?;
596        Some(shape_text(
597            &font,
598            &face.shaper_data,
599            face.instance.as_ref(),
600            text,
601            features,
602        ))
603    }
604}
605
606/// One shaping call: the buffer the engine always sets up the same
607/// way, plus whatever features the run asked for.
608fn shape_text(
609    font: &HarfFontRef<'_>,
610    data: &ShaperData,
611    instance: Option<&ShaperInstance>,
612    text: &str,
613    features: &Features,
614) -> Vec<ShapedGlyph> {
615    let shaper = data.shaper(font).instance(instance).build();
616    let mut buffer = UnicodeBuffer::new();
617    buffer.push_str(text);
618    buffer.set_cluster_level(BufferClusterLevel::MonotoneCharacters);
619    buffer.set_script(script::LATIN);
620    buffer.set_direction(harfrust::Direction::LeftToRight);
621    buffer.set_language(Language::new("en").unwrap());
622    let mut wanted = Vec::new();
623    if features.small_caps() {
624        wanted.push(Feature::new(Tag::new(b"smcp"), 1, ..));
625    }
626    for setting in features.settings() {
627        wanted.push(Feature::new(Tag::new(&setting.tag), setting.value, ..));
628    }
629    let shaped = shaper.shape(
630        buffer,
631        // unscaled: font units
632        harfrust::ShapeOptions::new().features(&wanted),
633    );
634    let positions = shaped.glyph_positions();
635    shaped
636        .glyph_infos()
637        .iter()
638        .zip(positions)
639        .map(|(info, pos)| ShapedGlyph {
640            id: info.glyph_id,
641            x_advance: pos.x_advance as u32,
642            cluster: info.cluster,
643        })
644        .collect()
645}
646
647/// Every feature tag a file lists, from both substitution and
648/// positioning, sorted for lookup.
649fn feature_tags(font: &skrifa::FontRef) -> Vec<[u8; 4]> {
650    let mut tags = Vec::new();
651    if let Ok(gsub) = font.gsub() {
652        collect_features(gsub.feature_list(), &mut tags);
653    }
654    if let Ok(gpos) = font.gpos() {
655        collect_features(gpos.feature_list(), &mut tags);
656    }
657    tags.sort_unstable();
658    tags.dedup();
659    tags
660}
661
662/// The tags of one feature list, added to `tags`.
663fn collect_features(
664    list: Result<skrifa::raw::tables::layout::FeatureList<'_>, skrifa::raw::ReadError>,
665    tags: &mut Vec<[u8; 4]>,
666) {
667    let Ok(list) = list else {
668        return;
669    };
670    tags.extend(
671        list.feature_records()
672            .iter()
673            .map(|record| record.feature_tag().into_bytes()),
674    );
675}
676
677/// Whether a face has small capitals of its own: shape a lowercase
678/// letter with `smcp` and see whether the face draws something else
679/// for it. A file that lists the feature and substitutes nothing has
680/// no small capitals to offer.
681fn draws_small_caps(
682    font: &HarfFontRef<'_>,
683    data: &ShaperData,
684    instance: Option<&ShaperInstance>,
685) -> bool {
686    const PROBE: &str = "a";
687    let small_caps = Features::new(true, Vec::new());
688    let plain = shape_text(font, data, instance, PROBE, &Features::NONE);
689    let small = shape_text(font, data, instance, PROBE, &small_caps);
690    plain.iter().map(|g| g.id).ne(small.iter().map(|g| g.id))
691}
692
693/// One face a font file yields: an identity, a location on the
694/// file's axes, and the slope and weight it is matched at.
695struct Cut {
696    name: String,
697    style: String,
698    attributes: FaceAttributes,
699    location: Location,
700    variations: Vec<AxisSetting>,
701}
702
703/// The faces a font file yields.
704///
705/// A variable file yields its named instances, which is the file's
706/// own statement of the cuts it offers. A static one — or one whose
707/// stylesheet already declared what it is — yields a single face at
708/// its default location.
709fn cuts(font: &skrifa::FontRef, source: &FontSource) -> Vec<Cut> {
710    let attributes = font.attributes();
711    let default = FaceAttributes {
712        italic: attributes.style != SlopeStyle::Normal,
713        weight: attributes.weight.value().round().clamp(1.0, 1000.0) as u16,
714    };
715    let whole_file = |attributes| {
716        vec![Cut {
717            name: source.name.clone(),
718            style: source.style.clone(),
719            attributes,
720            location: Location::default(),
721            variations: Vec::new(),
722        }]
723    };
724    if let Some(declared) = source.declared {
725        return whole_file(declared);
726    }
727    let axes: Vec<_> = font.axes().iter().collect();
728    let family = name_string(font, StringId::TYPOGRAPHIC_FAMILY_NAME)
729        .or_else(|| name_string(font, StringId::FAMILY_NAME))
730        .unwrap_or_else(|| source.name.clone());
731    let instances: Vec<Cut> = font
732        .named_instances()
733        .iter()
734        .filter_map(|instance| {
735            let style = name_string(font, instance.subfamily_name_id())?;
736            let settings: Vec<AxisSetting> = axes
737                .iter()
738                .zip(instance.user_coords())
739                .map(|(axis, value)| AxisSetting {
740                    tag: axis.tag().into_bytes(),
741                    value,
742                })
743                .collect();
744            let location = instance.location();
745            let at_default = location.coords().iter().all(|coord| coord.to_f32() == 0.0);
746            Some(Cut {
747                name: format!("{family} {style}"),
748                attributes: FaceAttributes {
749                    italic: default.italic || slanted(&settings),
750                    weight: setting(&settings, b"wght")
751                        .map(|weight| weight.round().clamp(1.0, 1000.0) as u16)
752                        .unwrap_or(default.weight),
753                },
754                style,
755                variations: if at_default { Vec::new() } else { settings },
756                location,
757            })
758        })
759        .collect();
760    if instances.is_empty() {
761        whole_file(default)
762    } else {
763        instances
764    }
765}
766
767/// The value of one axis in a face's location.
768fn setting(settings: &[AxisSetting], tag: &[u8; 4]) -> Option<f32> {
769    settings
770        .iter()
771        .find(|setting| setting.tag == *tag)
772        .map(|setting| setting.value)
773}
774
775/// Whether a location leans: a family that varies its slope says so
776/// on `ital` or `slnt` rather than in a separate file.
777fn slanted(settings: &[AxisSetting]) -> bool {
778    setting(settings, b"ital").is_some_and(|value| value >= 0.5)
779        || setting(settings, b"slnt").is_some_and(|value| value != 0.0)
780}
781
782/// Where a candidate weight sits in CSS's order of preference for a
783/// desired one: lower is better, and the tier dominates the distance.
784///
785/// The rules are asymmetric on purpose — in the text range a heavier
786/// cut is preferred to a lighter one, and outside it the search runs
787/// away from 400 first.
788fn weight_rank(candidate: u16, desired: u16) -> (u8, u16) {
789    if (400..=500).contains(&desired) {
790        if candidate >= desired && candidate <= 500 {
791            (0, candidate - desired)
792        } else if candidate < desired {
793            (1, desired - candidate)
794        } else {
795            (2, candidate - 500)
796        }
797    } else if desired < 400 {
798        if candidate <= desired {
799            (0, desired - candidate)
800        } else {
801            (1, candidate - desired)
802        }
803    } else if candidate >= desired {
804        (0, candidate - desired)
805    } else {
806        (1, desired - candidate)
807    }
808}
809
810fn name_string<'a>(font: &skrifa::FontRef<'a>, id: StringId) -> Option<String> {
811    font.localized_strings(id)
812        .english_or_first()
813        .map(|s| s.to_string())
814}
815
816fn read_metrics(font: &skrifa::FontRef, location: &Location) -> FontMetricsTable {
817    let metrics = Metrics::new(font, Size::unscaled(), LocationRef::from(location));
818    FontMetricsTable {
819        units_per_em: metrics.units_per_em,
820        ascender: metrics.ascent as i16,
821        descender: metrics.descent as i16,
822        line_gap: metrics.leading as i16,
823        cap_height: metrics.cap_height.unwrap_or_default() as i16,
824        underline: metrics.underline.map(rule),
825        strikeout: metrics.strikeout.map(rule),
826    }
827}
828
829/// One decoration of a face, rounded to font units.
830fn rule(decoration: skrifa::metrics::Decoration) -> Rule {
831    Rule {
832        offset: decoration.offset as i16,
833        thickness: decoration.thickness as i16,
834    }
835}
836
837/// What can go wrong loading a font.
838#[derive(Debug, thiserror::Error)]
839pub enum FontError {
840    /// The bytes are not a font this build can read.
841    #[error("font data could not be parsed")]
842    Parse,
843    /// A font with no family name has nothing to register under.
844    #[error("font has no family name")]
845    MissingName,
846}
847
848/// The bundled upright text face: EB Garamond (SIL OFL 1.1).
849pub const BUNDLED_FONT: &[u8] = include_bytes!("../fonts/EBGaramond-VF.ttf");
850
851/// Its italic companion, from the same release: emphasis is a
852/// different set of outlines, not a slanted copy of these.
853pub const BUNDLED_ITALIC: &[u8] = include_bytes!("../fonts/EBGaramond-Italic-VF.ttf");
854
855/// A registry with the bundled family registered — both slopes, and
856/// every weight each file names — and all generics mapped to it.
857pub fn bundled_registry() -> Result<FontRegistry, FontError> {
858    let mut registry = FontRegistry::new();
859    for bytes in [BUNDLED_FONT, BUNDLED_ITALIC] {
860        registry.add(FontSource::from_bytes(bytes.to_vec())?)?;
861    }
862    for generic in [
863        GenericFamily::Serif,
864        GenericFamily::SansSerif,
865        GenericFamily::Monospace,
866    ] {
867        registry
868            .map_generic(generic, "eb garamond")
869            .expect("bundled face is registered");
870    }
871    Ok(registry)
872}
873
874/// The bundled face with its glyph substitutions taken away: the
875/// table's tag is blanked, so every offset in the file still stands
876/// and nothing in it can substitute a glyph. This stands in for a
877/// face that never had small capitals.
878#[cfg(test)]
879pub(crate) fn registry_without_substitutions() -> FontRegistry {
880    let mut bytes = BUNDLED_FONT.to_vec();
881    let tables = u16::from_be_bytes([bytes[4], bytes[5]]) as usize;
882    for table in 0..tables {
883        let at = 12 + 16 * table;
884        if &bytes[at..at + 4] == b"GSUB" {
885            bytes[at..at + 4].copy_from_slice(b"xsub");
886        }
887    }
888    let mut registry = FontRegistry::new();
889    let mut source = FontSource::from_bytes(bytes).expect("the face parses");
890    source.declared = Some(FaceAttributes::REGULAR);
891    registry.add(source).expect("the face registers");
892    for generic in [
893        GenericFamily::Serif,
894        GenericFamily::SansSerif,
895        GenericFamily::Monospace,
896    ] {
897        registry
898            .map_generic(generic, "eb garamond")
899            .expect("the face is registered");
900    }
901    registry
902}
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907
908    fn registry() -> FontRegistry {
909        bundled_registry().expect("bundled font parses")
910    }
911
912    /// Bytes enter, sequential ids come out, and the registry counts
913    /// what it registered.
914    #[test]
915    fn registration_assigns_sequential_ids() {
916        let mut registry = FontRegistry::new();
917        assert!(registry.is_empty());
918        let mut source = FontSource::from_bytes(BUNDLED_FONT.to_vec()).unwrap();
919        source.declared = Some(FaceAttributes::REGULAR);
920        let id = registry.add(source.clone()).unwrap();
921        assert_eq!(id, vec![0]);
922        let second = registry.add(source).unwrap();
923        assert_eq!(second, vec![1]);
924        assert_eq!(registry.len(), 2);
925    }
926
927    /// A variable file registers the cuts it names, each pinned to
928    /// its own place on the axis. The default instance pins nothing:
929    /// there is no instancing to do at the location the file already
930    /// sits at.
931    #[test]
932    fn variable_files_register_their_named_instances() {
933        let registry = registry();
934        let cuts: Vec<(&str, bool, u16)> = (0..registry.len() as u16)
935            .map(|id| {
936                let entry = registry.font_ref(id).unwrap();
937                (
938                    entry.style.as_str(),
939                    entry.attributes.italic,
940                    entry.attributes.weight,
941                )
942            })
943            .collect();
944        assert_eq!(
945            cuts,
946            vec![
947                ("Regular", false, 400),
948                ("Medium", false, 500),
949                ("SemiBold", false, 600),
950                ("Bold", false, 700),
951                ("ExtraBold", false, 800),
952                ("Italic", true, 400),
953                ("Medium Italic", true, 500),
954                ("SemiBold Italic", true, 600),
955                ("Bold Italic", true, 700),
956                ("ExtraBold Italic", true, 800),
957            ],
958        );
959        assert_eq!(registry.variations(0).unwrap(), &[]);
960        assert_eq!(
961            registry.variations(3).unwrap(),
962            &[AxisSetting {
963                tag: *b"wght",
964                value: 700.0,
965            }],
966        );
967        // The instance is shaped, not just labelled: bold outlines
968        // are wider than regular ones at the same size.
969        let regular: u32 = registry
970            .shape(0, "quantities")
971            .unwrap()
972            .iter()
973            .map(|g| g.x_advance)
974            .sum();
975        let bold: u32 = registry
976            .shape(3, "quantities")
977            .unwrap()
978            .iter()
979            .map(|g| g.x_advance)
980            .sum();
981        assert!(
982            bold > regular,
983            "bold ({bold}) is no wider than regular ({regular})"
984        );
985    }
986
987    /// Slope decides first and weight second: the family has an
988    /// italic cut, so an italic request never comes back upright,
989    /// and a bold italic request lands on the bold italic instance.
990    #[test]
991    fn faces_match_by_slope_then_weight() {
992        let registry = registry();
993        let select = |italic, weight| {
994            registry
995                .select("EB Garamond", FaceAttributes { italic, weight })
996                .unwrap()
997        };
998        assert_eq!(select(false, 400).id, 0);
999        assert_eq!(select(true, 400).id, 5);
1000        assert_eq!(select(false, 700).id, 3);
1001        assert_eq!(select(true, 700).id, 8, "no bold italic cut");
1002        // CSS's desired-weight rules: in the text range the search
1003        // runs up first, and outside it away from the range.
1004        assert_eq!(select(false, 450).attributes.weight, 500);
1005        assert_eq!(select(false, 100).attributes.weight, 400);
1006        assert_eq!(select(false, 1000).attributes.weight, 800);
1007        assert_eq!(registry.select("nowhere", FaceAttributes::REGULAR), None);
1008    }
1009
1010    /// A family with one slope answers for both, and says which one
1011    /// it handed back.
1012    #[test]
1013    fn a_match_reports_the_face_it_settled_for() {
1014        let mut registry = FontRegistry::new();
1015        registry
1016            .add(FontSource::from_bytes(BUNDLED_FONT.to_vec()).unwrap())
1017            .unwrap();
1018        let found = registry
1019            .select(
1020                "eb garamond",
1021                FaceAttributes {
1022                    italic: true,
1023                    weight: 400,
1024                },
1025            )
1026            .unwrap();
1027        assert_eq!(found.id, 0);
1028        assert!(!found.attributes.italic, "there is no italic cut to find");
1029    }
1030
1031    /// A stylesheet that declares what its source is overrides the
1032    /// file, and registers it as the one cut it was called: a sheet
1033    /// naming one weight is not describing five.
1034    #[test]
1035    fn a_declared_face_registers_as_the_one_cut_it_was_called() {
1036        let mut registry = FontRegistry::new();
1037        let mut source = FontSource::from_bytes(BUNDLED_FONT.to_vec()).unwrap();
1038        source.family = "house".into();
1039        source.declared = Some(FaceAttributes {
1040            italic: true,
1041            weight: 700,
1042        });
1043        registry.add(source).unwrap();
1044        assert_eq!(registry.len(), 1);
1045        assert_eq!(
1046            registry.font_ref(0).unwrap().attributes,
1047            FaceAttributes {
1048                italic: true,
1049                weight: 700,
1050            },
1051        );
1052    }
1053
1054    /// Metrics come out in font units with the sign convention of the
1055    /// source tables.
1056    #[test]
1057    fn metrics_parse_in_font_units() {
1058        let registry = registry();
1059        let metrics = registry.metrics(0).unwrap();
1060        assert_eq!(metrics.units_per_em, 1000);
1061        assert_eq!(metrics.ascender, 1007);
1062        assert_eq!(metrics.descender, -298);
1063        assert_eq!(metrics.line_gap, 0);
1064    }
1065
1066    /// Part: the underline comes off the `post` table and the
1067    /// strikethrough off `OS/2`, so a rule drawn across the text
1068    /// sits where the designer put it.
1069    #[test]
1070    fn metrics_carry_the_underline_and_the_strikeout() {
1071        let registry = registry();
1072        let metrics = registry.metrics(0).unwrap();
1073        let underline = metrics.underline.expect("the face declares an underline");
1074        let strikeout = metrics.strikeout.expect("the face declares a strikeout");
1075        assert!(
1076            underline.offset < 0,
1077            "the underline sits above the baseline: {underline:?}",
1078        );
1079        assert!(underline.thickness > 0, "{underline:?}");
1080        assert!(
1081            strikeout.offset > 0,
1082            "the strikeout sits below the baseline: {strikeout:?}",
1083        );
1084        assert!(strikeout.thickness > 0, "{strikeout:?}");
1085    }
1086
1087    /// hmtx advances, read straight from the tables.
1088    #[test]
1089    fn advance_widths_match_hmtx() {
1090        let registry = registry();
1091        // gid 490 = 'o' (495 units), gid 991 = 'f_f_i' (776), gid
1092        // 2426 = 'space' (200) — cross-checked against ttx.
1093        let widths = registry.advance_widths(0, &[490, 991, 2426]).unwrap();
1094        assert_eq!(widths, vec![495, 776, 200]);
1095        assert_eq!(registry.advance_width(0, 490).unwrap(), 495);
1096    }
1097
1098    /// cmap: characters map to nominal glyphs.
1099    #[test]
1100    fn charmap_maps_to_nominal_glyphs() {
1101        let registry = registry();
1102        assert_eq!(registry.char_glyph(0, 'o'), Some(490));
1103        assert_eq!(registry.char_glyph(0, 'A'), Some(1));
1104        assert_eq!(registry.char_glyph(0, '\u{10FFF0}'), None);
1105    }
1106
1107    /// Family and name strings come from the name table (typographic
1108    /// family preferred).
1109    #[test]
1110    fn identity_comes_from_the_name_table() {
1111        let registry = registry();
1112        let entry = registry.font_ref(0).unwrap();
1113        assert_eq!(entry.family, "eb garamond");
1114        assert_eq!(entry.name, "EB Garamond Regular");
1115        assert_eq!(entry.style, "Regular");
1116    }
1117
1118    /// Generic keywords resolve to the bundled face.
1119    #[test]
1120    fn generic_families_map_to_bundled_defaults() {
1121        let registry = registry();
1122        for generic in [
1123            GenericFamily::Serif,
1124            GenericFamily::SansSerif,
1125            GenericFamily::Monospace,
1126        ] {
1127            assert_eq!(registry.generic(generic), Some(0));
1128        }
1129        assert_eq!(GenericFamily::parse("SERIF"), Some(GenericFamily::Serif));
1130        assert_eq!(GenericFamily::parse("fancy"), None);
1131    }
1132
1133    /// Family lookup by name finds the face, any case.
1134    #[test]
1135    fn families_resolve_case_insensitively() {
1136        let registry = registry();
1137        assert_eq!(registry.by_family("EB Garamond"), Some(0));
1138        assert_eq!(registry.by_family("nope"), None);
1139    }
1140
1141    /// Unparseable bytes are rejected at source build, not at first
1142    /// shape.
1143    #[test]
1144    fn garbage_bytes_fail_at_registration() {
1145        let err = FontSource::from_bytes(vec![0; 64]).unwrap_err();
1146        assert!(matches!(err, FontError::Parse));
1147    }
1148
1149    /// The acceptance run: harfrust output must equal hb-shape's, for
1150    /// a string exercising liga, kern, and the qu pair.
1151    #[test]
1152    fn shaping_matches_hb_shape_reference() {
1153        let registry = registry();
1154        let shaped = registry.shape(0, "AVAToffice quantities").unwrap();
1155        let expected: &[(u32, u32)] = &[
1156            (1, 552),   // A (kerned)
1157            (113, 542), // V (kerned)
1158            (1, 597),   // A (kerned)
1159            (98, 565),  // T (kerned)
1160            (490, 495), // o
1161            (991, 776), // f_f_i (liga)
1162            (430, 387), // c — kern pulls 'u' left
1163            (440, 390), // u
1164            (2426, 200),
1165            (505, 522),
1166            (519, 527),
1167            (415, 399),
1168            (484, 528),
1169            (516, 314),
1170            (462, 245),
1171            (516, 314),
1172            (462, 245),
1173            (440, 390),
1174            (509, 323),
1175        ];
1176        assert_eq!(
1177            shaped
1178                .iter()
1179                .map(|g| (g.id, g.x_advance))
1180                .collect::<Vec<_>>(),
1181            expected,
1182            "harfrust disagrees with hb-shape on the reference string"
1183        );
1184    }
1185
1186    /// Part: the registry answers for the tags a face carries, so a
1187    /// sheet asking for one the face has nothing for can be told.
1188    #[test]
1189    fn a_face_answers_for_the_features_it_carries() {
1190        let registry = registry();
1191        for tag in [b"onum", b"ss01", b"sups", b"liga", b"smcp"] {
1192            assert!(registry.has_feature(0, *tag), "the face carries {tag:?}");
1193        }
1194        assert!(!registry.has_feature(0, *b"zero"));
1195        assert!(!registry.has_feature(99, *b"onum"), "no such face");
1196    }
1197
1198    /// Part: a setting the style asked for reaches the shaper, and
1199    /// draws the glyphs of that feature rather than the default ones.
1200    #[test]
1201    fn a_requested_feature_draws_the_glyphs_of_that_feature() {
1202        let registry = registry();
1203        let plain = registry.shape(0, "1805").unwrap();
1204        let old_style = Features::new(false, vec![FeatureSetting::new(*b"onum", 1)]);
1205        let shaped = registry.shape_with(0, "1805", &old_style).unwrap();
1206        assert_ne!(
1207            shaped.iter().map(|g| g.id).collect::<Vec<_>>(),
1208            plain.iter().map(|g| g.id).collect::<Vec<_>>(),
1209            "old-style figures draw other glyphs than the lining ones"
1210        );
1211    }
1212
1213    /// Part: a setting of zero turns off a feature the shaper would
1214    /// otherwise apply, so the ligature falls back to its letters.
1215    #[test]
1216    fn a_setting_of_zero_turns_off_a_default_feature() {
1217        let registry = registry();
1218        let ligatures_off = Features::new(false, vec![FeatureSetting::new(*b"liga", 0)]);
1219        let shaped = registry.shape_with(0, "office", &ligatures_off).unwrap();
1220        assert_eq!(shaped.len(), 6, "every letter draws its own glyph");
1221        assert!(
1222            registry.shape(0, "office").unwrap().len() < shaped.len(),
1223            "the ffi ligature forms when nothing turns it off"
1224        );
1225    }
1226
1227    /// Clusters index the shaped string: a ligature takes its first
1228    /// cluster, and clusters are monotone even where glyphs merge.
1229    #[test]
1230    fn clusters_index_the_input_text() {
1231        let registry = registry();
1232        let shaped = registry.shape(0, "AVAToffice quantities").unwrap();
1233        let clusters: Vec<u32> = shaped.iter().map(|g| g.cluster).collect();
1234        assert_eq!(
1235            clusters,
1236            vec![
1237                0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
1238            ],
1239            "f_f_i (cluster 5) absorbs 'f','f','i' (6, 7) into one glyph"
1240        );
1241    }
1242}