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 !self.chars.is_sorted_by_key(|c| c.char)
140            || !self.kerning.is_sorted_by_key(|k| (k.left, k.right))
141        {
142            errors.push("chars and kerning must be sorted (Font::normalize)".to_string());
143        }
144        if let Some(fit) = self.fit
145            && (!fit.height.is_finite() || fit.height <= 0.0 || !fit.center.is_finite())
146        {
147            errors.push("fit.height must be positive and fit values must be finite".to_string());
148        }
149        if errors.is_empty() {
150            Ok(())
151        } else {
152            Err(errors)
153        }
154    }
155
156    /// Whether glyph `glyph` is one of the font's.
157    fn has_glyph(&self, glyph: u32) -> bool {
158        (glyph as usize) < self.glyphs.len()
159    }
160
161    fn validate_chars(&self, errors: &mut Vec<String>) {
162        for (i, m) in self.chars.iter().enumerate() {
163            if char::from_u32(m.char).is_none() {
164                errors.push(format!("chars[{i}].char is not a Unicode scalar value"));
165            }
166            if !self.has_glyph(m.glyph) {
167                errors.push(format!(
168                    "chars[{i}].glyph {} does not name a glyph",
169                    m.glyph
170                ));
171            }
172        }
173    }
174
175    fn validate_ligatures(&self, errors: &mut Vec<String>) {
176        for (i, ligature) in self.ligatures.iter().enumerate() {
177            if ligature.glyphs.len() < 2 {
178                errors.push(format!("ligatures[{i}] must contain at least two glyphs"));
179            }
180            for &glyph in ligature
181                .glyphs
182                .iter()
183                .chain(std::iter::once(&ligature.glyph))
184            {
185                if !self.has_glyph(glyph) {
186                    errors.push(format!("ligatures[{i}] references missing glyph {glyph}"));
187                }
188            }
189        }
190    }
191
192    /// Machine `i`: each class once, each glyph in one class, transitions
193    /// between known classes, once each, to known glyphs.
194    fn validate_machine(&self, i: usize, machine: &Machine, errors: &mut Vec<String>) {
195        let mut classes = std::collections::HashSet::new();
196        let mut members = std::collections::HashSet::new();
197        for class in &machine.classes {
198            if !classes.insert(class.class) {
199                errors.push(format!("machines[{i}] repeats class {}", class.class));
200            }
201            for &glyph in &class.glyphs {
202                if !self.has_glyph(glyph) {
203                    errors.push(format!(
204                        "machines[{i}] class {} references missing glyph {glyph}",
205                        class.class
206                    ));
207                }
208                if !members.insert(glyph) {
209                    errors.push(format!(
210                        "machines[{i}] assigns glyph {glyph} to more than one class"
211                    ));
212                }
213            }
214        }
215        let mut transitions = std::collections::HashSet::new();
216        for t in &machine.transitions {
217            if !classes.contains(&t.class) {
218                errors.push(format!(
219                    "machines[{i}] transition references missing class {}",
220                    t.class
221                ));
222            }
223            if !transitions.insert((t.state, t.class)) {
224                errors.push(format!(
225                    "machines[{i}] repeats transition from state {} for class {}",
226                    t.state, t.class
227                ));
228            }
229            if let Some(glyph) = t.glyph
230                && !self.has_glyph(glyph)
231            {
232                errors.push(format!(
233                    "machines[{i}] transition references missing glyph {glyph}"
234                ));
235            }
236        }
237    }
238
239    fn validate_kerning(&self, errors: &mut Vec<String>) {
240        let mut kerning = std::collections::HashSet::new();
241        for kern in &self.kerning {
242            if !self.has_glyph(kern.left) || !self.has_glyph(kern.right) {
243                errors.push(format!(
244                    "kerning pair ({}, {}) references a missing glyph",
245                    kern.left, kern.right
246                ));
247            }
248            if !kerning.insert((kern.left, kern.right)) {
249                errors.push(format!(
250                    "kerning pair ({}, {}) is repeated",
251                    kern.left, kern.right
252                ));
253            }
254        }
255    }
256
257    /// A built font from its file's bytes.
258    pub fn from_bytes(bytes: &[u8]) -> Result<Font, String> {
259        let mut font = yog_data::read_bytes::<Font>(bytes).map_err(|e| format!("{e:?}"))?;
260        font.normalize();
261        font.validate().map_err(|errors| errors.join("; "))?;
262        Ok(font)
263    }
264
265    /// Its tables in the order lookups search them: characters and
266    /// kerning pairs sorted, ligatures by their first glyph (the longest
267    /// first), each machine's classes' glyphs sorted. Whoever makes a font
268    /// (a loader, an importer, an editor) calls it once.
269    pub fn normalize(&mut self) {
270        self.chars.sort_unstable_by_key(|c| c.char);
271        self.kerning.sort_unstable_by_key(|k| (k.left, k.right));
272        self.ligatures
273            .sort_by_key(|l| (l.glyphs.first().copied(), std::cmp::Reverse(l.glyphs.len())));
274        for m in &mut self.machines {
275            for class in &mut m.classes {
276                class.glyphs.sort_unstable();
277            }
278            m.transitions.sort_unstable_by_key(|t| (t.state, t.class));
279        }
280    }
281
282    /// The font's file.
283    pub fn to_bytes(&self) -> Vec<u8> {
284        yog_data::binary::encode(&self.write())
285    }
286
287    /// The glyph that draws `c` (the font [`normalize`](Font::normalize)d).
288    pub fn glyph_of(&self, c: char) -> Option<u32> {
289        let c = c as u32;
290        let at = self.chars.binary_search_by_key(&c, |m| m.char).ok()?;
291        Some(self.chars[at].glyph)
292    }
293
294    /// The space kerning puts between `left` and `right`.
295    pub fn kern(&self, left: u32, right: u32) -> i32 {
296        self.kerning
297            .binary_search_by_key(&(left, right), |k| (k.left, k.right))
298            .map_or(0, |at| self.kerning[at].x)
299    }
300
301    /// The ligatures starting with glyph `first`, the longest first.
302    pub fn ligatures_from(&self, first: u32) -> &[Ligature] {
303        let from = self
304            .ligatures
305            .partition_point(|l| l.glyphs.first().is_some_and(|&g| g < first));
306        let to = self
307            .ligatures
308            .partition_point(|l| l.glyphs.first().is_some_and(|&g| g <= first));
309        &self.ligatures[from..to]
310    }
311}