Skip to main content

yog_font/
format.rs

1//! The `.yfont` format: what a font is once built, whatever it was made
2//! from (TrueType, a picture of glyphs, pixels drawn in an editor, a
3//! script's rules). Yog Data, binary.
4//!
5//! Units are the font's own (`units_per_em` to an em), `y` up from the
6//! baseline. A glyph is a drawing ([`Op`]: paths filled or stroked,
7//! images) — never a notion of what it's for.
8
9use yog_data::Schema;
10use yog_graphics::Op;
11
12/// A font: its metrics, its glyphs, which characters they draw and the
13/// rules that turn text into glyphs ([`crate::shape`]).
14#[derive(Schema, Debug, Clone, PartialEq, Default)]
15pub struct Font {
16    /// How many units an em is: a glyph drawn at size `s` is scaled by
17    /// `s / units_per_em`.
18    pub units_per_em: u32,
19    /// How far above the baseline its tallest glyphs reach.
20    pub ascent: i32,
21    /// How far below the baseline its glyphs reach (negative: below).
22    pub descent: i32,
23    /// Space between one line's descent and the next one's ascent.
24    #[data(default)]
25    pub line_gap: i32,
26    /// Its glyphs; a glyph is named by its index.
27    pub glyphs: Vec<Glyph>,
28    /// Which glyph draws each character.
29    pub chars: Vec<CharGlyph>,
30    /// Glyph sequences one glyph draws (`f` `i` → `fi`), the longest first.
31    #[data(default)]
32    pub ligatures: Vec<Ligature>,
33    /// Contextual forms: glyphs replaced by what comes before them.
34    #[data(default)]
35    pub machines: Vec<Machine>,
36    /// Extra space between pairs of glyphs.
37    #[data(default)]
38    pub kerning: Vec<Kern>,
39    /// How a run of glyphs joined to each other ([`Glyph::entry`]) that
40    /// grows too tall is fitted into the line.
41    pub fit: Option<Fit>,
42}
43
44/// One glyph: how far it moves the pen, and its drawing.
45#[derive(Schema, Debug, Clone, PartialEq, Default)]
46pub struct Glyph {
47    /// What it's called where the font is made (`a`, `heart`, `rune_ne`).
48    #[data(default)]
49    pub name: String,
50    /// How far the pen moves after it.
51    #[data(default)]
52    pub advance: i32,
53    /// Its drawing, in order; parts without a color take the text's.
54    #[data(default)]
55    pub ops: Vec<Op>,
56    /// Where the glyph before it, if that one has an [`Glyph::exit`], joins it
57    /// — joined glyphs are placed point to point, up and down too (cursive
58    /// scripts; runes drawn stroke by stroke).
59    pub entry: Option<[i32; 2]>,
60    /// Where the glyph after it joins it.
61    pub exit: Option<[i32; 2]>,
62}
63
64/// Character `char` (a Unicode scalar) is drawn by glyph `glyph`.
65#[derive(Schema, Debug, Clone, Copy, PartialEq)]
66pub struct CharGlyph {
67    pub char: u32,
68    pub glyph: u32,
69}
70
71/// Glyphs `glyphs`, in a row, are drawn by glyph `glyph`.
72#[derive(Schema, Debug, Clone, PartialEq)]
73pub struct Ligature {
74    pub glyphs: Vec<u32>,
75    pub glyph: u32,
76}
77
78/// A state machine over the glyphs of a run, left to right (as Apple's
79/// `morx` tables): each glyph's class and the machine's state pick a
80/// transition, which may put another glyph in its place and moves to its
81/// next state. A glyph with no transition from the state leaves it as it is
82/// and starts the machine over.
83#[derive(Schema, Debug, Clone, PartialEq, Default)]
84pub struct Machine {
85    /// Glyphs of each class; a glyph in none has no transitions.
86    pub classes: Vec<GlyphClass>,
87    pub transitions: Vec<Transition>,
88}
89
90#[derive(Schema, Debug, Clone, PartialEq)]
91pub struct GlyphClass {
92    pub class: u32,
93    pub glyphs: Vec<u32>,
94}
95
96/// From state `state` (0: the start), a glyph of class `class` moves the
97/// machine to `next`, put glyph `glyph` in its place if there is one.
98#[derive(Schema, Debug, Clone, Copy, PartialEq)]
99pub struct Transition {
100    pub state: u32,
101    pub class: u32,
102    pub next: u32,
103    pub glyph: Option<u32>,
104}
105
106/// `left` followed by `right` are `x` units further apart.
107#[derive(Schema, Debug, Clone, Copy, PartialEq)]
108pub struct Kern {
109    pub left: u32,
110    pub right: u32,
111    pub x: i32,
112}
113
114/// A run of joined glyphs taller than `height` units is scaled down to it,
115/// centred on `center` units above the baseline.
116#[derive(Schema, Debug, Clone, Copy, PartialEq)]
117pub struct Fit {
118    pub height: f32,
119    pub center: f32,
120}
121
122impl Font {
123    /// Checks the references and metrics that must be valid before a font is
124    /// shaped or rasterized.
125    pub fn validate(&self) -> Result<(), Vec<String>> {
126        let mut errors = Vec::new();
127        if self.units_per_em == 0 {
128            errors.push("units_per_em must be greater than zero".to_string());
129        }
130        if self.glyphs.is_empty() {
131            errors.push("a font must have at least one glyph".to_string());
132        }
133        self.validate_chars(&mut errors);
134        self.validate_ligatures(&mut errors);
135        for (i, machine) in self.machines.iter().enumerate() {
136            self.validate_machine(i, machine, &mut errors);
137        }
138        self.validate_kerning(&mut errors);
139        if let Some(fit) = self.fit
140            && (!fit.height.is_finite() || fit.height <= 0.0 || !fit.center.is_finite())
141        {
142            errors.push("fit.height must be positive and fit values must be finite".to_string());
143        }
144        if errors.is_empty() {
145            Ok(())
146        } else {
147            Err(errors)
148        }
149    }
150
151    /// Whether glyph `glyph` is one of the font's.
152    fn has_glyph(&self, glyph: u32) -> bool {
153        (glyph as usize) < self.glyphs.len()
154    }
155
156    fn validate_chars(&self, errors: &mut Vec<String>) {
157        for (i, m) in self.chars.iter().enumerate() {
158            if char::from_u32(m.char).is_none() {
159                errors.push(format!("chars[{i}].char is not a Unicode scalar value"));
160            }
161            if !self.has_glyph(m.glyph) {
162                errors.push(format!(
163                    "chars[{i}].glyph {} does not name a glyph",
164                    m.glyph
165                ));
166            }
167        }
168    }
169
170    fn validate_ligatures(&self, errors: &mut Vec<String>) {
171        for (i, ligature) in self.ligatures.iter().enumerate() {
172            if ligature.glyphs.len() < 2 {
173                errors.push(format!("ligatures[{i}] must contain at least two glyphs"));
174            }
175            for &glyph in ligature
176                .glyphs
177                .iter()
178                .chain(std::iter::once(&ligature.glyph))
179            {
180                if !self.has_glyph(glyph) {
181                    errors.push(format!("ligatures[{i}] references missing glyph {glyph}"));
182                }
183            }
184        }
185    }
186
187    /// Machine `i`: each class once, each glyph in one class, transitions
188    /// between known classes, once each, to known glyphs.
189    fn validate_machine(&self, i: usize, machine: &Machine, errors: &mut Vec<String>) {
190        let mut classes = std::collections::HashSet::new();
191        let mut members = std::collections::HashSet::new();
192        for class in &machine.classes {
193            if !classes.insert(class.class) {
194                errors.push(format!("machines[{i}] repeats class {}", class.class));
195            }
196            for &glyph in &class.glyphs {
197                if !self.has_glyph(glyph) {
198                    errors.push(format!(
199                        "machines[{i}] class {} references missing glyph {glyph}",
200                        class.class
201                    ));
202                }
203                if !members.insert(glyph) {
204                    errors.push(format!(
205                        "machines[{i}] assigns glyph {glyph} to more than one class"
206                    ));
207                }
208            }
209        }
210        let mut transitions = std::collections::HashSet::new();
211        for t in &machine.transitions {
212            if !classes.contains(&t.class) {
213                errors.push(format!(
214                    "machines[{i}] transition references missing class {}",
215                    t.class
216                ));
217            }
218            if !transitions.insert((t.state, t.class)) {
219                errors.push(format!(
220                    "machines[{i}] repeats transition from state {} for class {}",
221                    t.state, t.class
222                ));
223            }
224            if let Some(glyph) = t.glyph
225                && !self.has_glyph(glyph)
226            {
227                errors.push(format!(
228                    "machines[{i}] transition references missing glyph {glyph}"
229                ));
230            }
231        }
232    }
233
234    fn validate_kerning(&self, errors: &mut Vec<String>) {
235        let mut kerning = std::collections::HashSet::new();
236        for kern in &self.kerning {
237            if !self.has_glyph(kern.left) || !self.has_glyph(kern.right) {
238                errors.push(format!(
239                    "kerning pair ({}, {}) references a missing glyph",
240                    kern.left, kern.right
241                ));
242            }
243            if !kerning.insert((kern.left, kern.right)) {
244                errors.push(format!(
245                    "kerning pair ({}, {}) is repeated",
246                    kern.left, kern.right
247                ));
248            }
249        }
250    }
251
252    /// A built font from its file's bytes.
253    pub fn from_bytes(bytes: &[u8]) -> Result<Font, String> {
254        let font = yog_data::read_bytes::<Font>(bytes).map_err(|e| format!("{e:?}"))?;
255        font.validate().map_err(|errors| errors.join("; "))?;
256        Ok(font)
257    }
258
259    /// The font's file.
260    pub fn to_bytes(&self) -> Vec<u8> {
261        yog_data::binary::encode(&self.write())
262    }
263
264    /// The glyph that draws `c`.
265    pub fn glyph_of(&self, c: char) -> Option<u32> {
266        let c = c as u32;
267        self.chars.iter().find(|m| m.char == c).map(|m| m.glyph)
268    }
269}