Skip to main content

oxideav_ttf/tables/
math.rs

1//! `MATH` — the mathematical typesetting table (ISO/IEC 14496-22:2019
2//! §6.3.6).
3//!
4//! The MATH table carries the font-specific parameters a math-layout
5//! engine needs to position fractions, radicals, scripts, accents, large
6//! operators, and stretchy/assembled glyphs. It is **not** a layout
7//! algorithm — it is the data those algorithms consume.
8//!
9//! ## Structure (§6.3.6.2)
10//!
11//! ```text
12//!   MATH header  ──> MathConstants   (§6.3.6.2.3)  — ~57 font-wide values
13//!                ──> MathGlyphInfo   (§6.3.6.2.4)  — per-glyph data
14//!                │     ├─ MathItalicsCorrectionInfo
15//!                │     ├─ MathTopAccentAttachment
16//!                │     ├─ ExtendedShapeCoverage
17//!                │     └─ MathKernInfo (four per-corner MathKern tables)
18//!                ──> MathVariants    (§6.3.6.2.10) — stretchy variants +
19//!                      glyph assemblies for growing parens/radicals/etc.
20//! ```
21//!
22//! Many values are [`MathValueRecord`]s: a design-unit `int16` plus an
23//! optional device / VariationIndex offset (§6.3.6.2.1). The plain
24//! accessors expose the design-unit value; the `*_resolved` accessors fold
25//! in the variable-font delta at a given instance — a VariationIndex
26//! offset is evaluated against the GDEF `ItemVariationStore`, while a
27//! classic ppem-indexed Device table (a render-time concern) contributes
28//! no font-unit adjustment. Coverage tables reuse the common-layout
29//! Coverage parser.
30//!
31//! This module decodes the whole table structurally and exposes typed
32//! accessors. Each accessor borrows the parent table slice, so the
33//! parsed [`MathTable`] is a cheap set of validated offsets.
34
35use crate::parser::{read_i16, read_u16};
36use crate::tables::device::resolve_device_delta;
37use crate::tables::gdef::coverage_lookup;
38use crate::tables::mvar::ItemVariationStore;
39use crate::Error;
40
41/// The 4-byte table tag.
42pub const MATH_TABLE_TAG: [u8; 4] = *b"MATH";
43
44/// A `MathValueRecord` (§6.3.6.2.1): a design-unit value plus an optional
45/// device / VariationIndex table offset.
46///
47/// Per §6.3.6.2.1 the `deviceTableOffset` is measured **from the beginning
48/// of the parent table** that contains the record (the MathConstants
49/// table, a per-glyph value sub-table, a MathKern table, or a
50/// GlyphAssembly table — never the MATH-table root). In a variable font
51/// the referenced table is a VariationIndex table (§6.2, `deltaFormat`
52/// `0x8000`) whose `(outer, inner)` delta-set index is evaluated against
53/// the font-wide GDEF `ItemVariationStore`; in a non-variable font it is a
54/// classic ppem-indexed Device table whose pixel correction is a
55/// render-time concern and contributes no font-unit adjustment here.
56///
57/// The plain `value` field is always the unmodified design-unit value; use
58/// [`MathValueRecord::resolved_value`] to fold in the variable-font delta
59/// at a given instance.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61// internal — exposed for tests/fuzz; not part of the stable API
62#[doc(hidden)]
63pub struct MathValueRecord {
64    /// The X or Y value in font design units.
65    pub value: i16,
66    /// Offset to a device / VariationIndex table from the start of the
67    /// *parent* table, or 0 for none.
68    pub device_offset: u16,
69}
70
71impl MathValueRecord {
72    const LEN: usize = 4;
73
74    fn read(bytes: &[u8], at: usize) -> Result<Self, Error> {
75        Ok(Self {
76            value: read_i16(bytes, at)?,
77            device_offset: read_u16(bytes, at + 2)?,
78        })
79    }
80
81    /// The design-unit value adjusted for the current variation instance.
82    ///
83    /// `parent_bytes` is the slice the record's `device_offset` is relative
84    /// to (the parent sub-table base, per §6.3.6.2.1), `ivs` the GDEF
85    /// `ItemVariationStore` (pass `None` for a non-variable font), and
86    /// `coords` the normalised axis coordinates. A NULL `device_offset`, a
87    /// classic Device table, or a missing/out-of-range VariationIndex all
88    /// fold to a zero adjustment, so the return collapses to `value` for a
89    /// static instance.
90    ///
91    /// The result is `value as f32` plus the resolved font-unit delta; a
92    /// caller wanting an integer can round it.
93    pub fn resolved_value(
94        &self,
95        parent_bytes: &[u8],
96        ivs: Option<&ItemVariationStore>,
97        coords: &[f32],
98    ) -> f32 {
99        let delta = resolve_device_delta(parent_bytes, self.device_offset, ivs, coords);
100        self.value as f32 + delta
101    }
102}
103
104/// Parsed `MATH` table — a set of validated sub-table offsets into the
105/// borrowed table slice.
106#[derive(Debug, Clone)]
107// internal — exposed for tests/fuzz; not part of the stable API
108#[doc(hidden)]
109pub struct MathTable<'a> {
110    data: &'a [u8],
111    constants_off: usize,
112    glyph_info_off: usize,
113    variants_off: usize,
114}
115
116impl<'a> MathTable<'a> {
117    /// Parse the MATH header (§6.3.6.2.2) and validate its three offsets.
118    pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
119        // majorVersion(=1) minorVersion(=0) + three Offset16.
120        let major = read_u16(data, 0)?;
121        if major != 1 {
122            return Err(Error::BadStructure("MATH major version not 1"));
123        }
124        let constants_off = read_u16(data, 4)? as usize;
125        let glyph_info_off = read_u16(data, 6)? as usize;
126        let variants_off = read_u16(data, 8)? as usize;
127        // Offsets are from the start of the MATH table; a zero offset
128        // means the sub-table is absent. We validate non-zero offsets as
129        // in-bounds.
130        for &o in &[constants_off, glyph_info_off, variants_off] {
131            if o != 0 && o >= data.len() {
132                return Err(Error::BadStructure("MATH sub-table offset OOB"));
133            }
134        }
135        Ok(Self {
136            data,
137            constants_off,
138            glyph_info_off,
139            variants_off,
140        })
141    }
142
143    /// Borrow the MathConstants accessor, when the table publishes one.
144    pub fn constants(&self) -> Option<MathConstants<'a>> {
145        if self.constants_off == 0 {
146            return None;
147        }
148        Some(MathConstants {
149            data: self.data,
150            base: self.constants_off,
151        })
152    }
153
154    /// Borrow the MathGlyphInfo accessor, when present.
155    pub fn glyph_info(&self) -> Option<MathGlyphInfo<'a>> {
156        if self.glyph_info_off == 0 {
157            return None;
158        }
159        let base = self.glyph_info_off;
160        Some(MathGlyphInfo {
161            data: self.data,
162            base,
163        })
164    }
165
166    /// Borrow the MathVariants accessor, when present.
167    pub fn variants(&self) -> Option<MathVariants<'a>> {
168        if self.variants_off == 0 {
169            return None;
170        }
171        Some(MathVariants {
172            data: self.data,
173            base: self.variants_off,
174        })
175    }
176}
177
178// --- MathConstants (§6.3.6.2.3) --------------------------------------
179
180/// Field layout of the MathConstants table. The leading two `int16`
181/// fields and the next two `uint16` fields are plain values; every
182/// remaining field is a 4-byte `MathValueRecord`. We encode each field's
183/// byte offset rather than copying ~57 values eagerly.
184///
185/// Offsets (in bytes from the start of MathConstants):
186///
187/// ```text
188///   +0   int16  scriptPercentScaleDown
189///   +2   int16  scriptScriptPercentScaleDown
190///   +4   uint16 delimitedSubFormulaMinHeight
191///   +6   uint16 displayOperatorMinHeight
192///   +8   MathValueRecord  mathLeading
193///   ...  (the remaining 51 MathValueRecord fields, 4 bytes each)
194/// ```
195#[derive(Debug, Clone, Copy)]
196// internal — exposed for tests/fuzz; not part of the stable API
197#[doc(hidden)]
198pub struct MathConstants<'a> {
199    data: &'a [u8],
200    base: usize,
201}
202
203/// Indices into the MathConstants `MathValueRecord` array (the records
204/// that follow the four scalar fields, in spec order §6.3.6.2.3).
205///
206/// Record `i` lives at MathConstants byte offset `8 + i * 4`.
207pub mod constant {
208    /// Position of each MathValueRecord field within the record array,
209    /// in the spec's declaration order.
210    pub const MATH_LEADING: usize = 0;
211    pub const AXIS_HEIGHT: usize = 1;
212    pub const ACCENT_BASE_HEIGHT: usize = 2;
213    pub const FLATTENED_ACCENT_BASE_HEIGHT: usize = 3;
214    pub const SUBSCRIPT_SHIFT_DOWN: usize = 4;
215    pub const SUBSCRIPT_TOP_MAX: usize = 5;
216    pub const SUBSCRIPT_BASELINE_DROP_MIN: usize = 6;
217    pub const SUPERSCRIPT_SHIFT_UP: usize = 7;
218    pub const SUPERSCRIPT_SHIFT_UP_CRAMPED: usize = 8;
219    pub const SUPERSCRIPT_BOTTOM_MIN: usize = 9;
220    pub const SUPERSCRIPT_BASELINE_DROP_MAX: usize = 10;
221    pub const SUB_SUPERSCRIPT_GAP_MIN: usize = 11;
222    pub const SUPERSCRIPT_BOTTOM_MAX_WITH_SUBSCRIPT: usize = 12;
223    pub const SPACE_AFTER_SCRIPT: usize = 13;
224    pub const UPPER_LIMIT_GAP_MIN: usize = 14;
225    pub const UPPER_LIMIT_BASELINE_RISE_MIN: usize = 15;
226    pub const LOWER_LIMIT_GAP_MIN: usize = 16;
227    pub const LOWER_LIMIT_BASELINE_DROP_MIN: usize = 17;
228    pub const STACK_TOP_SHIFT_UP: usize = 18;
229    pub const STACK_TOP_DISPLAY_STYLE_SHIFT_UP: usize = 19;
230    pub const STACK_BOTTOM_SHIFT_DOWN: usize = 20;
231    pub const STACK_BOTTOM_DISPLAY_STYLE_SHIFT_DOWN: usize = 21;
232    pub const STACK_GAP_MIN: usize = 22;
233    pub const STACK_DISPLAY_STYLE_GAP_MIN: usize = 23;
234    pub const STRETCH_STACK_TOP_SHIFT_UP: usize = 24;
235    pub const STRETCH_STACK_BOTTOM_SHIFT_DOWN: usize = 25;
236    pub const STRETCH_STACK_GAP_ABOVE_MIN: usize = 26;
237    pub const STRETCH_STACK_GAP_BELOW_MIN: usize = 27;
238    pub const FRACTION_NUMERATOR_SHIFT_UP: usize = 28;
239    pub const FRACTION_NUMERATOR_DISPLAY_STYLE_SHIFT_UP: usize = 29;
240    pub const FRACTION_DENOMINATOR_SHIFT_DOWN: usize = 30;
241    pub const FRACTION_DENOMINATOR_DISPLAY_STYLE_SHIFT_DOWN: usize = 31;
242    pub const FRACTION_NUMERATOR_GAP_MIN: usize = 32;
243    pub const FRACTION_NUM_DISPLAY_STYLE_GAP_MIN: usize = 33;
244    pub const FRACTION_RULE_THICKNESS: usize = 34;
245    pub const FRACTION_DENOMINATOR_GAP_MIN: usize = 35;
246    pub const FRACTION_DENOM_DISPLAY_STYLE_GAP_MIN: usize = 36;
247    pub const SKEWED_FRACTION_HORIZONTAL_GAP: usize = 37;
248    pub const SKEWED_FRACTION_VERTICAL_GAP: usize = 38;
249    pub const OVERBAR_VERTICAL_GAP: usize = 39;
250    pub const OVERBAR_RULE_THICKNESS: usize = 40;
251    pub const OVERBAR_EXTRA_ASCENDER: usize = 41;
252    pub const UNDERBAR_VERTICAL_GAP: usize = 42;
253    pub const UNDERBAR_RULE_THICKNESS: usize = 43;
254    pub const UNDERBAR_EXTRA_DESCENDER: usize = 44;
255    pub const RADICAL_VERTICAL_GAP: usize = 45;
256    pub const RADICAL_DISPLAY_STYLE_VERTICAL_GAP: usize = 46;
257    pub const RADICAL_RULE_THICKNESS: usize = 47;
258    pub const RADICAL_EXTRA_ASCENDER: usize = 48;
259    pub const RADICAL_KERN_BEFORE_DEGREE: usize = 49;
260    pub const RADICAL_KERN_AFTER_DEGREE: usize = 50;
261}
262
263impl<'a> MathConstants<'a> {
264    /// `scriptPercentScaleDown` — percentage to scale level-1 scripts.
265    pub fn script_percent_scale_down(&self) -> i16 {
266        read_i16(self.data, self.base).unwrap_or(0)
267    }
268
269    /// `scriptScriptPercentScaleDown` — percentage to scale level-2
270    /// scripts.
271    pub fn script_script_percent_scale_down(&self) -> i16 {
272        read_i16(self.data, self.base + 2).unwrap_or(0)
273    }
274
275    /// `delimitedSubFormulaMinHeight`.
276    pub fn delimited_sub_formula_min_height(&self) -> u16 {
277        read_u16(self.data, self.base + 4).unwrap_or(0)
278    }
279
280    /// `displayOperatorMinHeight`.
281    pub fn display_operator_min_height(&self) -> u16 {
282        read_u16(self.data, self.base + 6).unwrap_or(0)
283    }
284
285    /// `radicalDegreeBottomRaisePercent` — the trailing `int16` field
286    /// that follows the MathValueRecord array (record 50 is the last
287    /// MathValueRecord; this `int16` sits right after it).
288    pub fn radical_degree_bottom_raise_percent(&self) -> i16 {
289        let at = self.base + 8 + 51 * MathValueRecord::LEN;
290        read_i16(self.data, at).unwrap_or(0)
291    }
292
293    /// One of the MathValueRecord constants (`constant::*`), or `None`
294    /// when the index is out of range or the record is truncated.
295    pub fn value(&self, index: usize) -> Option<MathValueRecord> {
296        if index > constant::RADICAL_KERN_AFTER_DEGREE {
297            return None;
298        }
299        let at = self.base + 8 + index * MathValueRecord::LEN;
300        MathValueRecord::read(self.data, at).ok()
301    }
302
303    /// Convenience: the design-unit value of MathValueRecord `index`, or
304    /// `0` when absent.
305    pub fn value_i16(&self, index: usize) -> i16 {
306        self.value(index).map(|r| r.value).unwrap_or(0)
307    }
308
309    /// MathValueRecord `index` resolved for the current variation instance.
310    ///
311    /// Folds in the record's device / VariationIndex correction per
312    /// §6.3.6.2.1: in a variable font the `(outer, inner)` delta-set index
313    /// is evaluated against the GDEF `ItemVariationStore` `ivs` at `coords`;
314    /// in a static font (or for an absent record) the result is the plain
315    /// design-unit value. The device offset is relative to the start of the
316    /// MathConstants table, so the parent slice is `self.data[self.base..]`.
317    pub fn value_resolved(
318        &self,
319        index: usize,
320        ivs: Option<&ItemVariationStore>,
321        coords: &[f32],
322    ) -> f32 {
323        match self.value(index) {
324            Some(r) => r.resolved_value(&self.data[self.base..], ivs, coords),
325            None => 0.0,
326        }
327    }
328}
329
330// --- MathGlyphInfo (§6.3.6.2.4) --------------------------------------
331
332/// Per-glyph math positioning data.
333#[derive(Debug, Clone, Copy)]
334// internal — exposed for tests/fuzz; not part of the stable API
335#[doc(hidden)]
336pub struct MathGlyphInfo<'a> {
337    data: &'a [u8],
338    base: usize,
339}
340
341impl<'a> MathGlyphInfo<'a> {
342    fn sub_off(&self, idx: usize) -> Option<usize> {
343        let o = read_u16(self.data, self.base + idx * 2).ok()? as usize;
344        if o == 0 {
345            None
346        } else {
347            Some(self.base + o)
348        }
349    }
350
351    /// Italics-correction value for `gid` (§6.3.6.2.5), or `None` when the
352    /// glyph isn't covered (treated as zero by layout).
353    pub fn italics_correction(&self, gid: u16) -> Option<i16> {
354        let base = self.sub_off(0)?; // mathItalicsCorrectionInfoOffset
355        let cov_off = read_u16(self.data, base).ok()? as usize;
356        if cov_off == 0 {
357            return None;
358        }
359        let idx = coverage_lookup(self.data.get(base + cov_off..)?, gid)? as usize;
360        let count = read_u16(self.data, base + 2).ok()? as usize;
361        if idx >= count {
362            return None;
363        }
364        let at = base + 4 + idx * MathValueRecord::LEN;
365        Some(MathValueRecord::read(self.data, at).ok()?.value)
366    }
367
368    /// Look up the per-glyph MathValueRecord at `value_sub`
369    /// (`mathItalicsCorrectionInfo` index 0 / `mathTopAccentAttachment`
370    /// index 1) for `gid`, returning the *record* (value + parent-relative
371    /// device offset) and the parent-table base needed to resolve that
372    /// offset per §6.3.6.2.1.
373    fn value_record_for(&self, value_sub: usize, gid: u16) -> Option<(MathValueRecord, usize)> {
374        let base = self.sub_off(value_sub)?;
375        let cov_off = read_u16(self.data, base).ok()? as usize;
376        if cov_off == 0 {
377            return None;
378        }
379        let idx = coverage_lookup(self.data.get(base + cov_off..)?, gid)? as usize;
380        let count = read_u16(self.data, base + 2).ok()? as usize;
381        if idx >= count {
382            return None;
383        }
384        let at = base + 4 + idx * MathValueRecord::LEN;
385        Some((MathValueRecord::read(self.data, at).ok()?, base))
386    }
387
388    /// Italics-correction for `gid` resolved at the current variation
389    /// instance (§6.3.6.2.5 + §6.3.6.2.1). Folds in a VariationIndex delta
390    /// against the GDEF `ItemVariationStore` `ivs` at `coords`; `None` when
391    /// uncovered (layout treats that as zero).
392    pub fn italics_correction_resolved(
393        &self,
394        gid: u16,
395        ivs: Option<&ItemVariationStore>,
396        coords: &[f32],
397    ) -> Option<f32> {
398        let (rec, base) = self.value_record_for(0, gid)?;
399        Some(rec.resolved_value(&self.data[base..], ivs, coords))
400    }
401
402    /// Top-accent horizontal attachment point for `gid` (§6.3.6.2.6), or
403    /// `None` when uncovered (use the glyph's geometric centre instead).
404    pub fn top_accent_attachment(&self, gid: u16) -> Option<i16> {
405        let base = self.sub_off(1)?; // mathTopAccentAttachmentOffset
406        let cov_off = read_u16(self.data, base).ok()? as usize;
407        if cov_off == 0 {
408            return None;
409        }
410        let idx = coverage_lookup(self.data.get(base + cov_off..)?, gid)? as usize;
411        let count = read_u16(self.data, base + 2).ok()? as usize;
412        if idx >= count {
413            return None;
414        }
415        let at = base + 4 + idx * MathValueRecord::LEN;
416        Some(MathValueRecord::read(self.data, at).ok()?.value)
417    }
418
419    /// Top-accent attachment for `gid` resolved at the current variation
420    /// instance (§6.3.6.2.6 + §6.3.6.2.1).
421    pub fn top_accent_attachment_resolved(
422        &self,
423        gid: u16,
424        ivs: Option<&ItemVariationStore>,
425        coords: &[f32],
426    ) -> Option<f32> {
427        let (rec, base) = self.value_record_for(1, gid)?;
428        Some(rec.resolved_value(&self.data[base..], ivs, coords))
429    }
430
431    /// Whether `gid` is flagged as an extended shape (§6.3.6.2.7).
432    pub fn is_extended_shape(&self, gid: u16) -> bool {
433        // extendedShapeCoverageOffset is the third Offset16.
434        match self.sub_off(2) {
435            Some(cov) => self
436                .data
437                .get(cov..)
438                .and_then(|b| coverage_lookup(b, gid))
439                .is_some(),
440            None => false,
441        }
442    }
443
444    /// Math-kern value for `gid` at one corner and a given correction
445    /// height, in design units (§6.3.6.2.8/.9). `corner` selects the
446    /// per-corner MathKern table; absent corners kern by zero.
447    pub fn math_kern(&self, gid: u16, corner: MathKernCorner, height: i16) -> Option<i16> {
448        let base = self.sub_off(3)?; // mathKernInfoOffset
449        let cov_off = read_u16(self.data, base).ok()? as usize;
450        if cov_off == 0 {
451            return None;
452        }
453        let idx = coverage_lookup(self.data.get(base + cov_off..)?, gid)? as usize;
454        let count = read_u16(self.data, base + 2).ok()? as usize;
455        if idx >= count {
456            return None;
457        }
458        // MathKernInfoRecord: four Offset16 per covered glyph.
459        let rec_at = base + 4 + idx * 8;
460        let kern_off = read_u16(self.data, rec_at + corner as usize * 2).ok()? as usize;
461        if kern_off == 0 {
462            return None;
463        }
464        math_kern_value(self.data, base + kern_off, height).map(|r| r.value)
465    }
466
467    /// Math-kern value for `gid` at one corner and correction `height`,
468    /// resolved at the current variation instance (§6.3.6.2.8/.9 +
469    /// §6.3.6.2.1). The selected kern value's device offset is parent-
470    /// relative to the MathKern table, so a VariationIndex delta is
471    /// evaluated against `ivs` at `coords`.
472    pub fn math_kern_resolved(
473        &self,
474        gid: u16,
475        corner: MathKernCorner,
476        height: i16,
477        ivs: Option<&ItemVariationStore>,
478        coords: &[f32],
479    ) -> Option<f32> {
480        let base = self.sub_off(3)?; // mathKernInfoOffset
481        let cov_off = read_u16(self.data, base).ok()? as usize;
482        if cov_off == 0 {
483            return None;
484        }
485        let idx = coverage_lookup(self.data.get(base + cov_off..)?, gid)? as usize;
486        let count = read_u16(self.data, base + 2).ok()? as usize;
487        if idx >= count {
488            return None;
489        }
490        let rec_at = base + 4 + idx * 8;
491        let kern_off = read_u16(self.data, rec_at + corner as usize * 2).ok()? as usize;
492        if kern_off == 0 {
493            return None;
494        }
495        let kern_base = base + kern_off;
496        let rec = math_kern_value(self.data, kern_base, height)?;
497        Some(rec.resolved_value(&self.data[kern_base..], ivs, coords))
498    }
499}
500
501/// The four corners a `MathKern` table can apply to (§6.3.6.2.8).
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub enum MathKernCorner {
504    TopRight = 0,
505    TopLeft = 1,
506    BottomRight = 2,
507    BottomLeft = 3,
508}
509
510/// Look up a MathKern value (§6.3.6.2.9) at `height` from a MathKern
511/// table located at `base`. `heightCount` correction heights partition
512/// the vertical extent; `heightCount + 1` kern values cover the ranges.
513fn math_kern_value(data: &[u8], base: usize, height: i16) -> Option<MathValueRecord> {
514    let n = read_u16(data, base).ok()? as usize;
515    // correctionHeight[n] then kernValue[n+1], each a MathValueRecord.
516    let heights_at = base + 2;
517    let kerns_at = heights_at + n * MathValueRecord::LEN;
518    // Find the first correction height strictly greater than `height`;
519    // the index of that boundary selects the kern range.
520    let mut sel = n; // default: past the last boundary -> last kern.
521    for i in 0..n {
522        let h = MathValueRecord::read(data, heights_at + i * MathValueRecord::LEN)
523            .ok()?
524            .value;
525        if height < h {
526            sel = i;
527            break;
528        }
529    }
530    let at = kerns_at + sel * MathValueRecord::LEN;
531    MathValueRecord::read(data, at).ok()
532}
533
534// --- MathVariants (§6.3.6.2.10) --------------------------------------
535
536/// Growth direction for stretchy / assembled glyph variants.
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538pub enum GrowDirection {
539    Vertical,
540    Horizontal,
541}
542
543/// One ready-made stretchy variant (§6.3.6.2.11 MathGlyphVariantRecord).
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545// internal — exposed for tests/fuzz; not part of the stable API
546#[doc(hidden)]
547pub struct GlyphVariant {
548    /// Glyph ID of the variant.
549    pub glyph: u16,
550    /// Advance (width or height) of the variant in the growth direction.
551    pub advance: u16,
552}
553
554/// One part of an assembled stretchy glyph (§6.3.6.2.12 GlyphPartRecord).
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556// internal — exposed for tests/fuzz; not part of the stable API
557#[doc(hidden)]
558pub struct GlyphPart {
559    pub glyph: u16,
560    pub start_connector_length: u16,
561    pub end_connector_length: u16,
562    pub full_advance: u16,
563    /// Part qualifiers; bit 0 (`0x0001`) marks an extender part.
564    pub part_flags: u16,
565}
566
567impl GlyphPart {
568    /// Whether this part is an extender (repeatable/skippable, §6.3.6.2.12).
569    pub fn is_extender(&self) -> bool {
570        self.part_flags & 0x0001 != 0
571    }
572}
573
574/// Stretchy / assembled glyph variants.
575#[derive(Debug, Clone, Copy)]
576// internal — exposed for tests/fuzz; not part of the stable API
577#[doc(hidden)]
578pub struct MathVariants<'a> {
579    data: &'a [u8],
580    base: usize,
581}
582
583impl<'a> MathVariants<'a> {
584    /// Minimum overlap of connecting glyph parts during assembly.
585    pub fn min_connector_overlap(&self) -> u16 {
586        read_u16(self.data, self.base).unwrap_or(0)
587    }
588
589    fn coverage_off(&self, dir: GrowDirection) -> Option<usize> {
590        // vertGlyphCoverageOffset @ +2, horizGlyphCoverageOffset @ +4.
591        let field = match dir {
592            GrowDirection::Vertical => 2,
593            GrowDirection::Horizontal => 4,
594        };
595        let o = read_u16(self.data, self.base + field).ok()? as usize;
596        if o == 0 {
597            None
598        } else {
599            Some(self.base + o)
600        }
601    }
602
603    fn glyph_count(&self, dir: GrowDirection) -> u16 {
604        // vertGlyphCount @ +6, horizGlyphCount @ +8.
605        let field = match dir {
606            GrowDirection::Vertical => 6,
607            GrowDirection::Horizontal => 8,
608        };
609        read_u16(self.data, self.base + field).unwrap_or(0)
610    }
611
612    /// Offset (from the MathVariants table) to the MathGlyphConstruction
613    /// table for `gid` growing in `dir`, or `None` when `gid` has no
614    /// construction in that direction.
615    fn construction_off(&self, gid: u16, dir: GrowDirection) -> Option<usize> {
616        let cov = self.coverage_off(dir)?;
617        let idx = coverage_lookup(self.data.get(cov..)?, gid)? as usize;
618        let count = self.glyph_count(dir) as usize;
619        if idx >= count {
620            return None;
621        }
622        // Construction offset arrays: vert @ +10, horiz @ +10 + vertCount*2.
623        let vert_count = self.glyph_count(GrowDirection::Vertical) as usize;
624        let array_base = match dir {
625            GrowDirection::Vertical => self.base + 10,
626            GrowDirection::Horizontal => self.base + 10 + vert_count * 2,
627        };
628        let o = read_u16(self.data, array_base + idx * 2).ok()? as usize;
629        if o == 0 {
630            None
631        } else {
632            Some(self.base + o)
633        }
634    }
635
636    /// Ready-made stretchy variants for `gid` growing in `dir`, ordered
637    /// by increasing size (§6.3.6.2.11).
638    pub fn variants(&self, gid: u16, dir: GrowDirection) -> Vec<GlyphVariant> {
639        let mut out = Vec::new();
640        let Some(ctor) = self.construction_off(gid, dir) else {
641            return out;
642        };
643        // MathGlyphConstruction: Offset16 glyphAssemblyOffset, uint16
644        // variantCount, then variantCount MathGlyphVariantRecords.
645        let Ok(count) = read_u16(self.data, ctor + 2) else {
646            return out;
647        };
648        for i in 0..count as usize {
649            let at = ctor + 4 + i * 4;
650            let (Ok(glyph), Ok(advance)) = (read_u16(self.data, at), read_u16(self.data, at + 2))
651            else {
652                break;
653            };
654            out.push(GlyphVariant { glyph, advance });
655        }
656        out
657    }
658
659    /// The glyph-assembly parts for `gid` growing in `dir`, when the font
660    /// supplies a general assembly mechanism (§6.3.6.2.12). Returns the
661    /// `(italics_correction, parts)` pair, or `None` when no assembly is
662    /// defined.
663    pub fn assembly(&self, gid: u16, dir: GrowDirection) -> Option<(i16, Vec<GlyphPart>)> {
664        let ctor = self.construction_off(gid, dir)?;
665        let asm_off = read_u16(self.data, ctor).ok()? as usize;
666        if asm_off == 0 {
667            return None;
668        }
669        let asm = ctor + asm_off;
670        // GlyphAssembly: MathValueRecord italicsCorrection, uint16
671        // partCount, GlyphPartRecord[partCount].
672        let italics = MathValueRecord::read(self.data, asm).ok()?.value;
673        let part_count = read_u16(self.data, asm + MathValueRecord::LEN).ok()? as usize;
674        let parts_at = asm + MathValueRecord::LEN + 2;
675        let mut parts = Vec::with_capacity(part_count);
676        for i in 0..part_count {
677            let at = parts_at + i * 10; // GlyphPartRecord is 5 * u16.
678            parts.push(GlyphPart {
679                glyph: read_u16(self.data, at).ok()?,
680                start_connector_length: read_u16(self.data, at + 2).ok()?,
681                end_connector_length: read_u16(self.data, at + 4).ok()?,
682                full_advance: read_u16(self.data, at + 6).ok()?,
683                part_flags: read_u16(self.data, at + 8).ok()?,
684            });
685        }
686        Some((italics, parts))
687    }
688
689    /// The glyph-assembly italics correction for `gid` growing in `dir`,
690    /// resolved at the current variation instance (§6.3.6.2.12 +
691    /// §6.3.6.2.1). The italicsCorrection record's device offset is
692    /// relative to the GlyphAssembly table, so a VariationIndex delta is
693    /// evaluated against `ivs` at `coords`. `None` when no assembly is
694    /// defined for `gid` in `dir`.
695    pub fn assembly_italics_correction_resolved(
696        &self,
697        gid: u16,
698        dir: GrowDirection,
699        ivs: Option<&ItemVariationStore>,
700        coords: &[f32],
701    ) -> Option<f32> {
702        let ctor = self.construction_off(gid, dir)?;
703        let asm_off = read_u16(self.data, ctor).ok()? as usize;
704        if asm_off == 0 {
705            return None;
706        }
707        let asm = ctor + asm_off;
708        let rec = MathValueRecord::read(self.data, asm).ok()?;
709        Some(rec.resolved_value(&self.data[asm..], ivs, coords))
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716
717    /// Build a MATH table with a MathConstants table only, exercising the
718    /// scalar fields + a couple of MathValueRecords + the trailing int16.
719    fn build_math_constants_only() -> Vec<u8> {
720        let mut data = Vec::new();
721        // Header: major=1 minor=0, constantsOff, glyphInfoOff=0, variantsOff=0
722        data.extend_from_slice(&1u16.to_be_bytes());
723        data.extend_from_slice(&0u16.to_be_bytes());
724        let const_off_pos = data.len();
725        data.extend_from_slice(&0u16.to_be_bytes()); // patched
726        data.extend_from_slice(&0u16.to_be_bytes()); // glyphInfo
727        data.extend_from_slice(&0u16.to_be_bytes()); // variants
728
729        let const_off = data.len();
730        // scalar fields
731        data.extend_from_slice(&80i16.to_be_bytes()); // scriptPercentScaleDown
732        data.extend_from_slice(&60i16.to_be_bytes()); // scriptScriptPercentScaleDown
733        data.extend_from_slice(&300u16.to_be_bytes()); // delimitedSubFormulaMinHeight
734        data.extend_from_slice(&1500u16.to_be_bytes()); // displayOperatorMinHeight
735                                                        // 51 MathValueRecords; set axisHeight (index 1) = 250, rest 0.
736        for i in 0..51 {
737            let v: i16 = if i == constant::AXIS_HEIGHT as i32 as usize {
738                250
739            } else if i == constant::FRACTION_RULE_THICKNESS {
740                40
741            } else {
742                0
743            };
744            data.extend_from_slice(&v.to_be_bytes());
745            data.extend_from_slice(&0u16.to_be_bytes()); // device offset
746        }
747        // trailing int16 radicalDegreeBottomRaisePercent
748        data.extend_from_slice(&60i16.to_be_bytes());
749
750        // patch the constants offset
751        let off = const_off as u16;
752        data[const_off_pos..const_off_pos + 2].copy_from_slice(&off.to_be_bytes());
753        data
754    }
755
756    #[test]
757    fn math_constants_scalars_and_records() {
758        let data = build_math_constants_only();
759        let m = MathTable::parse(&data).expect("parse");
760        let c = m.constants().expect("constants");
761        assert_eq!(c.script_percent_scale_down(), 80);
762        assert_eq!(c.script_script_percent_scale_down(), 60);
763        assert_eq!(c.delimited_sub_formula_min_height(), 300);
764        assert_eq!(c.display_operator_min_height(), 1500);
765        assert_eq!(c.value_i16(constant::AXIS_HEIGHT), 250);
766        assert_eq!(c.value_i16(constant::FRACTION_RULE_THICKNESS), 40);
767        assert_eq!(c.value_i16(constant::MATH_LEADING), 0);
768        assert_eq!(c.radical_degree_bottom_raise_percent(), 60);
769        // Out-of-range record index.
770        assert!(c.value(100).is_none());
771        assert!(m.glyph_info().is_none());
772        assert!(m.variants().is_none());
773    }
774
775    #[test]
776    fn rejects_wrong_version() {
777        let mut data = vec![0u8; 10];
778        data[1] = 2; // major = 2
779        assert!(MathTable::parse(&data).is_err());
780    }
781
782    /// A single-axis, single-region ItemVariationStore that contributes
783    /// `delta` font units at the +1 end of the axis. Mirrors the GDEF /
784    /// GPOS test stores so MATH VariationIndex resolution exercises the
785    /// same shared decoder.
786    fn build_single_region_ivs(delta: i16) -> Vec<u8> {
787        let mut b = vec![0u8; 32];
788        b[0..2].copy_from_slice(&1u16.to_be_bytes()); // format 1
789        b[2..6].copy_from_slice(&12u32.to_be_bytes()); // regionListOffset
790        b[6..8].copy_from_slice(&1u16.to_be_bytes()); // itemVariationDataCount
791        b[8..12].copy_from_slice(&22u32.to_be_bytes()); // IVD[0] offset
792        b[12..14].copy_from_slice(&1u16.to_be_bytes()); // axisCount
793        b[14..16].copy_from_slice(&1u16.to_be_bytes()); // regionCount
794        b[16..18].copy_from_slice(&0i16.to_be_bytes()); // startCoord
795        b[18..20].copy_from_slice(&16384i16.to_be_bytes()); // peakCoord = 1.0
796        b[20..22].copy_from_slice(&16384i16.to_be_bytes()); // endCoord = 1.0
797        b[22..24].copy_from_slice(&1u16.to_be_bytes()); // itemCount
798        b[24..26].copy_from_slice(&1u16.to_be_bytes()); // shortDeltaCount
799        b[26..28].copy_from_slice(&1u16.to_be_bytes()); // regionIndexCount
800        b[28..30].copy_from_slice(&0u16.to_be_bytes()); // regionIndexes[0]
801        b[30..32].copy_from_slice(&delta.to_be_bytes()); // deltaSets[0]
802        b
803    }
804
805    /// Build a MATH table whose MathConstants `axisHeight` record carries a
806    /// VariationIndex device offset (outer 0, inner 0) pointing at a
807    /// VariationIndex sub-table appended after the constants table. The
808    /// device offset is measured from the start of the MathConstants table
809    /// per §6.3.6.2.1.
810    fn build_math_constants_with_var_axis_height(base: i16) -> Vec<u8> {
811        let mut data = Vec::new();
812        data.extend_from_slice(&1u16.to_be_bytes()); // major
813        data.extend_from_slice(&0u16.to_be_bytes()); // minor
814        let const_off_pos = data.len();
815        data.extend_from_slice(&0u16.to_be_bytes()); // constants (patched)
816        data.extend_from_slice(&0u16.to_be_bytes()); // glyphInfo
817        data.extend_from_slice(&0u16.to_be_bytes()); // variants
818
819        let const_off = data.len();
820        data.extend_from_slice(&80i16.to_be_bytes()); // scriptPercentScaleDown
821        data.extend_from_slice(&60i16.to_be_bytes()); // scriptScriptPercentScaleDown
822        data.extend_from_slice(&300u16.to_be_bytes()); // delimitedSubFormulaMinHeight
823        data.extend_from_slice(&1500u16.to_be_bytes()); // displayOperatorMinHeight
824        let records_at = data.len();
825        for i in 0..51usize {
826            if i == constant::AXIS_HEIGHT {
827                data.extend_from_slice(&base.to_be_bytes()); // value
828                data.extend_from_slice(&0u16.to_be_bytes()); // device offset (patched)
829            } else {
830                data.extend_from_slice(&0i16.to_be_bytes());
831                data.extend_from_slice(&0u16.to_be_bytes());
832            }
833        }
834        data.extend_from_slice(&60i16.to_be_bytes()); // radicalDegreeBottomRaisePercent
835
836        // VariationIndex sub-table (outer 0, inner 0, fmt 0x8000), appended
837        // right after the constants table; its offset is relative to the
838        // MathConstants table start.
839        let var_idx_at = data.len();
840        data.extend_from_slice(&0u16.to_be_bytes()); // outer
841        data.extend_from_slice(&0u16.to_be_bytes()); // inner
842        data.extend_from_slice(&0x8000u16.to_be_bytes()); // deltaFormat
843
844        // Patch the axisHeight record's device offset (parent-relative).
845        let dev_off_pos = records_at + constant::AXIS_HEIGHT * MathValueRecord::LEN + 2;
846        let dev_off = (var_idx_at - const_off) as u16;
847        data[dev_off_pos..dev_off_pos + 2].copy_from_slice(&dev_off.to_be_bytes());
848        // Patch the constants offset.
849        data[const_off_pos..const_off_pos + 2].copy_from_slice(&(const_off as u16).to_be_bytes());
850        data
851    }
852
853    #[test]
854    fn math_constants_value_resolves_variation_index_delta() {
855        let data = build_math_constants_with_var_axis_height(250);
856        let m = MathTable::parse(&data).expect("parse");
857        let c = m.constants().expect("constants");
858        let ivs_bytes = build_single_region_ivs(-40);
859        let ivs = ItemVariationStore::parse(&ivs_bytes).expect("ivs");
860
861        // Plain value ignores the device offset entirely.
862        assert_eq!(c.value_i16(constant::AXIS_HEIGHT), 250);
863
864        // No IVS → static value (device contributes nothing).
865        assert_eq!(c.value_resolved(constant::AXIS_HEIGHT, None, &[0.0]), 250.0);
866        // Default instance (coord 0): region scalar 0 → no delta.
867        assert_eq!(
868            c.value_resolved(constant::AXIS_HEIGHT, Some(&ivs), &[0.0]),
869            250.0
870        );
871        // Max instance (coord +1): 250 + (-40) = 210.
872        assert_eq!(
873            c.value_resolved(constant::AXIS_HEIGHT, Some(&ivs), &[1.0]),
874            210.0
875        );
876        // Half: 250 + (-20) = 230.
877        assert_eq!(
878            c.value_resolved(constant::AXIS_HEIGHT, Some(&ivs), &[0.5]),
879            230.0
880        );
881
882        // A record with no device offset folds to its plain value.
883        assert_eq!(
884            c.value_resolved(constant::MATH_LEADING, Some(&ivs), &[1.0]),
885            0.0
886        );
887    }
888
889    /// Build a MATH table with a MathVariants table carrying one vertical
890    /// stretchy glyph (gid 5) with two variants and a 3-part assembly.
891    fn build_math_variants() -> Vec<u8> {
892        let mut data = Vec::new();
893        data.extend_from_slice(&1u16.to_be_bytes()); // major
894        data.extend_from_slice(&0u16.to_be_bytes()); // minor
895        data.extend_from_slice(&0u16.to_be_bytes()); // constants off = none
896        data.extend_from_slice(&0u16.to_be_bytes()); // glyphInfo = none
897        let var_off_pos = data.len();
898        data.extend_from_slice(&0u16.to_be_bytes()); // variants (patched)
899
900        let var_base = data.len();
901        // MathVariants header.
902        data.extend_from_slice(&20u16.to_be_bytes()); // minConnectorOverlap
903        let vcov_pos = data.len();
904        data.extend_from_slice(&0u16.to_be_bytes()); // vertGlyphCoverageOffset (patched)
905        data.extend_from_slice(&0u16.to_be_bytes()); // horizGlyphCoverageOffset = none
906        data.extend_from_slice(&1u16.to_be_bytes()); // vertGlyphCount
907        data.extend_from_slice(&0u16.to_be_bytes()); // horizGlyphCount
908        let vctor_pos = data.len();
909        data.extend_from_slice(&0u16.to_be_bytes()); // vertGlyphConstructionOffsets[0] (patched)
910
911        // Coverage (format 1, one glyph = 5).
912        let cov_at = data.len();
913        data.extend_from_slice(&1u16.to_be_bytes()); // format
914        data.extend_from_slice(&1u16.to_be_bytes()); // glyphCount
915        data.extend_from_slice(&5u16.to_be_bytes()); // glyph 5
916        data[vcov_pos..vcov_pos + 2].copy_from_slice(&((cov_at - var_base) as u16).to_be_bytes());
917
918        // MathGlyphConstruction for gid 5.
919        let ctor_at = data.len();
920        let asm_off_pos = data.len();
921        data.extend_from_slice(&0u16.to_be_bytes()); // glyphAssemblyOffset (patched)
922        data.extend_from_slice(&2u16.to_be_bytes()); // variantCount
923                                                     // variant records: (glyph, advance)
924        data.extend_from_slice(&10u16.to_be_bytes());
925        data.extend_from_slice(&1000u16.to_be_bytes());
926        data.extend_from_slice(&11u16.to_be_bytes());
927        data.extend_from_slice(&2000u16.to_be_bytes());
928        data[vctor_pos..vctor_pos + 2]
929            .copy_from_slice(&((ctor_at - var_base) as u16).to_be_bytes());
930
931        // GlyphAssembly: italics=0, partCount=2, two parts (one extender).
932        let asm_at = data.len();
933        data.extend_from_slice(&0i16.to_be_bytes()); // italics value
934        data.extend_from_slice(&0u16.to_be_bytes()); // italics device
935        data.extend_from_slice(&2u16.to_be_bytes()); // partCount
936                                                     // part 0: top, not extender
937        data.extend_from_slice(&20u16.to_be_bytes()); // glyph
938        data.extend_from_slice(&0u16.to_be_bytes()); // startConn
939        data.extend_from_slice(&50u16.to_be_bytes()); // endConn
940        data.extend_from_slice(&300u16.to_be_bytes()); // fullAdvance
941        data.extend_from_slice(&0u16.to_be_bytes()); // flags
942                                                     // part 1: extender
943        data.extend_from_slice(&21u16.to_be_bytes());
944        data.extend_from_slice(&50u16.to_be_bytes());
945        data.extend_from_slice(&50u16.to_be_bytes());
946        data.extend_from_slice(&200u16.to_be_bytes());
947        data.extend_from_slice(&1u16.to_be_bytes()); // extender flag
948        data[asm_off_pos..asm_off_pos + 2]
949            .copy_from_slice(&((asm_at - ctor_at) as u16).to_be_bytes());
950
951        data[var_off_pos..var_off_pos + 2].copy_from_slice(&(var_base as u16).to_be_bytes());
952        data
953    }
954
955    #[test]
956    fn math_variants_and_assembly() {
957        let data = build_math_variants();
958        let m = MathTable::parse(&data).expect("parse");
959        let v = m.variants().expect("variants");
960        assert_eq!(v.min_connector_overlap(), 20);
961
962        let vars = v.variants(5, GrowDirection::Vertical);
963        assert_eq!(vars.len(), 2);
964        assert_eq!(
965            vars[0],
966            GlyphVariant {
967                glyph: 10,
968                advance: 1000
969            }
970        );
971        assert_eq!(
972            vars[1],
973            GlyphVariant {
974                glyph: 11,
975                advance: 2000
976            }
977        );
978        // Uncovered glyph -> no variants.
979        assert!(v.variants(99, GrowDirection::Vertical).is_empty());
980        // No horizontal coverage.
981        assert!(v.variants(5, GrowDirection::Horizontal).is_empty());
982
983        let (italics, parts) = v.assembly(5, GrowDirection::Vertical).expect("assembly");
984        assert_eq!(italics, 0);
985        assert_eq!(parts.len(), 2);
986        assert_eq!(parts[0].glyph, 20);
987        assert!(!parts[0].is_extender());
988        assert_eq!(parts[1].glyph, 21);
989        assert!(parts[1].is_extender());
990        assert_eq!(parts[1].full_advance, 200);
991    }
992
993    /// Build a MATH table with a MathGlyphInfo carrying, for gid 7:
994    ///   * an italicsCorrectionInfo entry (value `ic`, VariationIndex dev),
995    ///   * a topAccentAttachment entry (value `tac`, no device),
996    ///   * a MathKernInfo with a single TopRight kern (one height boundary,
997    ///     two kern values; the upper kern carries a VariationIndex dev).
998    ///
999    /// All device offsets are parent-relative per §6.3.6.2.1.
1000    fn build_math_glyph_info(ic: i16, tac: i16, kern_hi: i16) -> Vec<u8> {
1001        // We build the MathGlyphInfo body self-contained, then splice it
1002        // into a MATH header at glyphInfoOffset.
1003        // ---- italicsCorrectionInfo (gi-relative offsets) ----
1004        // header: coverageOffset(=8), italicsCorrectionCount(=1),
1005        //         MathValueRecord[1] = { ic, devOff }
1006        // then Coverage at +8, then a VariationIndex at +(after coverage).
1007        let mut gi = Vec::new();
1008        // We assemble four sub-tables back to back, recording their
1009        // gi-relative starts so the four leading Offset16 fields can point
1010        // at them. Layout: [4 Offset16 header][ic][tac][esc=0][kern].
1011        let header_len = 8usize; // four Offset16
1012
1013        // -- italicsCorrectionInfo sub-table --
1014        let mut ic_sub = Vec::new();
1015        ic_sub.extend_from_slice(&8u16.to_be_bytes()); // coverageOffset
1016        ic_sub.extend_from_slice(&1u16.to_be_bytes()); // count
1017        ic_sub.extend_from_slice(&ic.to_be_bytes()); // value
1018        let ic_dev_pos = ic_sub.len();
1019        ic_sub.extend_from_slice(&0u16.to_be_bytes()); // device (patched)
1020                                                       // Coverage @ +8: format 1, [7]
1021        ic_sub.extend_from_slice(&1u16.to_be_bytes());
1022        ic_sub.extend_from_slice(&1u16.to_be_bytes());
1023        ic_sub.extend_from_slice(&7u16.to_be_bytes());
1024        // VariationIndex (outer 0, inner 0) right after coverage.
1025        let ic_var_at = ic_sub.len();
1026        ic_sub.extend_from_slice(&0u16.to_be_bytes());
1027        ic_sub.extend_from_slice(&0u16.to_be_bytes());
1028        ic_sub.extend_from_slice(&0x8000u16.to_be_bytes());
1029        ic_sub[ic_dev_pos..ic_dev_pos + 2].copy_from_slice(&(ic_var_at as u16).to_be_bytes());
1030
1031        // -- topAccentAttachment sub-table (no device) --
1032        let mut tac_sub = Vec::new();
1033        tac_sub.extend_from_slice(&8u16.to_be_bytes()); // coverageOffset
1034        tac_sub.extend_from_slice(&1u16.to_be_bytes()); // count
1035        tac_sub.extend_from_slice(&tac.to_be_bytes());
1036        tac_sub.extend_from_slice(&0u16.to_be_bytes()); // no device
1037        tac_sub.extend_from_slice(&1u16.to_be_bytes()); // cov fmt
1038        tac_sub.extend_from_slice(&1u16.to_be_bytes());
1039        tac_sub.extend_from_slice(&7u16.to_be_bytes());
1040
1041        // -- MathKernInfo sub-table --
1042        // header: coverageOffset, mathKernCount(=1),
1043        //         MathKernInfoRecord[1] = four Offset16 (TR,TL,BR,BL).
1044        // Only TopRight is non-zero → points at a MathKern table.
1045        let mut kern_sub = Vec::new();
1046        let kcov_pos = kern_sub.len();
1047        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // coverageOffset (patched)
1048        kern_sub.extend_from_slice(&1u16.to_be_bytes()); // mathKernCount
1049        let krec_pos = kern_sub.len();
1050        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // TR (patched)
1051        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // TL
1052        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // BR
1053        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // BL
1054                                                         // Coverage [7].
1055        let kcov_at = kern_sub.len();
1056        kern_sub.extend_from_slice(&1u16.to_be_bytes());
1057        kern_sub.extend_from_slice(&1u16.to_be_bytes());
1058        kern_sub.extend_from_slice(&7u16.to_be_bytes());
1059        kern_sub[kcov_pos..kcov_pos + 2].copy_from_slice(&(kcov_at as u16).to_be_bytes());
1060        // MathKern table: heightCount=1, correctionHeight[0]=100 (no dev),
1061        //   kernValue[0]=10 (no dev), kernValue[1]=kern_hi (+ VariationIndex).
1062        let mkern_at = kern_sub.len();
1063        kern_sub.extend_from_slice(&1u16.to_be_bytes()); // heightCount
1064        kern_sub.extend_from_slice(&100i16.to_be_bytes()); // height[0] value
1065        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // height[0] dev
1066        kern_sub.extend_from_slice(&10i16.to_be_bytes()); // kern[0] value
1067        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // kern[0] dev
1068        kern_sub.extend_from_slice(&kern_hi.to_be_bytes()); // kern[1] value
1069        let khi_dev_pos = kern_sub.len();
1070        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // kern[1] dev (patched)
1071        let kvar_at = kern_sub.len();
1072        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // outer
1073        kern_sub.extend_from_slice(&0u16.to_be_bytes()); // inner
1074        kern_sub.extend_from_slice(&0x8000u16.to_be_bytes()); // fmt
1075                                                              // kern[1] device offset is relative to the MathKern table start.
1076        kern_sub[khi_dev_pos..khi_dev_pos + 2]
1077            .copy_from_slice(&((kvar_at - mkern_at) as u16).to_be_bytes());
1078        kern_sub[krec_pos..krec_pos + 2].copy_from_slice(&(mkern_at as u16).to_be_bytes());
1079
1080        // Assemble the MathGlyphInfo: header (four Offset16) + sub-tables.
1081        let ic_at = header_len;
1082        let tac_at = ic_at + ic_sub.len();
1083        let kern_at = tac_at + tac_sub.len();
1084        gi.extend_from_slice(&(ic_at as u16).to_be_bytes()); // italicsCorrectionInfoOffset
1085        gi.extend_from_slice(&(tac_at as u16).to_be_bytes()); // topAccentAttachmentOffset
1086        gi.extend_from_slice(&0u16.to_be_bytes()); // extendedShapeCoverageOffset = none
1087        gi.extend_from_slice(&(kern_at as u16).to_be_bytes()); // mathKernInfoOffset
1088        gi.extend_from_slice(&ic_sub);
1089        gi.extend_from_slice(&tac_sub);
1090        gi.extend_from_slice(&kern_sub);
1091
1092        // MATH header: glyphInfo only.
1093        let mut data = Vec::new();
1094        data.extend_from_slice(&1u16.to_be_bytes()); // major
1095        data.extend_from_slice(&0u16.to_be_bytes()); // minor
1096        data.extend_from_slice(&0u16.to_be_bytes()); // constants = none
1097        let gi_off_pos = data.len();
1098        data.extend_from_slice(&0u16.to_be_bytes()); // glyphInfo (patched)
1099        data.extend_from_slice(&0u16.to_be_bytes()); // variants = none
1100        let gi_off = data.len();
1101        data.extend_from_slice(&gi);
1102        data[gi_off_pos..gi_off_pos + 2].copy_from_slice(&(gi_off as u16).to_be_bytes());
1103        data
1104    }
1105
1106    #[test]
1107    fn glyph_info_values_resolve_variation_deltas() {
1108        let data = build_math_glyph_info(120, 300, 25);
1109        let m = MathTable::parse(&data).expect("parse");
1110        let gi = m.glyph_info().expect("glyph info");
1111        let ivs_bytes = build_single_region_ivs(-15);
1112        let ivs = ItemVariationStore::parse(&ivs_bytes).expect("ivs");
1113
1114        // Plain accessors ignore device offsets.
1115        assert_eq!(gi.italics_correction(7), Some(120));
1116        assert_eq!(gi.top_accent_attachment(7), Some(300));
1117        // Below the single height boundary (100) → first kern value (10).
1118        assert_eq!(gi.math_kern(7, MathKernCorner::TopRight, 50), Some(10));
1119        // At/above the boundary → second kern value (25).
1120        assert_eq!(gi.math_kern(7, MathKernCorner::TopRight, 150), Some(25));
1121
1122        // Resolved italics correction tracks the instance.
1123        assert_eq!(
1124            gi.italics_correction_resolved(7, Some(&ivs), &[0.0]),
1125            Some(120.0)
1126        );
1127        assert_eq!(
1128            gi.italics_correction_resolved(7, Some(&ivs), &[1.0]),
1129            Some(105.0)
1130        ); // 120 + (-15)
1131           // Top-accent has no device → unchanged.
1132        assert_eq!(
1133            gi.top_accent_attachment_resolved(7, Some(&ivs), &[1.0]),
1134            Some(300.0)
1135        );
1136        // The lower kern range has no device → static.
1137        assert_eq!(
1138            gi.math_kern_resolved(7, MathKernCorner::TopRight, 50, Some(&ivs), &[1.0]),
1139            Some(10.0)
1140        );
1141        // The upper kern range carries the VariationIndex.
1142        assert_eq!(
1143            gi.math_kern_resolved(7, MathKernCorner::TopRight, 150, Some(&ivs), &[0.0]),
1144            Some(25.0)
1145        );
1146        assert_eq!(
1147            gi.math_kern_resolved(7, MathKernCorner::TopRight, 150, Some(&ivs), &[1.0]),
1148            Some(10.0)
1149        ); // 25 + (-15)
1150
1151        // Uncovered glyph → None on every accessor.
1152        assert!(gi
1153            .italics_correction_resolved(99, Some(&ivs), &[1.0])
1154            .is_none());
1155        assert!(gi
1156            .math_kern_resolved(99, MathKernCorner::TopRight, 0, Some(&ivs), &[1.0])
1157            .is_none());
1158    }
1159}