Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/src/mathtable.rs

9.6 KiB, 21 runs

created by r1870400018:36326, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A reader for the OpenType MATH table: the layout constants and the vertical glyph variants a
2//! mathematics font carries for growing a delimiter or a radical to its content.
3//!
4//! No crate in the workspace exposes this table, so it is parsed here, from the font's own bytes. Only
5//! what the engine uses is read: a handful of `MathConstants` (the axis, the rule thicknesses, the
6//! script shifts, the radical gaps) and the vertical `MathVariants` (a base glyph mapped to a list of
7//! taller pre-drawn variants). Glyph assembly -- building an arbitrarily tall delimiter from repeating
8//! pieces -- is left for later; the discrete variants cover the sizes a set page reaches for. Values
9//! are in font design units; a caller scales them by the type size over the units-per-em.
10//!
11//! This is generic enough to belong in `fe2o3_font` once a second caller needs it; it sits here until
12//! then.
13
14use oxedyne_fe2o3_core::prelude::*;
15
16use std::collections::HashMap;
17
18/// The subset of `MathConstants` the engine sets to, each a raw design-unit value. Scaled to a size by
19/// [`MathTable::scaled`].
20#[derive(Clone, Copy, Debug, Default)]
21pub struct Constants {
22 // The two script size percentages open the table, before the MathValueRecord run: a script is set
23 // at this percent of the running size, a script of a script at the second percent.
24 pub script_percent_scale_down: i16,
25 pub script_script_percent_scale_down: i16,
26 pub axis_height: i16, // the maths axis a fraction bar and a relation centre on
27 pub fraction_rule_thickness: i16,
28 pub fraction_num_shift_up: i16, // text-style numerator baseline rise
29 pub fraction_den_shift_down: i16, // text-style denominator baseline drop
30 // Display-style fraction metrics: a display fraction sets its parts full size and further apart.
31 pub fraction_num_display_shift_up: i16,
32 pub fraction_den_display_shift_down: i16,
33 pub fraction_num_display_gap_min: i16, // least gap between numerator and bar, display style
34 pub fraction_denom_display_gap_min: i16, // least gap between bar and denominator, display style
35 pub radical_vertical_gap: i16, // clearance between the radicand and the rule above it
36 pub radical_rule_thickness: i16, // the vinculum's thickness
37 pub radical_extra_ascender: i16, // space above the vinculum
38 pub superscript_shift_up: i16,
39 pub superscript_bottom_min: i16, // least height of a superscript's foot above the baseline
40 pub superscript_baseline_drop_max: i16, // most a superscript baseline sits below the base's top
41 pub subscript_shift_down: i16,
42 pub subscript_top_max: i16, // most a subscript's top may reach above the baseline
43 pub subscript_baseline_drop_min: i16, // least a subscript baseline sits below the base's foot
44 pub sub_superscript_gap_min: i16, // least gap between a superscript foot and a subscript top
45}
46
47/// The parsed MATH table: the units-per-em its values are in, the constants, and each vertically
48/// extensible base glyph mapped to its taller variants as `(variant glyph id, height in design units)`.
49pub struct MathTable {
50 upem: f32,
51 consts: Constants,
52 vertical: HashMap<u16, Vec<(u16, u16)>>,
53}
54
55impl MathTable {
56 /// Parses the MATH table from a whole font file, or `None` when the font carries none. The font's
57 /// units-per-em is read from `head` so the values can later be scaled to a type size.
58 pub fn parse(font: &[u8]) -> Outcome<Option<Self>> {
59 let (math, head) = match (find_table(font, b"MATH"), find_table(font, b"head")) {
60 (Some(m), Some(h)) => (m, h),
61 _ => return Ok(None),
62 };
63 let upem = res!(be_u16(font, head + 18)) as f32;
64 if upem <= 0.0 {
65 return Err(err!("MATH: the font declares {} units per em.", upem; Input, Invalid));
66 }
67
68 let consts = res!(parse_constants(font, math + res!(be_u16(font, math + 4)) as usize));
69 let vertical = res!(parse_vertical_variants(font, math + res!(be_u16(font, math + 8)) as usize));
70 Ok(Some(Self { upem, consts, vertical }))
71 }
72
73 /// A design-unit length scaled to a size in points.
74 pub fn scaled(&self, du: i16, size_pt: f32) -> f32 {
75 du as f32 * size_pt / self.upem
76 }
77
78 pub fn constants(&self) -> &Constants { &self.consts }
79
80 /// The vertical variant of `base` at least `min_height_pt` tall at a type size, choosing the tightest
81 /// that fits (or the tallest available). The height is given and compared in points; the conversion
82 /// to the design units the table stores is done here.
83 pub fn variant_for(&self, base: u16, min_height_pt: f32, size_pt: f32) -> Option<u16> {
84 let min_du = min_height_pt * self.upem / size_pt;
85 self.vertical_variant(base, min_du)
86 }
87
88 /// The glyph id of the smallest vertical variant of `base` at least `min_du` design units tall, or
89 /// the tallest variant when none reaches that, or `None` when the glyph has no variants at all. The
90 /// base glyph's own record is included by the font as its first (smallest) variant.
91 pub fn vertical_variant(&self, base: u16, min_du: f32) -> Option<u16> {
92 let vars = self.vertical.get(&base)?;
93 let mut best: Option<(u16, u16)> = None; // the tallest seen, as a fallback
94 for &(gid, h) in vars {
95 if (h as f32) >= min_du {
96 return Some(gid); // the list is smallest-first, so the first that fits is the tightest
97 }
98 match best {
99 Some((_, bh)) if bh >= h => {},
100 _ => best = Some((gid, h)),
101 }
102 }
103 best.map(|(gid, _)| gid)
104 }
105}
106
107/// Reads the fixed run of `MathConstants` fields the engine uses. The table opens with two `int16` and
108/// two `uint16`, then a run of `MathValueRecord`s (an `int16` value and an offset), so field *i* of that
109/// run is the value at `8 + 4*i`.
110fn parse_constants(b: &[u8], c: usize) -> Outcome<Constants> {
111 let mvr = |i: usize| -> Outcome<i16> { be_i16(b, c + 8 + 4 * i) };
112 Ok(Constants {
113 // The two percentages precede the MathValueRecord run, at the table's very start.
114 script_percent_scale_down: res!(be_i16(b, c)),
115 script_script_percent_scale_down: res!(be_i16(b, c + 2)),
116 axis_height: res!(mvr(1)),
117 subscript_shift_down: res!(mvr(4)),
118 subscript_top_max: res!(mvr(5)),
119 subscript_baseline_drop_min: res!(mvr(6)),
120 superscript_shift_up: res!(mvr(7)),
121 superscript_bottom_min: res!(mvr(9)),
122 superscript_baseline_drop_max: res!(mvr(10)),
123 sub_superscript_gap_min: res!(mvr(11)),
124 fraction_num_shift_up: res!(mvr(28)),
125 fraction_num_display_shift_up: res!(mvr(29)),
126 fraction_den_shift_down: res!(mvr(30)),
127 fraction_den_display_shift_down: res!(mvr(31)),
128 fraction_num_display_gap_min: res!(mvr(33)),
129 fraction_rule_thickness: res!(mvr(34)),
130 fraction_denom_display_gap_min: res!(mvr(36)),
131 radical_vertical_gap: res!(mvr(45)),
132 radical_rule_thickness: res!(mvr(47)),
133 radical_extra_ascender: res!(mvr(48)),
134 })
135}
136
137/// Reads the vertical `MathVariants`: a coverage of base glyph ids, and for each a construction listing
138/// its taller variants. The construction offsets are indexed by coverage index, so the *i*th
139/// construction belongs to the *i*th glyph in coverage order.
140fn parse_vertical_variants(b: &[u8], v: usize) -> Outcome<HashMap<u16, Vec<(u16, u16)>>> {
141 let cov_off = res!(be_u16(b, v + 2)) as usize;
142 let count = res!(be_u16(b, v + 6)) as usize;
143 let coverage = res!(parse_coverage(b, v + cov_off));
144
145 let mut out: HashMap<u16, Vec<(u16, u16)>> = HashMap::with_capacity(count);
146 for i in 0..count {
147 let base = match coverage.get(i) {
148 Some(g) => *g,
149 None => break, // a construction with no covered glyph: nothing to key it by
150 };
151 // The construction offset array follows the header: minConnectorOverlap, the two coverage
152 // offsets, and BOTH counts (vertical then horizontal) -- ten bytes -- before the first offset.
153 let con = v + res!(be_u16(b, v + 10 + 2 * i)) as usize;
154 let vcount = res!(be_u16(b, con + 2)) as usize;
155 let mut vars = Vec::with_capacity(vcount);
156 for k in 0..vcount {
157 let gid = res!(be_u16(b, con + 4 + 4 * k));
158 let adv = res!(be_u16(b, con + 4 + 4 * k + 2));
159 vars.push((gid, adv));
160 }
161 out.insert(base, vars);
162 }
163 Ok(out)
164}
165
166/// Reads a coverage table (format 1 or 2) into the glyph ids in coverage-index order.
167fn parse_coverage(b: &[u8], o: usize) -> Outcome<Vec<u16>> {
168 match res!(be_u16(b, o)) {
169 1 => {
170 let n = res!(be_u16(b, o + 2)) as usize;
171 let mut out = Vec::with_capacity(n);
172 for i in 0..n {
173 out.push(res!(be_u16(b, o + 4 + 2 * i)));
174 }
175 Ok(out)
176 },
177 2 => {
178 let n = res!(be_u16(b, o + 2)) as usize;
179 let mut placed: Vec<(u16, u16)> = Vec::new(); // (coverage index, glyph id)
180 for i in 0..n {
181 let start = res!(be_u16(b, o + 4 + 6 * i));
182 let end = res!(be_u16(b, o + 4 + 6 * i + 2));
183 let first = res!(be_u16(b, o + 4 + 6 * i + 4));
184 for (j, g) in (start..=end).enumerate() {
185 placed.push((first + j as u16, g));
186 }
187 }
188 placed.sort_by_key(|(idx, _)| *idx);
189 Ok(placed.into_iter().map(|(_, g)| g).collect())
190 },
191 other => Err(err!("MATH: unknown coverage format {}.", other; Input, Invalid)),
192 }
193}
194
195/// The offset of a table in an sfnt font, by its four-byte tag.
196fn find_table(b: &[u8], tag: &[u8; 4]) -> Option<usize> {
197 let num = be_u16(b, 4).ok()?;
198 for i in 0..num as usize {
199 let rec = 12 + i * 16;
200 if b.get(rec..rec + 4) == Some(&tag[..]) {
201 return be_u32(b, rec + 8).ok().map(|o| o as usize);
202 }
203 }
204 None
205}
206
207fn be_u16(b: &[u8], o: usize) -> Outcome<u16> {
208 match b.get(o..o + 2) {
209 Some(s) => Ok(u16::from_be_bytes([s[0], s[1]])),
210 None => Err(err!("MATH: 16-bit read past the end of the table at byte {}.", o; Input, Invalid)),
211 }
212}
213
214fn be_i16(b: &[u8], o: usize) -> Outcome<i16> {
215 Ok(res!(be_u16(b, o)) as i16)
216}
217
218fn be_u32(b: &[u8], o: usize) -> Outcome<u32> {
219 match b.get(o..o + 4) {
220 Some(s) => Ok(u32::from_be_bytes([s[0], s[1], s[2], s[3]])),
221 None => Err(err!("MATH: 32-bit read past the end of the table at byte {}.", o; Input, Invalid)),
222 }
223}