Skip to main content

rustyfi_backend/
font.rs

1use crate::context::Script;
2use crate::length::Length;
3
4/// An abstract handle to a loaded font face: the base-14 Helvetica family
5/// (regular/bold/oblique), or a key handed out by a real font registry.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct FontKey(pub u16);
8
9/// OpenType MATH `MathConstants` table, each field stored as a RATIO of
10/// the font size (design-units ÷ `units_per_em`, or percent ÷ 100 for the
11/// two scale-downs) so lang-side callers just multiply by
12/// `ctx.font_size`/the current script size. Mirrors upstream
13/// `FontFormat.math_constants` (fontFormat.ml:2292-2323).
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct MathConstants {
16    pub axis_height: f64,
17    pub superscript_bottom_min: f64,
18    pub superscript_shift_up: f64,
19    /// `superscript_shift_up_cramped` — OpenType `SuperscriptShiftUpCramped`,
20    /// the lowered shift-up used when the enclosing sub-formula is "cramped"
21    /// (TeXbook Appendix G rule 18a).
22    pub superscript_shift_up_cramped: f64,
23    pub superscript_baseline_drop_max: f64,
24    pub subscript_top_max: f64,
25    pub subscript_shift_down: f64,
26    pub subscript_baseline_drop_min: f64,
27    /// `script_percent_scale_down / 100`.
28    pub script_scale_down: f64,
29    /// `script_script_percent_scale_down / 100`.
30    pub script_script_scale_down: f64,
31    pub space_after_script: f64,
32    pub sub_superscript_gap_min: f64,
33    pub fraction_rule_thickness: f64,
34    /// `fraction_numerator_display_style_shift_up`.
35    pub fraction_numer_shift_up: f64,
36    /// `fraction_num_display_style_gap_min`.
37    pub fraction_numer_gap_min: f64,
38    /// `fraction_denominator_display_style_shift_down`.
39    pub fraction_denom_shift_down: f64,
40    /// `fraction_denom_display_style_gap_min`.
41    pub fraction_denom_gap_min: f64,
42    pub radical_extra_ascender: f64,
43    pub radical_rule_thickness: f64,
44    /// `radical_display_style_vertical_gap`.
45    pub radical_vertical_gap: f64,
46    pub upper_limit_gap_min: f64,
47    pub upper_limit_baseline_rise_min: f64,
48    pub lower_limit_gap_min: f64,
49    pub lower_limit_baseline_drop_min: f64,
50}
51
52/// Which corner of a math-kerned glyph a `MathKernInfo` entry describes
53/// (OpenType MATH `MathKernInfoRecord`: top-right/top-left/bottom-right/
54/// bottom-left).
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum MathCorner {
57    TopRight,
58    TopLeft,
59    BottomRight,
60    BottomLeft,
61}
62
63/// Classify one character into the four-way script bucket a font scheme is
64/// indexed by. Deviation from the originally-proposed standalone
65/// `CharScript` enum: this port already has `context::Script`
66/// (`set-dominant-*-script`) with the exact same four
67/// constructors in the exact same order (`HanIdeographic=0, Kana=1,
68/// Latin=2, OtherScript=3`) — introducing a second, structurally-identical
69/// enum just to keep "per-char classifier" and "context-stored dominant
70/// script" conceptually separate would add a conversion at every call site
71/// for no behavioral gain, so this reuses `Script` directly as the
72/// per-char classification result too.
73///
74/// Upstream classifies via `Scripts.txt` + East-Asian-width
75/// (`scriptDataMap.ml:74-167`, itself labelled "temporary" by its own
76/// comment); this range classifier has no unidata file to ship and matches
77/// upstream's *observable* output for the stdja corpus, not the full
78/// Unicode script property. CJK punctuation/fullwidth forms classify as
79/// `HanIdeographic` (not `OtherScript`) so `「」。、` render in the CJK
80/// (mincho) face, matching upstream's Kana/Han default-font assignment.
81pub fn char_script(c: char) -> Script {
82    match c as u32 {
83        // Hiragana, Katakana (+ phonetic extensions).
84        0x3040..=0x30FF | 0x31F0..=0x31FF => Script::Kana,
85        // CJK Unified Ideographs (+ Ext-A), CJK symbols/punctuation,
86        // compatibility ideographs, halfwidth/fullwidth forms.
87        0x3400..=0x4DBF
88        | 0x4E00..=0x9FFF
89        | 0xF900..=0xFAFF
90        | 0x3000..=0x303F
91        | 0xFF00..=0xFFEF
92        | 0x20000..=0x2FA1F => Script::HanIdeographic,
93        // Basic Latin .. Latin Extended-B.
94        0x0000..=0x024F => Script::Latin,
95        _ => Script::OtherScript,
96    }
97}
98
99/// The seam between typesetting and font data: the line breaker and the box
100/// builders only measure through this trait. Implemented in `rustyfi-pdf`,
101/// over the hardcoded base-14 AFM tables (`Base14Metrics`) and the
102/// ttf-parser-backed registry (`TtfFontStore`).
103pub trait FontMetrics {
104    /// Horizontal advance of `c` at `size`, or `None` if the font has no
105    /// glyph for it.
106    fn advance(&self, font: FontKey, c: char, size: Length) -> Option<Length>;
107
108    /// Height above the baseline at `size`.
109    fn ascender(&self, font: FontKey, size: Length) -> Length;
110
111    /// Depth below the baseline at `size` (a positive value).
112    fn descender(&self, font: FontKey, size: Length) -> Length;
113
114    /// One glyph's vertical extent from its ACTUAL bounding box —
115    /// `(height above baseline = ymax, depth below baseline = -ymin)`, both in
116    /// `size` units. `None` when the provider has no per-glyph bbox (base-14 /
117    /// test stubs), in which case `run_vextent` falls back to
118    /// `ascender`/`descender`. This is how SATySFi measures glyphs
119    /// (`fontFormat.ml`'s `get_glyph_metrics`: `hgt = ymax`, `dpt = ymin`).
120    fn glyph_vextent(&self, _font: FontKey, _c: char, _size: Length) -> Option<(Length, Length)> {
121        None
122    }
123
124    /// A text run's `(height, depth)` the way SATySFi's `get_metrics_of_word`
125    /// (`fontInfo.ml:192`) computes it: the MAX glyph `ymax` and MAX `-ymin`
126    /// over the run's actual glyph bounding boxes — NOT the font-level
127    /// ascender/descender. Starting the folds at zero clamps a run with no
128    /// descenders (Japanese, digits, TOC leader dots) to depth 0, matching
129    /// SATySFi's much tighter inter-line advance for such content. Falls back
130    /// to `ascender`/`descender` when no glyph exposes a bbox.
131    fn run_vextent(&self, font: FontKey, text: &str, size: Length) -> (Length, Length) {
132        let mut hgt = Length::ZERO;
133        let mut dpt = Length::ZERO;
134        let mut any = false;
135        for c in text.chars() {
136            if let Some((h, d)) = self.glyph_vextent(font, c, size) {
137                hgt = hgt.max(h);
138                dpt = dpt.max(d);
139                any = true;
140            }
141        }
142        if any {
143            (hgt, dpt)
144        } else {
145            (self.ascender(font, size), self.descender(font, size))
146        }
147    }
148
149    fn text_width(&self, font: FontKey, text: &str, size: Length) -> Option<Length> {
150        let mut w = Length::ZERO;
151        for c in text.chars() {
152            w += self.advance(font, c, size)?;
153        }
154        Some(w)
155    }
156
157    /// The font's OpenType MATH `MathConstants` table, or `None` when the
158    /// font has no MATH table (every base-14/non-math provider). Lang-side
159    /// math layout (`MathC` resolver) falls back to the pre-MATH-table
160    /// fixed constants whenever this is `None`, so a provider that never
161    /// overrides it (like `Base14Metrics`) keeps today's fixtures
162    /// byte-identical.
163    fn math_constants(&self, _font: FontKey) -> Option<MathConstants> {
164        None
165    }
166
167    /// The italic correction of `c` at `size` (OpenType MATH
168    /// `MathItalicsCorrectionInfo`), or `None` when the font has no MATH
169    /// table or no entry for this glyph.
170    fn italic_correction(&self, _font: FontKey, _c: char, _size: Length) -> Option<Length> {
171        None
172    }
173
174    /// The OpenType MATH per-glyph corner kern of `c` at `size`, sampled at
175    /// correction height `corr` (`MathKernInfo`/`MathKern`), or `None` when
176    /// the font has no MATH table or no kern data for this glyph/corner.
177    fn math_kern(
178        &self,
179        _font: FontKey,
180        _c: char,
181        _size: Length,
182        _corner: MathCorner,
183        _corr: Length,
184    ) -> Option<Length> {
185        None
186    }
187
188    /// A vertically-grown MATH variant of `c` at `size`, selected per
189    /// `policy` (OpenType MATH `MathVariants`, — big operators/stretchy
190    /// delimiters). `None` when the font has no MATH table, no vertical
191    /// construction for `c`, or the construction has no prepared variant
192    /// records (assembly-only); every caller must treat
193    /// `None` as "use the base glyph unchanged" (`push_char_glyph`), so a
194    /// provider that never overrides this (every base-14 provider) leaves
195    /// every fixture that predates vertical MATH-variant support
196    /// byte-identical.
197    fn math_vertical_variant(
198        &self,
199        _font: FontKey,
200        _c: char,
201        _size: Length,
202        _policy: VertVariantPolicy,
203    ) -> Option<MathVariantGlyph> {
204        None
205    }
206
207    /// The `ssty` (Math Script Style) GSUB variant of `c` at `size` — upstream
208    /// `FontFormat.get_math_script_variant` (`fontFormat.ml:2216`), applied by
209    /// `fontInfo.ml:379-383` to every math glyph below base level. These are
210    /// purpose-drawn exponent/index forms with their OWN advances, not the base
211    /// glyph scaled (Latin Modern Math's `two.st` advances 569/1000 em against
212    /// plain `two`'s 500), so this is a width contract as much as a shape one.
213    ///
214    /// `None` when the font has no GSUB, no `ssty`, or no substitution covering
215    /// this glyph; a caller must read that as "use the base glyph unchanged",
216    /// which is what leaves every non-overriding provider unaffected.
217    fn math_script_variant(
218        &self,
219        _font: FontKey,
220        _c: char,
221        _size: Length,
222    ) -> Option<MathVariantGlyph> {
223        None
224    }
225
226    /// Build a vertically-stretched delimiter/big-op from the OpenType MATH
227    /// `GlyphAssembly` of `c` (the stretch-beyond-the-largest-discrete-variant
228    /// path). Returns the placed parts as `(gid, dy, advance)`, bottom-to-top,
229    /// where `dy` is the **y-up, box-local** vertical offset of the part's own
230    /// baseline (the bottom part sits at `dy = 0`, each subsequent part is
231    /// raised by the previous part's advance minus their connector overlap),
232    /// and `advance` is the part glyph's design-unit `full_advance` scaled to
233    /// `size` (the vertical extent it contributes). The parts stack with
234    /// overlaps `>= min_connector_overlap`, repeating `extender` parts as many
235    /// times as needed to reach `target`. `None` when the font has no MATH
236    /// table, no vertical construction for `c`, or that construction has no
237    /// `GlyphAssembly` — every caller must treat `None` as "fall back to the
238    /// largest discrete variant" (`push_delimiter_glyph`), so a provider that
239    /// never overrides this (every base-14 provider) is unaffected.
240    fn math_vertical_assembly(
241        &self,
242        _font: FontKey,
243        _c: char,
244        _size: Length,
245        _target: Length,
246    ) -> Option<Vec<(u16, Length, Length)>> {
247        None
248    }
249
250    /// Resolve a registry abbrev (`"ipaexm"`, `"Junicode-b"`, ...) to its
251    /// `FontKey`. `None` means either "no such abbrev in this
252    /// provider's registry" or "this provider has no registry at all" (every
253    /// provider that predates the registry, `Base14Metrics`) — the caller
254    /// then falls back to the milestone-1 3-face name heuristic
255    /// (`resolve_font_abbrev` free fn, rustyfi-lang), keeping every existing
256    /// `set-font` call byte-identical.
257    fn resolve_font_abbrev(&self, _abbrev: &str) -> Option<FontKey> {
258        None
259    }
260
261    /// The inverse of [`FontMetrics::resolve_font_abbrev`]: which registry
262    /// abbrev minted `key`. This exists for 0.0.6's `get-font`, whose result
263    /// type `tFONT = string * float * float` leads with an ABBREV — upstream
264    /// keeps the abbrev in `context_main.font_scheme` and only resolves it to
265    /// a file at render time, whereas this port resolves eagerly at `set-font`
266    /// and stores a `FontKey`, so the name has to be recovered from whoever
267    /// minted the key.
268    ///
269    /// It is a genuine inverse where it answers at all: `build_store`
270    /// allocates one `FontKey` per CONFIGURED abbrev even when two abbrevs
271    /// name the same font file (they share a `files` index, not a key), so no
272    /// key is reachable from two abbrevs. `None` means the key was never named
273    /// by the registry — the three seeded default faces (regular/bold/oblique,
274    /// `FontKey(0..3)`), anything a bare `TtfFontStore::load` produced, and
275    /// every `Base14Metrics` key. `get-font` reports those as `""` rather than
276    /// inventing a name; nothing in the corpus reads the slot (every caller,
277    /// and upstream's own `convertText.ml:78`, writes `let (_, ratio, _) =`),
278    /// so the ratio and rising — which are exact either way — are what
279    /// actually matters.
280    fn font_abbrev(&self, _key: FontKey) -> Option<String> {
281        None
282    }
283
284    /// The configured default `(font, ratio, rising)` for `script`, from
285    /// `default-font.satysfi-hash`'s `scripts` block. `None` means "no
286    /// scheme configured for this script" — the caller then falls back to
287    /// `(ctx.font, 1.0, 0.0)`, i.e. today's single-font behavior.
288    fn default_script_font(&self, _script: Script) -> Option<(FontKey, f64, f64)> {
289        None
290    }
291
292    /// The configured default math font, from `default-font.satysfi-hash`'s
293    /// optional `"math"` abbrev. `None` means "no math default
294    /// configured" — the caller (`get-initial-context`) then leaves
295    /// `Context::math_font` at its `Context::initial` seed (`FontKey(0)`, the
296    /// regular text face), i.e. today's behavior. Every provider that
297    /// predates this math-default support (`Base14Metrics`, a bare
298    /// `TtfFontStore::load`, a registry with no `"math"` entry) returns
299    /// `None` here, so this is purely additive.
300    fn default_math_font(&self) -> Option<FontKey> {
301        None
302    }
303}
304
305/// How to pick a vertically-grown MATH variant (`MathVariants`).
306#[derive(Clone, Copy, Debug, PartialEq)]
307pub enum VertVariantPolicy {
308    /// v0.0.6 big-operator policy (`fontInfo.ml:386-401`): the 2nd record if
309    /// present, else the 1st ("somewhat ad-hoc; uses the second smallest" —
310    /// upstream's own comment). Upstream's `is_in_display && is_big` guard
311    /// reduces to just `is_big` here since `convert_math_char` hardcodes
312    /// `is_in_display = true`; the port tracks no display/inline distinction
313    /// and needs none — see the caller (`push_big_char_glyph`) for detail.
314    BigOp,
315    /// Stretchy-delimiter policy: the smallest record whose
316    /// `advance_measurement` covers `Length`, else the largest record.
317    AtLeast(Length),
318}
319
320/// One selected vertical variant with real per-glyph ink metrics at size
321/// (`fontFormat.ml:2257` `get_math_glyph_metrics`: `hgt = max(0, ymax)`,
322/// `dpt = -min(0, ymin)`, both from the variant glyph's own outline bbox).
323#[derive(Clone, Copy, Debug, PartialEq)]
324pub struct MathVariantGlyph {
325    /// Raw font glyph id — NOT necessarily cmap-reachable (variant glyphs
326    /// like `summation.v1` typically have no cmap entry at all); the CID
327    /// writer emits this directly as an Identity-H content byte pair rather
328    /// than re-deriving it from a character.
329    pub gid: u16,
330    pub advance: Length,
331    pub height: Length,
332    pub depth: Length,
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn char_script_classifies_the_stdja_corpus() {
341        assert_eq!(char_script('あ'), Script::Kana); // Hiragana
342        assert_eq!(char_script('ア'), Script::Kana); // Katakana
343        assert_eq!(char_script('漢'), Script::HanIdeographic);
344        assert_eq!(char_script('A'), Script::Latin);
345        assert_eq!(char_script('z'), Script::Latin);
346        assert_eq!(char_script('é'), Script::Latin); // Latin-1 Supplement
347        assert_eq!(char_script('→'), Script::OtherScript); // U+2192 arrow
348        assert_eq!(char_script('。'), Script::HanIdeographic); // CJK punctuation
349        assert_eq!(char_script('「'), Script::HanIdeographic);
350        assert_eq!(char_script('、'), Script::HanIdeographic);
351    }
352}