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 | |
| 14 | use oxedyne_fe2o3_core::prelude::*; |
| 15 | |
| 16 | use 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)] |
| 21 | pub 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)`. |
| 49 | pub struct MathTable { |
| 50 | upem: f32, |
| 51 | consts: Constants, |
| 52 | vertical: HashMap<u16, Vec<(u16, u16)>>, |
| 53 | } |
| 54 | |
| 55 | impl 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`. |
| 110 | fn 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. |
| 140 | fn 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. |
| 167 | fn 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. |
| 196 | fn 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 | |
| 207 | fn 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 | |
| 214 | fn be_i16(b: &[u8], o: usize) -> Outcome<i16> { |
| 215 | Ok(res!(be_u16(b, o)) as i16) |
| 216 | } |
| 217 | |
| 218 | fn 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 | } |