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}