Skip to main content

read_fonts/tables/
math.rs

1//! The [MATH](https://learn.microsoft.com/en-us/typography/opentype/spec/math) table
2
3use super::layout::{CoverageTable, DeviceOrVariationIndex};
4
5include!("../../generated/generated_math.rs");
6
7/// Which corner of a glyph a [`MathKern`] applies to.
8#[derive(Clone, Copy, PartialEq, Eq, Debug)]
9pub enum MathKernCorner {
10    /// Above and to the right.
11    TopRight,
12    /// Above and to the left.
13    TopLeft,
14    /// Below and to the right.
15    BottomRight,
16    /// Below and to the left.
17    BottomLeft,
18}
19
20/// The axis a glyph is stretched along.
21#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22pub enum StretchAxis {
23    /// Grows to the left and right, like an overbrace.
24    Horizontal,
25    /// Grows up and down, like a parenthesis.
26    Vertical,
27}
28
29/// A `MATH` value read at a particular size.
30///
31/// The two fields are in different units and cannot simply be added: a device
32/// table adjusts by whole pixels, so scaling its adjustment into design units
33/// needs the units per em, which the `MATH` table does not know. A caller
34/// working in font units wants `value + delta_px * upem / ppem`; one working
35/// in pixels has the adjustment already.
36#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
37pub struct MathValue {
38    /// The value as the font stores it, in design units.
39    pub value: i32,
40    /// The adjustment the value's device table makes at the requested size,
41    /// in pixels. Zero when there is no device table, when it does not cover
42    /// that size, or when the offset names a `VariationIndex`.
43    pub delta_px: i32,
44}
45
46/// One band of a [`MathKern`] table.
47#[derive(Clone, Copy, PartialEq, Eq, Debug)]
48pub struct MathKernEntry {
49    /// The height the band reaches up to, or `None` for the last band, which
50    /// is unbounded.
51    pub max_height: Option<MathValue>,
52    /// The kern to apply within the band.
53    pub kern: MathValue,
54}
55
56impl Math<'_> {
57    /// Whether this is one of the Cambria Math builds that stores
58    /// `delimitedSubFormulaMinHeight` and `displayOperatorMinHeight` the wrong
59    /// way round.
60    ///
61    /// A display mode n-ary operator should have the taller of the two
62    /// thresholds, and in these builds it does not. Microsoft's implementation
63    /// reads them the other way round, so anything that wants to lay out maths
64    /// the way Word does has to swap them back; HarfBuzz does the same. This
65    /// only reports the font, and leaves that decision to the caller, so that
66    /// the constants keep saying what the font says.
67    ///
68    /// The font is recognized by its table length together with the two
69    /// values, since either value alone is one a font could legitimately hold.
70    /// That means it only answers `true` for a table read as a table: a
71    /// [`Math`] read from a buffer with anything after it measures longer and
72    /// is not recognized.
73    ///
74    /// See [harfbuzz#4653](https://github.com/harfbuzz/harfbuzz/issues/4653).
75    pub fn has_swapped_min_heights(&self) -> bool {
76        // The three known builds of cambria.ttc, by sha1:
77        // ab4a4fe054d23061f3c039493d6f665cfda2ecf5
78        // 086855301bff644f9d8827b88491fcf73a6d4cb9
79        // b1e5a3feaca2ea3dfcf79ccb377de749ecf60343
80        const TABLE_LEN: usize = 25722;
81        const DELIMITED_SUB_FORMULA_MIN_HEIGHT: u16 = 3000;
82        const DISPLAY_OPERATOR_MIN_HEIGHT: u16 = 2500;
83
84        if self.offset_data().len() != TABLE_LEN {
85            return false;
86        }
87        let Ok(constants) = self.math_constants() else {
88            return false;
89        };
90        constants.delimited_sub_formula_min_height().to_u16() == DELIMITED_SUB_FORMULA_MIN_HEIGHT
91            && constants.display_operator_min_height().to_u16() == DISPLAY_OPERATOR_MIN_HEIGHT
92    }
93}
94
95impl MathValueRecord {
96    /// The value as read at a size.
97    ///
98    /// `data` is the data of the table this record was read from, since its
99    /// device offset is measured from there rather than from the record.
100    pub fn value_for_ppem(&self, data: FontData<'_>, ppem: u16) -> MathValue {
101        // A `VariationIndex` yields nothing: the deltas one names live in an
102        // item variation store, and the `MATH` table has none.
103        let delta_px = match self.device(data) {
104            Some(Ok(DeviceOrVariationIndex::Device(device))) => device.delta_for_ppem(ppem),
105            _ => 0,
106        };
107        MathValue {
108            value: self.value().to_i16() as i32,
109            delta_px,
110        }
111    }
112}
113
114/// Declares the constants of `MathConstants`, in the order the spec lists them.
115///
116/// One list defines the [`MathConstant`] enum, its numbering, and the arm of
117/// [`MathConstants::constant`] that reads each one, so a constant cannot be
118/// numbered as one field and read from another. The matches have no catch-all,
119/// so a constant added here without a field will not compile.
120///
121/// The `record` constants are `MathValueRecord`s and can carry a device table.
122/// The `int` and `uint` ones are stored as bare integers, so they never do.
123macro_rules! math_constants {
124    ($($variant:ident = $discriminant:literal, $kind:ident $field:ident;)+) => {
125        /// A constant in the `MathConstants` subtable.
126        ///
127        /// The values must match HarfBuzz's [`hb_ot_math_constant_t`], which is
128        /// also the order the OpenType spec lists the fields in.
129        ///
130        /// [`hb_ot_math_constant_t`]: https://github.com/harfbuzz/harfbuzz/blob/92e67ef19f2d595b0fe81f05a80783a321bb918f/src/hb-ot-math.h#L133
131        #[derive(Clone, Copy, PartialEq, Eq, Debug)]
132        #[repr(u8)]
133        pub enum MathConstant {
134            $(
135                #[doc = concat!("See [`MathConstants::", stringify!($field), "`].")]
136                $variant = $discriminant,
137            )+
138        }
139
140        impl MathConstant {
141            /// Every constant, in the order the spec lists them.
142            ///
143            /// A constant's position here is its numeric value.
144            pub const ALL: &'static [MathConstant] = &[$(MathConstant::$variant,)+];
145
146            /// The constant with a given numeric value, or `None` if the value
147            /// names no constant.
148            pub fn new(value: u8) -> Option<Self> {
149                Self::ALL.get(value as usize).copied()
150            }
151        }
152
153        impl MathConstants<'_> {
154            /// The value of a constant, in design units.
155            pub fn constant(&self, constant: MathConstant) -> i32 {
156                match constant {
157                    $(
158                        MathConstant::$variant => math_constants!(@value self, $kind $field),
159                    )+
160                }
161            }
162
163            /// The value of a constant, as read at a size.
164            ///
165            /// The four percentages and the two minimum heights are stored as
166            /// bare integers, so their [`delta_px`][MathValue::delta_px] is
167            /// always zero.
168            pub fn constant_for_ppem(&self, constant: MathConstant, ppem: u16) -> MathValue {
169                match constant {
170                    $(
171                        MathConstant::$variant => math_constants!(@for_ppem self, $kind $field, ppem),
172                    )+
173                }
174            }
175        }
176    };
177    (@value $self:ident, record $field:ident) => {
178        $self.$field().value().to_i16() as i32
179    };
180    (@value $self:ident, int $field:ident) => {
181        $self.$field() as i32
182    };
183    (@value $self:ident, uint $field:ident) => {
184        $self.$field().to_u16() as i32
185    };
186    (@for_ppem $self:ident, record $field:ident, $ppem:ident) => {
187        $self.$field().value_for_ppem($self.offset_data(), $ppem)
188    };
189    (@for_ppem $self:ident, int $field:ident, $ppem:ident) => {
190        MathValue { value: $self.$field() as i32, delta_px: 0 }
191    };
192    (@for_ppem $self:ident, uint $field:ident, $ppem:ident) => {
193        MathValue { value: $self.$field().to_u16() as i32, delta_px: 0 }
194    };
195}
196
197math_constants! {
198    ScriptPercentScaleDown = 0, int script_percent_scale_down;
199    ScriptScriptPercentScaleDown = 1, int script_script_percent_scale_down;
200    DelimitedSubFormulaMinHeight = 2, uint delimited_sub_formula_min_height;
201    DisplayOperatorMinHeight = 3, uint display_operator_min_height;
202    MathLeading = 4, record math_leading;
203    AxisHeight = 5, record axis_height;
204    AccentBaseHeight = 6, record accent_base_height;
205    FlattenedAccentBaseHeight = 7, record flattened_accent_base_height;
206    SubscriptShiftDown = 8, record subscript_shift_down;
207    SubscriptTopMax = 9, record subscript_top_max;
208    SubscriptBaselineDropMin = 10, record subscript_baseline_drop_min;
209    SuperscriptShiftUp = 11, record superscript_shift_up;
210    SuperscriptShiftUpCramped = 12, record superscript_shift_up_cramped;
211    SuperscriptBottomMin = 13, record superscript_bottom_min;
212    SuperscriptBaselineDropMax = 14, record superscript_baseline_drop_max;
213    SubSuperscriptGapMin = 15, record sub_superscript_gap_min;
214    SuperscriptBottomMaxWithSubscript = 16, record superscript_bottom_max_with_subscript;
215    SpaceAfterScript = 17, record space_after_script;
216    UpperLimitGapMin = 18, record upper_limit_gap_min;
217    UpperLimitBaselineRiseMin = 19, record upper_limit_baseline_rise_min;
218    LowerLimitGapMin = 20, record lower_limit_gap_min;
219    LowerLimitBaselineDropMin = 21, record lower_limit_baseline_drop_min;
220    StackTopShiftUp = 22, record stack_top_shift_up;
221    StackTopDisplayStyleShiftUp = 23, record stack_top_display_style_shift_up;
222    StackBottomShiftDown = 24, record stack_bottom_shift_down;
223    StackBottomDisplayStyleShiftDown = 25, record stack_bottom_display_style_shift_down;
224    StackGapMin = 26, record stack_gap_min;
225    StackDisplayStyleGapMin = 27, record stack_display_style_gap_min;
226    StretchStackTopShiftUp = 28, record stretch_stack_top_shift_up;
227    StretchStackBottomShiftDown = 29, record stretch_stack_bottom_shift_down;
228    StretchStackGapAboveMin = 30, record stretch_stack_gap_above_min;
229    StretchStackGapBelowMin = 31, record stretch_stack_gap_below_min;
230    FractionNumeratorShiftUp = 32, record fraction_numerator_shift_up;
231    FractionNumeratorDisplayStyleShiftUp = 33, record fraction_numerator_display_style_shift_up;
232    FractionDenominatorShiftDown = 34, record fraction_denominator_shift_down;
233    FractionDenominatorDisplayStyleShiftDown = 35, record fraction_denominator_display_style_shift_down;
234    FractionNumeratorGapMin = 36, record fraction_numerator_gap_min;
235    FractionNumDisplayStyleGapMin = 37, record fraction_num_display_style_gap_min;
236    FractionRuleThickness = 38, record fraction_rule_thickness;
237    FractionDenominatorGapMin = 39, record fraction_denominator_gap_min;
238    FractionDenomDisplayStyleGapMin = 40, record fraction_denom_display_style_gap_min;
239    SkewedFractionHorizontalGap = 41, record skewed_fraction_horizontal_gap;
240    SkewedFractionVerticalGap = 42, record skewed_fraction_vertical_gap;
241    OverbarVerticalGap = 43, record overbar_vertical_gap;
242    OverbarRuleThickness = 44, record overbar_rule_thickness;
243    OverbarExtraAscender = 45, record overbar_extra_ascender;
244    UnderbarVerticalGap = 46, record underbar_vertical_gap;
245    UnderbarRuleThickness = 47, record underbar_rule_thickness;
246    UnderbarExtraDescender = 48, record underbar_extra_descender;
247    RadicalVerticalGap = 49, record radical_vertical_gap;
248    RadicalDisplayStyleVerticalGap = 50, record radical_display_style_vertical_gap;
249    RadicalRuleThickness = 51, record radical_rule_thickness;
250    RadicalExtraAscender = 52, record radical_extra_ascender;
251    RadicalKernBeforeDegree = 53, record radical_kern_before_degree;
252    RadicalKernAfterDegree = 54, record radical_kern_after_degree;
253    RadicalDegreeBottomRaisePercent = 55, int radical_degree_bottom_raise_percent;
254}
255
256impl MathGlyphInfo<'_> {
257    /// Whether a glyph is an extended shape: one tall enough that a following
258    /// script is positioned against its own height rather than the font's.
259    pub fn is_extended_shape(&self, glyph: GlyphId) -> bool {
260        self.extended_shape_coverage()
261            .and_then(|coverage| coverage.ok())
262            .and_then(|coverage| coverage.get(glyph))
263            .is_some()
264    }
265}
266
267impl MathItalicsCorrectionInfo<'_> {
268    /// The italics correction for a glyph in design units, or `None` where the
269    /// table does not cover it.
270    pub fn correction(&self, glyph: GlyphId) -> Option<i32> {
271        Some(self.record(glyph)?.value().to_i16() as i32)
272    }
273
274    /// The italics correction for a glyph, as read at a size, or `None` where
275    /// the table does not cover it.
276    pub fn correction_for_ppem(&self, glyph: GlyphId, ppem: u16) -> Option<MathValue> {
277        Some(self.record(glyph)?.value_for_ppem(self.offset_data(), ppem))
278    }
279
280    fn record(&self, glyph: GlyphId) -> Option<&MathValueRecord> {
281        let index = self.coverage().ok()?.get(glyph)?;
282        self.italics_correction().get(index as usize)
283    }
284}
285
286impl MathTopAccentAttachment<'_> {
287    /// The horizontal position at which an accent sits above a glyph, in
288    /// design units, or `None` where the table does not cover it.
289    pub fn attachment(&self, glyph: GlyphId) -> Option<i32> {
290        Some(self.record(glyph)?.value().to_i16() as i32)
291    }
292
293    /// The horizontal position at which an accent sits above a glyph, as read
294    /// at a size, or `None` where the table does not cover it.
295    pub fn attachment_for_ppem(&self, glyph: GlyphId, ppem: u16) -> Option<MathValue> {
296        Some(self.record(glyph)?.value_for_ppem(self.offset_data(), ppem))
297    }
298
299    fn record(&self, glyph: GlyphId) -> Option<&MathValueRecord> {
300        let index = self.top_accent_coverage().ok()?.get(glyph)?;
301        self.top_accent_attachment().get(index as usize)
302    }
303}
304
305impl<'a> MathKernInfo<'a> {
306    /// The kerning for one corner of a glyph, or `None` where the table does
307    /// not cover the glyph or gives that corner nothing.
308    pub fn kern(&self, glyph: GlyphId, corner: MathKernCorner) -> Option<MathKern<'a>> {
309        let index = self.math_kern_coverage().ok()?.get(glyph)?;
310        let record = self.math_kern_info_records().get(index as usize)?;
311        let data = self.offset_data();
312        match corner {
313            MathKernCorner::TopRight => record.top_right_math_kern(data),
314            MathKernCorner::TopLeft => record.top_left_math_kern(data),
315            MathKernCorner::BottomRight => record.bottom_right_math_kern(data),
316            MathKernCorner::BottomLeft => record.bottom_left_math_kern(data),
317        }?
318        .ok()
319    }
320}
321
322impl MathKern<'_> {
323    /// The kern to apply at a height above the baseline, in design units.
324    ///
325    /// The table divides the vertical range into bands, and this answers the
326    /// value for the band `correction_height` falls in: the one whose lower
327    /// bound is the last correction height at or below it. A height above
328    /// every correction height takes the final value.
329    ///
330    /// Correction heights are stored in ascending order and there are rarely
331    /// more than a handful, so this walks them.
332    ///
333    /// The heights are compared as the font stores them, without the device
334    /// adjustments they may carry. Applying those would mean choosing a size
335    /// and a coordinate space to compare in, which is the caller's to choose:
336    /// [`entries_for_ppem`][Self::entries_for_ppem] hands over the bands so it
337    /// can.
338    pub fn kerning(&self, correction_height: i32) -> Option<i32> {
339        let value = self.kern_values().get(self.band(correction_height))?;
340        Some(value.value().to_i16() as i32)
341    }
342
343    /// The upper bound and kern of each band, lowest first.
344    ///
345    /// Every value comes as read at `ppem`, carrying its own adjustment.
346    ///
347    /// This is what to pick a kern from when the adjustments matter. Which
348    /// band a height falls in depends on the space the comparison happens in
349    /// -- design units, pixels, or a scaled position, the last of which also
350    /// decides whether the vertical axis points up or down -- and only the
351    /// caller knows that.
352    pub fn entries_for_ppem(&self, ppem: u16) -> impl Iterator<Item = MathKernEntry> + '_ {
353        let data = self.offset_data();
354        let heights = self.correction_height();
355        self.kern_values()
356            .iter()
357            .enumerate()
358            .map(move |(i, kern)| MathKernEntry {
359                max_height: heights
360                    .get(i)
361                    .map(|height| height.value_for_ppem(data, ppem)),
362                kern: kern.value_for_ppem(data, ppem),
363            })
364    }
365
366    /// The index of the band a height falls in.
367    fn band(&self, correction_height: i32) -> usize {
368        let heights = self.correction_height();
369        heights
370            .iter()
371            .position(|height| correction_height < height.value().to_i16() as i32)
372            .unwrap_or(heights.len())
373    }
374}
375
376impl<'a> MathVariants<'a> {
377    /// How a glyph is stretched along an axis, or `None` where the table does
378    /// not stretch it that way.
379    pub fn glyph_construction(
380        &self,
381        glyph: GlyphId,
382        axis: StretchAxis,
383    ) -> Option<MathGlyphConstruction<'a>> {
384        let index = match axis {
385            StretchAxis::Vertical => self.vert_glyph_coverage(),
386            StretchAxis::Horizontal => self.horiz_glyph_coverage(),
387        }?
388        .ok()?
389        .get(glyph)?;
390        match axis {
391            StretchAxis::Vertical => self.vert_glyph_constructions(),
392            StretchAxis::Horizontal => self.horiz_glyph_constructions(),
393        }
394        .get(index as usize)
395        .ok()
396    }
397}
398
399impl GlyphAssembly<'_> {
400    /// The italics correction of the assembled glyph, as read at a size.
401    pub fn italics_correction_for_ppem(&self, ppem: u16) -> MathValue {
402        self.italics_correction()
403            .value_for_ppem(self.offset_data(), ppem)
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use font_test_data::bebuffer::BeBuffer;
411
412    /// Index of `math_leading` among the MathValueRecords of MathConstants.
413    ///
414    /// The test data gives it a `VariationIndex`, which the `MATH` table has
415    /// no store to resolve against.
416    const MATH_LEADING_INDEX: usize = 0;
417
418    /// Index of `axis_height` among the MathValueRecords of MathConstants.
419    ///
420    /// The test data gives it the one `Device` table among the constants.
421    const AXIS_HEIGHT_INDEX: usize = 1;
422
423    /// The MathValueRecords of MathConstants, in spec order, named as they are
424    /// on the generated table.
425    ///
426    /// The test data gives the record at index `i` the value `100 + i`, so a
427    /// field transcribed out of order reads back as one of its neighbours.
428    const VALUE_RECORD_ORDER: &[&str] = &[
429        "math_leading",
430        "axis_height",
431        "accent_base_height",
432        "flattened_accent_base_height",
433        "subscript_shift_down",
434        "subscript_top_max",
435        "subscript_baseline_drop_min",
436        "superscript_shift_up",
437        "superscript_shift_up_cramped",
438        "superscript_bottom_min",
439        "superscript_baseline_drop_max",
440        "sub_superscript_gap_min",
441        "superscript_bottom_max_with_subscript",
442        "space_after_script",
443        "upper_limit_gap_min",
444        "upper_limit_baseline_rise_min",
445        "lower_limit_gap_min",
446        "lower_limit_baseline_drop_min",
447        "stack_top_shift_up",
448        "stack_top_display_style_shift_up",
449        "stack_bottom_shift_down",
450        "stack_bottom_display_style_shift_down",
451        "stack_gap_min",
452        "stack_display_style_gap_min",
453        "stretch_stack_top_shift_up",
454        "stretch_stack_bottom_shift_down",
455        "stretch_stack_gap_above_min",
456        "stretch_stack_gap_below_min",
457        "fraction_numerator_shift_up",
458        "fraction_numerator_display_style_shift_up",
459        "fraction_denominator_shift_down",
460        "fraction_denominator_display_style_shift_down",
461        "fraction_numerator_gap_min",
462        "fraction_num_display_style_gap_min",
463        "fraction_rule_thickness",
464        "fraction_denominator_gap_min",
465        "fraction_denom_display_style_gap_min",
466        "skewed_fraction_horizontal_gap",
467        "skewed_fraction_vertical_gap",
468        "overbar_vertical_gap",
469        "overbar_rule_thickness",
470        "overbar_extra_ascender",
471        "underbar_vertical_gap",
472        "underbar_rule_thickness",
473        "underbar_extra_descender",
474        "radical_vertical_gap",
475        "radical_display_style_vertical_gap",
476        "radical_rule_thickness",
477        "radical_extra_ascender",
478        "radical_kern_before_degree",
479        "radical_kern_after_degree",
480    ];
481
482    /// A MATH table with all three subtables, built so that every value
483    /// identifies the field it belongs to.
484    fn math_table() -> BeBuffer {
485        let mut buf = BeBuffer::new()
486            // MATH header
487            .push(MajorMinor::VERSION_1_0)
488            .push_with_tag(0u16, "constants_offset")
489            .push_with_tag(0u16, "glyph_info_offset")
490            .push_with_tag(0u16, "variants_offset");
491
492        // -- MathConstants --
493        let constants = buf.len();
494        buf = buf
495            .push(1i16) // script_percent_scale_down
496            .push(2i16) // script_script_percent_scale_down
497            .push_with_tag(3u16, "delimited_sub_formula_min_height")
498            .push_with_tag(4u16, "display_operator_min_height");
499        for i in 0..VALUE_RECORD_ORDER.len() {
500            buf = buf.push(100i16 + i as i16); // value
501            buf = match i {
502                MATH_LEADING_INDEX => buf.push_with_tag(0u16, "math_leading_device"),
503                AXIS_HEIGHT_INDEX => buf.push_with_tag(0u16, "axis_height_device"),
504                _ => buf.push(0u16), // null device
505            };
506        }
507        buf = buf.push(5i16); // radical_degree_bottom_raise_percent
508
509        // -- MathGlyphInfo --
510        let glyph_info = buf.len();
511        buf = buf
512            .push_with_tag(0u16, "italics_offset")
513            .push(0u16) // top accent attachment: null
514            .push(0u16) // extended shape coverage: null
515            .push_with_tag(0u16, "kern_info_offset");
516
517        // MathItalicsCorrectionInfo, covering one glyph, with a device table.
518        let italics = buf.len();
519        buf = buf
520            .push_with_tag(0u16, "italics_coverage_offset")
521            .push(1u16) // count
522            .push(37i16) // value
523            .push_with_tag(0u16, "italics_device_offset");
524        let italics_coverage = buf.len();
525        buf = buf
526            .push(1u16) // coverage format 1
527            .push(1u16) // glyph count
528            .push(9u16); // glyph 9
529        let italics_device = buf.len();
530        buf = buf
531            .push(11u16) // start size
532            .push(12u16) // end size
533            .push(3u16) // delta format: 8 bit
534            .push(0x05FFu16); // ppem 11 -> 5, ppem 12 -> -1
535
536        // MathKernInfo, one record carrying a single top-right kern.
537        let kern_info = buf.len();
538        buf = buf
539            .push_with_tag(0u16, "kern_coverage_offset")
540            .push(1u16) // count
541            .push_with_tag(0u16, "top_right_kern_offset")
542            .push(0u16) // top left: null
543            .push(0u16) // bottom right: null
544            .push(0u16); // bottom left: null
545        let kern_coverage = buf.len();
546        buf = buf.push(1u16).push(1u16).push(9u16);
547        let math_kern = buf.len();
548        buf = buf
549            .push(2u16) // height_count
550            .push(20i16) // correction height 0
551            .push(0u16)
552            .push(40i16) // correction height 1
553            .push_with_tag(0u16, "kern_height_device")
554            .push(-1i16) // kern value 0
555            .push(0u16)
556            .push(-2i16) // kern value 1
557            .push_with_tag(0u16, "kern_value_device")
558            .push(-3i16) // kern value 2, the height_count + 1 entry
559            .push(0u16);
560
561        // -- MathVariants --
562        let variants = buf.len();
563        buf = buf
564            .push(6u16) // min_connector_overlap
565            .push_with_tag(0u16, "vert_coverage_offset")
566            .push(0u16) // horizontal coverage: null
567            .push(1u16) // vert_glyph_count
568            .push(0u16) // horiz_glyph_count
569            .push_with_tag(0u16, "vert_construction_offset");
570        let vert_coverage = buf.len();
571        buf = buf.push(1u16).push(1u16).push(9u16);
572        let construction = buf.len();
573        buf = buf
574            .push_with_tag(0u16, "assembly_offset")
575            .push(2u16) // variant_count
576            .push(21u16) // variant glyph
577            .push(300u16) // advance
578            .push(22u16)
579            .push(400u16);
580        let assembly = buf.len();
581        buf = buf
582            .push(17i16) // italics correction value
583            .push(0u16) // italics correction device: null
584            .push(1u16) // part_count
585            .push(23u16) // glyph
586            .push(10u16) // start connector
587            .push(11u16) // end connector
588            .push(50u16) // full advance
589            .push(1u16); // part flags: extender
590
591        // Device tables inside MathKern: one on a correction height, one on a
592        // kern value, so the two are told apart.
593        let kern_height_device = buf.len();
594        buf = buf
595            .push(10u16) // start size
596            .push(11u16) // end size
597            .push(3u16) // delta format: 8 bit
598            .push(0x04FAu16); // ppem 10 -> 4, ppem 11 -> -6
599        let kern_value_device = buf.len();
600        buf = buf
601            .push(10u16) // start size
602            .push(11u16) // end size
603            .push(3u16) // delta format: 8 bit
604            .push(0x0102u16); // ppem 10 -> 1, ppem 11 -> 2
605
606        // A Device table for axis_height.
607        let axis_height_device = buf.len();
608        buf = buf
609            .push(10u16) // start size
610            .push(12u16) // end size
611            .push(3u16) // delta format: 8 bit
612            .push(0xFE03u16) // ppem 10 -> -2, ppem 11 -> 3
613            .push(0x0700u16); // ppem 12 -> 7
614
615        // A VariationIndex for math_leading. Nothing can resolve it: the
616        // deltas it names would live in an item variation store, and the MATH
617        // table has none.
618        let math_leading_varidx = buf.len();
619        buf = buf
620            .push(0u16) // delta set outer index
621            .push(0u16) // delta set inner index
622            .push(0x8000u16); // delta format: VARIATION_INDEX
623
624        buf.write_at("constants_offset", constants as u16);
625        buf.write_at("glyph_info_offset", glyph_info as u16);
626        buf.write_at("variants_offset", variants as u16);
627        buf.write_at("italics_offset", (italics - glyph_info) as u16);
628        buf.write_at("kern_info_offset", (kern_info - glyph_info) as u16);
629        buf.write_at(
630            "italics_coverage_offset",
631            (italics_coverage - italics) as u16,
632        );
633        buf.write_at("italics_device_offset", (italics_device - italics) as u16);
634        buf.write_at("kern_coverage_offset", (kern_coverage - kern_info) as u16);
635        buf.write_at("top_right_kern_offset", (math_kern - kern_info) as u16);
636        // Both are measured from MathKern, the table holding the records.
637        buf.write_at(
638            "kern_height_device",
639            (kern_height_device - math_kern) as u16,
640        );
641        buf.write_at("kern_value_device", (kern_value_device - math_kern) as u16);
642        buf.write_at("vert_coverage_offset", (vert_coverage - variants) as u16);
643        buf.write_at("vert_construction_offset", (construction - variants) as u16);
644        buf.write_at("assembly_offset", (assembly - construction) as u16);
645        // Both are measured from MathConstants, the table holding the records.
646        buf.write_at(
647            "axis_height_device",
648            (axis_height_device - constants) as u16,
649        );
650        buf.write_at(
651            "math_leading_device",
652            (math_leading_varidx - constants) as u16,
653        );
654        buf
655    }
656
657    /// A value with no device adjustment.
658    fn plain(value: i32) -> MathValue {
659        MathValue { value, delta_px: 0 }
660    }
661
662    /// A value and the pixel adjustment its device table makes.
663    fn adjusted(value: i32, delta_px: i32) -> MathValue {
664        MathValue { value, delta_px }
665    }
666
667    /// A kern band.
668    fn entry(max_height: Option<MathValue>, kern: MathValue) -> MathKernEntry {
669        MathKernEntry { max_height, kern }
670    }
671
672    /// The length of the MATH table in the known-bad Cambria Math builds.
673    const CAMBRIA_TABLE_LEN: usize = 25722;
674
675    /// The test table with the two minimum heights set, padded to a length.
676    fn cambria_table(len: usize, delimited: u16, display_operator: u16) -> BeBuffer {
677        let mut buf = math_table();
678        buf.write_at("delimited_sub_formula_min_height", delimited);
679        buf.write_at("display_operator_min_height", display_operator);
680        let padding = len - buf.len();
681        buf.extend(vec![0u8; padding])
682    }
683
684    fn math(buf: &BeBuffer) -> Math<'_> {
685        Math::read(buf.data().into()).unwrap()
686    }
687
688    #[test]
689    fn constants_are_in_spec_order() {
690        let buf = math_table();
691        let math = math(&buf);
692        let constants = math.math_constants().unwrap();
693
694        assert_eq!(constants.script_percent_scale_down(), 1);
695        assert_eq!(constants.script_script_percent_scale_down(), 2);
696        assert_eq!(constants.delimited_sub_formula_min_height(), UfWord::new(3));
697        assert_eq!(constants.display_operator_min_height(), UfWord::new(4));
698        assert_eq!(constants.radical_degree_bottom_raise_percent(), 5);
699
700        // Each record carries its own index, so reading them in the order they
701        // are named checks the whole run in one go.
702        let values = [
703            constants.math_leading(),
704            constants.axis_height(),
705            constants.accent_base_height(),
706            constants.flattened_accent_base_height(),
707            constants.subscript_shift_down(),
708            constants.subscript_top_max(),
709            constants.subscript_baseline_drop_min(),
710            constants.superscript_shift_up(),
711            constants.superscript_shift_up_cramped(),
712            constants.superscript_bottom_min(),
713            constants.superscript_baseline_drop_max(),
714            constants.sub_superscript_gap_min(),
715            constants.superscript_bottom_max_with_subscript(),
716            constants.space_after_script(),
717            constants.upper_limit_gap_min(),
718            constants.upper_limit_baseline_rise_min(),
719            constants.lower_limit_gap_min(),
720            constants.lower_limit_baseline_drop_min(),
721            constants.stack_top_shift_up(),
722            constants.stack_top_display_style_shift_up(),
723            constants.stack_bottom_shift_down(),
724            constants.stack_bottom_display_style_shift_down(),
725            constants.stack_gap_min(),
726            constants.stack_display_style_gap_min(),
727            constants.stretch_stack_top_shift_up(),
728            constants.stretch_stack_bottom_shift_down(),
729            constants.stretch_stack_gap_above_min(),
730            constants.stretch_stack_gap_below_min(),
731            constants.fraction_numerator_shift_up(),
732            constants.fraction_numerator_display_style_shift_up(),
733            constants.fraction_denominator_shift_down(),
734            constants.fraction_denominator_display_style_shift_down(),
735            constants.fraction_numerator_gap_min(),
736            constants.fraction_num_display_style_gap_min(),
737            constants.fraction_rule_thickness(),
738            constants.fraction_denominator_gap_min(),
739            constants.fraction_denom_display_style_gap_min(),
740            constants.skewed_fraction_horizontal_gap(),
741            constants.skewed_fraction_vertical_gap(),
742            constants.overbar_vertical_gap(),
743            constants.overbar_rule_thickness(),
744            constants.overbar_extra_ascender(),
745            constants.underbar_vertical_gap(),
746            constants.underbar_rule_thickness(),
747            constants.underbar_extra_descender(),
748            constants.radical_vertical_gap(),
749            constants.radical_display_style_vertical_gap(),
750            constants.radical_rule_thickness(),
751            constants.radical_extra_ascender(),
752            constants.radical_kern_before_degree(),
753            constants.radical_kern_after_degree(),
754        ];
755        assert_eq!(values.len(), VALUE_RECORD_ORDER.len());
756        for (i, (value, name)) in values.iter().zip(VALUE_RECORD_ORDER).enumerate() {
757            assert_eq!(
758                value.value(),
759                FWord::new(100 + i as i16),
760                "{name} is not at spec index {i}"
761            );
762        }
763    }
764
765    #[test]
766    fn cambria_math_is_recognized_by_all_three_values() {
767        // The whole fingerprint: the table length and both heights.
768        let buf = cambria_table(CAMBRIA_TABLE_LEN, 3000, 2500);
769        assert!(math(&buf).has_swapped_min_heights());
770
771        // A byte longer is a different build of the font.
772        let buf = cambria_table(CAMBRIA_TABLE_LEN + 1, 3000, 2500);
773        assert!(!math(&buf).has_swapped_min_heights());
774
775        // The right length, but the heights are the right way round.
776        let buf = cambria_table(CAMBRIA_TABLE_LEN, 2500, 3000);
777        assert!(!math(&buf).has_swapped_min_heights());
778
779        // And an ordinary font is not it.
780        let buf = math_table();
781        assert!(!math(&buf).has_swapped_min_heights());
782    }
783
784    #[test]
785    fn constant_numbering_matches_harfbuzz() {
786        // These cross to and from C as plain integers, so the two ends have to
787        // agree on every one of them.
788        assert_eq!(MathConstant::ALL.len(), 56);
789        for (i, constant) in MathConstant::ALL.iter().enumerate() {
790            assert_eq!(*constant as usize, i);
791            assert_eq!(MathConstant::new(i as u8), Some(*constant));
792        }
793        assert_eq!(MathConstant::ScriptPercentScaleDown as u8, 0);
794        assert_eq!(MathConstant::MathLeading as u8, 4);
795        assert_eq!(MathConstant::AxisHeight as u8, 5);
796        assert_eq!(MathConstant::RadicalKernAfterDegree as u8, 54);
797        assert_eq!(MathConstant::RadicalDegreeBottomRaisePercent as u8, 55);
798        assert_eq!(MathConstant::new(56), None);
799    }
800
801    #[test]
802    fn every_constant_reads_its_own_field() {
803        let buf = math_table();
804        let math = math(&buf);
805        let constants = math.math_constants().unwrap();
806        let first_record = MathConstant::MathLeading as i32;
807
808        // The test data numbers each MathValueRecord by its position, so a
809        // constant wired to the wrong field reads back as its neighbour.
810        for constant in MathConstant::ALL {
811            let expected = match constant {
812                MathConstant::ScriptPercentScaleDown => 1,
813                MathConstant::ScriptScriptPercentScaleDown => 2,
814                MathConstant::DelimitedSubFormulaMinHeight => 3,
815                MathConstant::DisplayOperatorMinHeight => 4,
816                MathConstant::RadicalDegreeBottomRaisePercent => 5,
817                record => 100 + (*record as i32 - first_record),
818            };
819            assert_eq!(
820                constants.constant(*constant),
821                expected,
822                "{constant:?} does not read its own field"
823            );
824            // With no device table the two agree, and the bare integers never
825            // have one at all.
826            assert_eq!(constants.constant_for_ppem(*constant, 12).value, expected);
827        }
828    }
829
830    #[test]
831    fn constant_device_deltas_track_ppem() {
832        let buf = math_table();
833        let math = math(&buf);
834        let constants = math.math_constants().unwrap();
835        let stored = 100 + AXIS_HEIGHT_INDEX as i32;
836
837        // axis_height's device covers 10 to 12.
838        assert_eq!(
839            constants.constant_for_ppem(MathConstant::AxisHeight, 10),
840            adjusted(stored, -2)
841        );
842        assert_eq!(
843            constants.constant_for_ppem(MathConstant::AxisHeight, 11),
844            adjusted(stored, 3)
845        );
846        assert_eq!(
847            constants.constant_for_ppem(MathConstant::AxisHeight, 12),
848            adjusted(stored, 7)
849        );
850        // Outside the range it covers, and at no size at all.
851        assert_eq!(
852            constants.constant_for_ppem(MathConstant::AxisHeight, 9),
853            plain(stored)
854        );
855        assert_eq!(
856            constants.constant_for_ppem(MathConstant::AxisHeight, 13),
857            plain(stored)
858        );
859        assert_eq!(
860            constants.constant_for_ppem(MathConstant::AxisHeight, 0),
861            plain(stored)
862        );
863    }
864
865    #[test]
866    fn variation_index_yields_no_adjustment() {
867        let buf = math_table();
868        let math = math(&buf);
869        let constants = math.math_constants().unwrap();
870        let stored = 100 + MATH_LEADING_INDEX as i32;
871
872        // math_leading's offset points at a VariationIndex. The deltas it
873        // names would live in an item variation store, and MATH has none, so
874        // there is nothing to apply at any size.
875        assert_eq!(constants.constant(MathConstant::MathLeading), stored);
876        for ppem in [0u16, 10, 11, 12, 100] {
877            assert_eq!(
878                constants.constant_for_ppem(MathConstant::MathLeading, ppem),
879                plain(stored)
880            );
881        }
882    }
883
884    #[test]
885    fn italics_correction_resolves_against_its_parent() {
886        let buf = math_table();
887        let math = math(&buf);
888        let info = math
889            .math_glyph_info()
890            .unwrap()
891            .math_italics_correction_info()
892            .unwrap()
893            .unwrap();
894
895        assert_eq!(info.coverage().unwrap().get(GlyphId::new(9)), Some(0));
896        assert_eq!(info.correction(GlyphId::new(9)), Some(37));
897        // Glyph 10 is outside the coverage.
898        assert_eq!(info.correction(GlyphId::new(10)), None);
899
900        // The device offset is measured from MathItalicsCorrectionInfo rather
901        // than from the record, so it only resolves against the parent's data.
902        assert_eq!(
903            info.correction_for_ppem(GlyphId::new(9), 11),
904            Some(adjusted(37, 5))
905        );
906        assert_eq!(
907            info.correction_for_ppem(GlyphId::new(9), 12),
908            Some(adjusted(37, -1))
909        );
910        assert_eq!(
911            info.correction_for_ppem(GlyphId::new(9), 20),
912            Some(plain(37))
913        );
914    }
915
916    #[test]
917    fn glyph_info_lookups() {
918        let buf = math_table();
919        let math = math(&buf);
920        let glyph_info = math.math_glyph_info().unwrap();
921
922        // The font has no top accent attachment or extended shape coverage.
923        assert!(glyph_info.math_top_accent_attachment().is_none());
924        assert!(!glyph_info.is_extended_shape(GlyphId::new(9)));
925    }
926
927    #[test]
928    fn math_kern_has_one_more_value_than_height() {
929        let buf = math_table();
930        let math = math(&buf);
931        let kern_info = math
932            .math_glyph_info()
933            .unwrap()
934            .math_kern_info()
935            .unwrap()
936            .unwrap();
937
938        let kern = kern_info
939            .kern(GlyphId::new(9), MathKernCorner::TopRight)
940            .unwrap();
941
942        assert_eq!(kern.height_count(), 2);
943        assert_eq!(
944            kern.correction_height()
945                .iter()
946                .map(|v| v.value())
947                .collect::<Vec<_>>(),
948            vec![FWord::new(20), FWord::new(40)]
949        );
950        // Three kern values for two heights.
951        assert_eq!(
952            kern.kern_values()
953                .iter()
954                .map(|v| v.value())
955                .collect::<Vec<_>>(),
956            vec![FWord::new(-1), FWord::new(-2), FWord::new(-3)]
957        );
958
959        // The other three corners are null in this font.
960        assert!(kern_info
961            .kern(GlyphId::new(9), MathKernCorner::TopLeft)
962            .is_none());
963        // And glyph 10 is outside the coverage.
964        assert!(kern_info
965            .kern(GlyphId::new(10), MathKernCorner::TopRight)
966            .is_none());
967    }
968
969    #[test]
970    fn kerning_picks_the_band_the_height_falls_in() {
971        let buf = math_table();
972        let math = math(&buf);
973        let kern = math
974            .math_glyph_info()
975            .unwrap()
976            .math_kern_info()
977            .unwrap()
978            .unwrap()
979            .kern(GlyphId::new(9), MathKernCorner::TopRight)
980            .unwrap();
981
982        // Correction heights are 20 and 40, kern values -1, -2 and -3.
983        assert_eq!(kern.kerning(0), Some(-1));
984        assert_eq!(kern.kerning(19), Some(-1));
985        // A height equal to a correction height starts the next band.
986        assert_eq!(kern.kerning(20), Some(-2));
987        assert_eq!(kern.kerning(39), Some(-2));
988        assert_eq!(kern.kerning(40), Some(-3));
989        assert_eq!(kern.kerning(1000), Some(-3));
990    }
991
992    #[test]
993    fn kern_entries_carry_their_own_adjustments() {
994        let buf = math_table();
995        let math = math(&buf);
996        let kern = math
997            .math_glyph_info()
998            .unwrap()
999            .math_kern_info()
1000            .unwrap()
1001            .unwrap()
1002            .kern(GlyphId::new(9), MathKernCorner::TopRight)
1003            .unwrap();
1004
1005        // Three bands for two heights, the last one unbounded. The second
1006        // height and the second kern each carry a device table, and they are
1007        // adjusted independently.
1008        assert_eq!(
1009            kern.entries_for_ppem(10).collect::<Vec<_>>(),
1010            vec![
1011                entry(Some(plain(20)), plain(-1)),
1012                entry(Some(adjusted(40, 4)), adjusted(-2, 1)),
1013                entry(None, plain(-3)),
1014            ]
1015        );
1016        assert_eq!(
1017            kern.entries_for_ppem(11).collect::<Vec<_>>(),
1018            vec![
1019                entry(Some(plain(20)), plain(-1)),
1020                entry(Some(adjusted(40, -6)), adjusted(-2, 2)),
1021                entry(None, plain(-3)),
1022            ]
1023        );
1024        // Outside the sizes those devices cover, nothing is adjusted.
1025        assert_eq!(
1026            kern.entries_for_ppem(12).collect::<Vec<_>>(),
1027            vec![
1028                entry(Some(plain(20)), plain(-1)),
1029                entry(Some(plain(40)), plain(-2)),
1030                entry(None, plain(-3)),
1031            ]
1032        );
1033
1034        // `kerning` compares stored heights, so it is unaffected by them.
1035        assert_eq!(kern.kerning(40), Some(-3));
1036    }
1037
1038    #[test]
1039    fn variants_and_assembly() {
1040        let buf = math_table();
1041        let math = math(&buf);
1042        let variants = math.math_variants().unwrap();
1043
1044        assert_eq!(variants.min_connector_overlap(), UfWord::new(6));
1045
1046        let construction = variants
1047            .glyph_construction(GlyphId::new(9), StretchAxis::Vertical)
1048            .unwrap();
1049        assert_eq!(
1050            construction
1051                .math_glyph_variant_records()
1052                .iter()
1053                .map(|r| (r.variant_glyph(), r.advance_measurement()))
1054                .collect::<Vec<_>>(),
1055            vec![
1056                (GlyphId16::new(21), UfWord::new(300)),
1057                (GlyphId16::new(22), UfWord::new(400)),
1058            ]
1059        );
1060
1061        // Nothing stretches horizontally in this font.
1062        assert!(variants
1063            .glyph_construction(GlyphId::new(9), StretchAxis::Horizontal)
1064            .is_none());
1065
1066        let assembly = construction.glyph_assembly().unwrap().unwrap();
1067        assert_eq!(assembly.italics_correction().value(), FWord::new(17));
1068        // Its italics correction has no device table.
1069        assert_eq!(assembly.italics_correction_for_ppem(12), plain(17));
1070
1071        let part = &assembly.part_records()[0];
1072        assert_eq!(part.glyph_id(), GlyphId16::new(23));
1073        assert_eq!(part.start_connector_length(), UfWord::new(10));
1074        assert_eq!(part.end_connector_length(), UfWord::new(11));
1075        assert_eq!(part.full_advance(), UfWord::new(50));
1076        assert!(part.part_flags().contains(PartFlags::EXTENDER_FLAG));
1077    }
1078}