harfbuzz / harfbuzz/harfrust

Plan for supporting hb_ot_math_* functions in the C FFI

Open
#466 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
301
Forks
26
Avg merge
10h 11m
Merged PRs (30d)
43

Description

# Implement the `hb_ot_math_*` C API against harfrust

## Goal

Provide the eleven `hb_ot_math_*` entry points directly from Rust, so the
`hb-harfrust.cc` bridge is not the only path to math metrics. All of the
table reading is already available; what remains is the C surface plus the
font-level arithmetic HarfBuzz does around it.

## Background

`read-fonts` gained a `MATH` table (branch `tables/math`). Two facts shape
the design:

- **`MATH` has no item variation store.** A `VariationIndex` inside it names
a delta set in a store the table does not have, so it contributes nothing.
Only hinting `Device` tables apply, and those are indexed by ppem.
- **Device adjustments are in pixels, not design units.** Converting one to
font units needs upem, which the table does not know, so `read-fonts`
returns the two separately and leaves the arithmetic to the caller.

## What read-fonts provides

```rust
// entry
font.math() -> Result
Math::has_swapped_min_heights() -> bool // Cambria Math quirk
Math::version() -> MajorMinor

// values read at a size
struct MathValue { value: i32, delta_px: i32 } // design units, pixels

// constants
enum MathConstant { .. } // 56, numbered as hb_ot_math_constant_t
MathConstant::new(u8) -> Option
MathConstant::ALL -> &[MathConstant]
MathConstants::constant(MathConstant) -> i32
MathConstants::constant_for_ppem(MathConstant, ppem: u16) -> MathValue

// per glyph
MathGlyphInfo::is_extended_shape(GlyphId) -> bool
MathItalicsCorrectionInfo::correction(GlyphId) -> Option
MathItalicsCorrectionInfo::correction_for_ppem(GlyphId, u16) -> Option
MathTopAccentAttachment::attachment(GlyphId) -> Option
MathTopAccentAttachment::attachment_for_ppem(GlyphId, u16) -> Option

// kerning
enum MathKernSide { TopRight, TopLeft, BottomRight, BottomLeft }
MathKernInfo::kern(GlyphId, MathKernSide) -> Option
MathKern::kerning(correction_height: i32) -> Option
MathKern::entries_for_ppem(u16) -> impl Iterator
struct MathKernEntry { max_height: Option, kern: MathValue }

// stretching
enum StretchAxis { Horizontal, Vertical }
MathVariants::min_connector_overlap() -> UfWord
MathVariants::glyph_construction(GlyphId, StretchAxis) -> Option
MathGlyphConstruction::math_glyph_variant_records() -> &[MathGlyphVariantRecord]
MathGlyphConstruction::glyph_assembly() -> Option>
GlyphAssembly::part_records() -> &[GlyphPartRecord]
GlyphAssembly::italics_correction_for_ppem(u16) -> MathValue
```

## Entry points

| function | maps to | notes |
| --- | --- | --- |
| `hb_ot_math_has_data` | `font.math().is_ok()` && non-zero version | HB is `version.to_int()` |
| `hb_ot_math_get_constant` | `MathConstants::constant_for_ppem` | apply the Cambria swap first |
| `hb_ot_math_get_glyph_italics_correction` | `MathItalicsCorrectionInfo::correction_for_ppem` | `None` → 0 |
| `hb_ot_math_get_glyph_top_accent_attachment` | `MathTopAccentAttachment::attachment_for_ppem` | **`None` → `h_advance / 2`**, not 0 |
| `hb_ot_math_is_glyph_extended_shape` | `MathGlyphInfo::is_extended_shape` | face-level, no scaling |
| `hb_ot_math_get_glyph_kerning` | fold over `MathKern::entries_for_ppem` | see band selection below |
| `hb_ot_math_get_glyph_kernings` | `MathKern::entries_for_ppem` | `max_height: None` → `INT32_MAX` |
| `hb_ot_math_get_glyph_variants` | `MathGlyphConstruction::math_glyph_variant_records` | scale advance by direction |
| `hb_ot_math_get_min_connector_overlap` | `MathVariants::min_connector_overlap` | scale by direction |
| `hb_ot_math_get_glyph_assembly` | `GlyphAssembly::part_records` + `italics_correction_for_ppem` | scale part lengths |
| `hb_ot_math_constant_t` | `MathConstant` | numbering already matches |

## Conversions the FFI owns

Everything below needs font-level context (`upem`, `x_scale`, `y_scale`,
`x_ppem`, `y_ppem`) that a table parser should not hold.

- **A value at a size.** For `MathValue { value, delta_px }` on the x axis:

```
em_scale_x(value) + delta_px * x_scale / x_ppem
```

and the y equivalent. This is what HarfBuzz's `get_x_value` /
`get_y_value` compute. Pick the axis per field: correction heights are y,
kern values are x, italics correction is x, top accent attachment is x.
- **Uncovered glyphs** are 0 everywhere except top accent attachment.
- **`ppem == 0`** means no device adjustment; `read-fonts` already returns
`delta_px == 0` there.

## Gotchas

1. **Top accent fallback.** `hb_ot_math_get_glyph_top_accent_attachment`
returns `font->get_glyph_h_advance(glyph) / 2` when the glyph is not
covered. This is the only entry point where `None` must not become 0.
2. **Band selection space.** `MathKern::kerning` compares stored design unit
heights and ignores their device adjustments. HarfBuzz compares in scaled
space with the adjustments applied, and flips the comparison when
`y_scale < 0`. For parity, fold over `entries_for_ppem` and do the
comparison in the same space the caller's `correction_height` is in:

```
i = first band whose max_height exceeds correction_height (sign-adjusted)
otherwise the last band
```
3. **Cambria Math.** Three builds of `cambria.ttc` store
`delimitedSubFormulaMinHeight` and `displayOperatorMinHeight` swapped, and
Microsoft reads them the other way round. When
`Math::has_swapped_min_heights()` is true, swap those two constants before
reading them. See https://github.com/harfbuzz/harfbuzz/issues/4653.
4. **Direction to axis.** `HB_DIRECTION_IS_VERTICAL(direction)` selects
`StretchAxis::Vertical`. Note this is the direction the *glyph* stretches,
not the text direction.
5. **Pagination.** `hb_ot_math_get_glyph_variants`,
`_get_glyph_assembly` and `_get_glyph_kernings` take `start_offset` and an
in/out count, and must return the total available even when the caller
passes a null buffer.
6. **`VariationIndex` is inert**, per the note above. If a font carries one
in `MATH`, `delta_px` is 0 and that is correct.

## Suggested order

1. Face and font plumbing: get a `Math` from whatever handle the FFI uses,
plus `upem`, scales and ppem.
2. The three that need no scaling or fallback: `has_data`,
`is_glyph_extended_shape`, `get_min_connector_overlap`.
3. `get_constant`, including the Cambria swap.
4. `get_glyph_italics_correction`, then `get_glyph_top_accent_attachment`
with its fallback.
5. `get_glyph_kernings`, then `get_glyph_kerning` as a fold over it.
6. `get_glyph_variants` and `get_glyph_assembly`, including pagination.

## Testing

A differential harness against HarfBuzz is the highest value test here: for
each font, query every constant at several ppem, and every glyph for italics
correction, top accent, extended shape, all four kern corners at a spread of
correction heights, and both stretch axes for variants and assembly.

Worth covering: Latin Modern Math, STIX Two Math, XITS Math, Asana Math,
Fira Math, TeX Gyre Pagella Math, and Cambria Math specifically for the
min-height swap. A font with hinting `Device` tables in `MATH` is needed to
exercise the ppem path; if none of the above has one, synthesise it.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the existing hb-harfrust.cc bridge and the face/font handle plumbing needed to expose Math data, then follow the listed hb_ot_math_* entry points in the suggested order. Use the differential harness against HarfBuzz across the named math fonts, ppem values, glyphs, kern corners, variants, and assemblies; done means matching HarfBuzz results, including Cambria swapping and pagination.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, rust
Domain
api, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.