Skip to main content

typst_library/text/font/
metrics.rs

1use std::sync::OnceLock;
2
3use crate::foundations::Cast;
4use crate::layout::{Em, Frame};
5use crate::text::{DEFAULT_SUBSCRIPT_METRICS, DEFAULT_SUPERSCRIPT_METRICS, FontInstance};
6
7/// Metrics of a font.
8#[derive(Debug, Clone)]
9pub struct FontMetrics {
10    /// How many font units represent one em unit.
11    pub units_per_em: f64,
12    /// The distance from the baseline to the typographic ascender.
13    pub ascender: Em,
14    /// The approximate height of uppercase letters.
15    pub cap_height: Em,
16    /// The approximate height of non-ascending lowercase letters.
17    pub x_height: Em,
18    /// The distance from the baseline to the typographic descender.
19    pub descender: Em,
20    /// Recommended metrics for a strikethrough line.
21    pub strikethrough: LineMetrics,
22    /// Recommended metrics for an underline.
23    pub underline: LineMetrics,
24    /// Recommended metrics for an overline.
25    pub overline: LineMetrics,
26    /// Metrics for subscripts, if provided by the font.
27    pub subscript: Option<ScriptMetrics>,
28    /// Metrics for superscripts, if provided by the font.
29    pub superscript: Option<ScriptMetrics>,
30    /// Metrics for math layout.
31    pub math: OnceLock<Box<MathConstants>>,
32}
33
34impl FontMetrics {
35    /// Extract the font's metrics.
36    pub fn from_ttf(ttf: &ttf_parser::Face) -> Self {
37        let units_per_em = f64::from(ttf.units_per_em());
38        let to_em = |units| Em::from_units(units, units_per_em);
39
40        let ascender = to_em(ttf.typographic_ascender().unwrap_or(ttf.ascender()));
41        let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em);
42        let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em);
43        let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender()));
44
45        let strikeout = ttf.strikeout_metrics();
46        let underline = ttf.underline_metrics();
47
48        let strikethrough = LineMetrics {
49            position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)),
50            thickness: strikeout
51                .or(underline)
52                .map_or(Em::new(0.06), |s| to_em(s.thickness)),
53        };
54
55        let underline = LineMetrics {
56            position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)),
57            thickness: underline
58                .or(strikeout)
59                .map_or(Em::new(0.06), |s| to_em(s.thickness)),
60        };
61
62        let overline = LineMetrics {
63            position: cap_height + Em::new(0.1),
64            thickness: underline.thickness,
65        };
66
67        let subscript = ttf.subscript_metrics().map(|metrics| ScriptMetrics {
68            width: to_em(metrics.x_size),
69            height: to_em(metrics.y_size),
70            horizontal_offset: to_em(metrics.x_offset),
71            vertical_offset: -to_em(metrics.y_offset),
72        });
73
74        let superscript = ttf.superscript_metrics().map(|metrics| ScriptMetrics {
75            width: to_em(metrics.x_size),
76            height: to_em(metrics.y_size),
77            horizontal_offset: to_em(metrics.x_offset),
78            vertical_offset: to_em(metrics.y_offset),
79        });
80
81        Self {
82            units_per_em,
83            ascender,
84            cap_height,
85            x_height,
86            descender,
87            strikethrough,
88            underline,
89            overline,
90            superscript,
91            subscript,
92            math: OnceLock::new(),
93        }
94    }
95
96    /// Look up a vertical metric.
97    pub fn vertical(&self, metric: VerticalFontMetric) -> Em {
98        match metric {
99            VerticalFontMetric::Ascender => self.ascender,
100            VerticalFontMetric::CapHeight => self.cap_height,
101            VerticalFontMetric::XHeight => self.x_height,
102            VerticalFontMetric::Baseline => Em::zero(),
103            VerticalFontMetric::Descender => self.descender,
104        }
105    }
106}
107
108/// Metrics for a decorative line.
109#[derive(Debug, Copy, Clone)]
110pub struct LineMetrics {
111    /// The vertical offset of the line from the baseline. Positive goes
112    /// upwards, negative downwards.
113    pub position: Em,
114    /// The thickness of the line.
115    pub thickness: Em,
116}
117
118/// Metrics for subscripts or superscripts.
119#[derive(Debug, Copy, Clone)]
120pub struct ScriptMetrics {
121    /// The width of those scripts, relative to the outer font size.
122    pub width: Em,
123    /// The height of those scripts, relative to the outer font size.
124    pub height: Em,
125    /// The horizontal (to the right) offset of those scripts, relative to the
126    /// outer font size.
127    ///
128    /// This is used for italic correction.
129    pub horizontal_offset: Em,
130    /// The vertical (to the top) offset of those scripts, relative to the outer font size.
131    ///
132    /// For superscripts, this is positive. For subscripts, this is negative.
133    pub vertical_offset: Em,
134}
135
136/// Constants from the OpenType MATH constants table used in Typst.
137///
138/// Ones not currently used are omitted.
139#[derive(Debug, Copy, Clone)]
140pub struct MathConstants {
141    // This is not from the OpenType MATH spec.
142    pub space_width: Em,
143    // These are both i16 instead of f64 as they need to go on the StyleChain.
144    pub script_percent_scale_down: i16,
145    pub script_script_percent_scale_down: i16,
146    pub display_operator_min_height: Em,
147    pub axis_height: Em,
148    pub accent_base_height: Em,
149    pub flattened_accent_base_height: Em,
150    pub subscript_shift_down: Em,
151    pub subscript_top_max: Em,
152    pub subscript_baseline_drop_min: Em,
153    pub superscript_shift_up: Em,
154    pub superscript_shift_up_cramped: Em,
155    pub superscript_bottom_min: Em,
156    pub superscript_baseline_drop_max: Em,
157    pub sub_superscript_gap_min: Em,
158    pub superscript_bottom_max_with_subscript: Em,
159    pub space_after_script: Em,
160    pub upper_limit_gap_min: Em,
161    pub upper_limit_baseline_rise_min: Em,
162    pub lower_limit_gap_min: Em,
163    pub lower_limit_baseline_drop_min: Em,
164    pub stack_top_shift_up: Em,
165    pub stack_top_display_style_shift_up: Em,
166    pub stack_bottom_shift_down: Em,
167    pub stack_bottom_display_style_shift_down: Em,
168    pub stack_gap_min: Em,
169    pub stack_display_style_gap_min: Em,
170    pub fraction_numerator_shift_up: Em,
171    pub fraction_numerator_display_style_shift_up: Em,
172    pub fraction_denominator_shift_down: Em,
173    pub fraction_denominator_display_style_shift_down: Em,
174    pub fraction_numerator_gap_min: Em,
175    pub fraction_num_display_style_gap_min: Em,
176    pub fraction_rule_thickness: Em,
177    pub fraction_denominator_gap_min: Em,
178    pub fraction_denom_display_style_gap_min: Em,
179    pub skewed_fraction_vertical_gap: Em,
180    pub skewed_fraction_horizontal_gap: Em,
181    pub overbar_vertical_gap: Em,
182    pub overbar_rule_thickness: Em,
183    pub overbar_extra_ascender: Em,
184    pub underbar_vertical_gap: Em,
185    pub underbar_rule_thickness: Em,
186    pub underbar_extra_descender: Em,
187    pub radical_vertical_gap: Em,
188    pub radical_display_style_vertical_gap: Em,
189    pub radical_rule_thickness: Em,
190    pub radical_extra_ascender: Em,
191    pub radical_kern_before_degree: Em,
192    pub radical_kern_after_degree: Em,
193    pub radical_degree_bottom_raise_percent: f64,
194}
195
196impl MathConstants {
197    pub(super) fn new(font: &FontInstance) -> Box<Self> {
198        let ttf = font.ttf();
199
200        let space_width = ttf
201            .glyph_index(' ')
202            .and_then(|id| ttf.glyph_hor_advance(id).map(|units| font.to_em(units)))
203            .unwrap_or(typst_library::math::THICK);
204
205        ttf.tables()
206            .math
207            .and_then(|math| math.constants)
208            .map(|constants| Self::from_constants(font, &constants, space_width))
209            .unwrap_or_else(|| Self::fallback(font, space_width))
210    }
211
212    fn from_constants(
213        font: &FontInstance,
214        constants: &ttf_parser::math::Constants,
215        space_width: Em,
216    ) -> Box<Self> {
217        let is_cambria =
218            || font.post_script_name().is_some_and(|name| name == "CambriaMath");
219
220        Box::new(Self {
221            space_width,
222            script_percent_scale_down: constants.script_percent_scale_down(),
223            script_script_percent_scale_down: constants
224                .script_script_percent_scale_down(),
225            display_operator_min_height: font.to_em(if is_cambria() {
226                constants.delimited_sub_formula_min_height()
227            } else {
228                constants.display_operator_min_height()
229            }),
230            axis_height: font.to_em(constants.axis_height().value),
231            accent_base_height: font.to_em(constants.accent_base_height().value),
232            flattened_accent_base_height: font
233                .to_em(constants.flattened_accent_base_height().value),
234            subscript_shift_down: font.to_em(constants.subscript_shift_down().value),
235            subscript_top_max: font.to_em(constants.subscript_top_max().value),
236            subscript_baseline_drop_min: font
237                .to_em(constants.subscript_baseline_drop_min().value),
238            superscript_shift_up: font.to_em(constants.superscript_shift_up().value),
239            superscript_shift_up_cramped: font
240                .to_em(constants.superscript_shift_up_cramped().value),
241            superscript_bottom_min: font.to_em(constants.superscript_bottom_min().value),
242            superscript_baseline_drop_max: font
243                .to_em(constants.superscript_baseline_drop_max().value),
244            sub_superscript_gap_min: font
245                .to_em(constants.sub_superscript_gap_min().value),
246            superscript_bottom_max_with_subscript: font
247                .to_em(constants.superscript_bottom_max_with_subscript().value),
248            space_after_script: font.to_em(constants.space_after_script().value),
249            upper_limit_gap_min: font.to_em(constants.upper_limit_gap_min().value),
250            upper_limit_baseline_rise_min: font
251                .to_em(constants.upper_limit_baseline_rise_min().value),
252            lower_limit_gap_min: font.to_em(constants.lower_limit_gap_min().value),
253            lower_limit_baseline_drop_min: font
254                .to_em(constants.lower_limit_baseline_drop_min().value),
255            stack_top_shift_up: font.to_em(constants.stack_top_shift_up().value),
256            stack_top_display_style_shift_up: font
257                .to_em(constants.stack_top_display_style_shift_up().value),
258            stack_bottom_shift_down: font
259                .to_em(constants.stack_bottom_shift_down().value),
260            stack_bottom_display_style_shift_down: font
261                .to_em(constants.stack_bottom_display_style_shift_down().value),
262            stack_gap_min: font.to_em(constants.stack_gap_min().value),
263            stack_display_style_gap_min: font
264                .to_em(constants.stack_display_style_gap_min().value),
265            fraction_numerator_shift_up: font
266                .to_em(constants.fraction_numerator_shift_up().value),
267            fraction_numerator_display_style_shift_up: font
268                .to_em(constants.fraction_numerator_display_style_shift_up().value),
269            fraction_denominator_shift_down: font
270                .to_em(constants.fraction_denominator_shift_down().value),
271            fraction_denominator_display_style_shift_down: font
272                .to_em(constants.fraction_denominator_display_style_shift_down().value),
273            fraction_numerator_gap_min: font
274                .to_em(constants.fraction_numerator_gap_min().value),
275            fraction_num_display_style_gap_min: font
276                .to_em(constants.fraction_num_display_style_gap_min().value),
277            fraction_rule_thickness: font
278                .to_em(constants.fraction_rule_thickness().value),
279            fraction_denominator_gap_min: font
280                .to_em(constants.fraction_denominator_gap_min().value),
281            fraction_denom_display_style_gap_min: font
282                .to_em(constants.fraction_denom_display_style_gap_min().value),
283            skewed_fraction_vertical_gap: font
284                .to_em(constants.skewed_fraction_vertical_gap().value),
285            skewed_fraction_horizontal_gap: font
286                .to_em(constants.skewed_fraction_horizontal_gap().value),
287            overbar_vertical_gap: font.to_em(constants.overbar_vertical_gap().value),
288            overbar_rule_thickness: font.to_em(constants.overbar_rule_thickness().value),
289            overbar_extra_ascender: font.to_em(constants.overbar_extra_ascender().value),
290            underbar_vertical_gap: font.to_em(constants.underbar_vertical_gap().value),
291            underbar_rule_thickness: font
292                .to_em(constants.underbar_rule_thickness().value),
293            underbar_extra_descender: font
294                .to_em(constants.underbar_extra_descender().value),
295            radical_vertical_gap: font.to_em(constants.radical_vertical_gap().value),
296            radical_display_style_vertical_gap: font
297                .to_em(constants.radical_display_style_vertical_gap().value),
298            radical_rule_thickness: font.to_em(constants.radical_rule_thickness().value),
299            radical_extra_ascender: font.to_em(constants.radical_extra_ascender().value),
300            radical_kern_before_degree: font
301                .to_em(constants.radical_kern_before_degree().value),
302            radical_kern_after_degree: font
303                .to_em(constants.radical_kern_after_degree().value),
304            radical_degree_bottom_raise_percent: constants
305                .radical_degree_bottom_raise_percent()
306                as f64
307                / 100.0,
308        })
309    }
310
311    /// Most of these fallback constants are from the MathML Core
312    /// spec, with the exceptions of
313    /// - `flattened_accent_base_height` from Building Math Fonts
314    /// - `overbar_rule_thickness` and `underbar_rule_thickness`
315    ///   from our best guess
316    /// - `skewed_fraction_vertical_gap` and `skewed_fraction_horizontal_gap`
317    ///   from our best guess
318    /// - `script_percent_scale_down` and
319    ///   `script_script_percent_scale_down` from Building Math
320    ///   Fonts as the defaults given in MathML Core have more
321    ///   precision than i16.
322    ///
323    /// <https://www.w3.org/TR/mathml-core/#layout-constants-mathconstants>
324    /// <https://github.com/notofonts/math/blob/main/documentation/building-math-fonts/index.md>
325    fn fallback(font: &FontInstance, space_width: Em) -> Box<Self> {
326        let metrics = font.metrics();
327        Box::new(MathConstants {
328            space_width,
329            script_percent_scale_down: 70,
330            script_script_percent_scale_down: 50,
331            display_operator_min_height: Em::zero(),
332            axis_height: metrics.x_height / 2.0,
333            accent_base_height: metrics.x_height,
334            flattened_accent_base_height: metrics.cap_height,
335            subscript_shift_down: metrics
336                .subscript
337                .map(|metrics| metrics.vertical_offset)
338                .unwrap_or(DEFAULT_SUBSCRIPT_METRICS.vertical_offset),
339            subscript_top_max: 0.8 * metrics.x_height,
340            subscript_baseline_drop_min: Em::zero(),
341            superscript_shift_up: metrics
342                .superscript
343                .map(|metrics| metrics.vertical_offset)
344                .unwrap_or(DEFAULT_SUPERSCRIPT_METRICS.vertical_offset),
345            superscript_shift_up_cramped: Em::zero(),
346            superscript_bottom_min: 0.25 * metrics.x_height,
347            superscript_baseline_drop_max: Em::zero(),
348            sub_superscript_gap_min: 4.0 * metrics.underline.thickness,
349            superscript_bottom_max_with_subscript: 0.8 * metrics.x_height,
350            space_after_script: Em::new(1.0 / 24.0),
351            upper_limit_gap_min: Em::zero(),
352            upper_limit_baseline_rise_min: Em::zero(),
353            lower_limit_gap_min: Em::zero(),
354            lower_limit_baseline_drop_min: Em::zero(),
355            stack_top_shift_up: Em::zero(),
356            stack_top_display_style_shift_up: Em::zero(),
357            stack_bottom_shift_down: Em::zero(),
358            stack_bottom_display_style_shift_down: Em::zero(),
359            stack_gap_min: 3.0 * metrics.underline.thickness,
360            stack_display_style_gap_min: 7.0 * metrics.underline.thickness,
361            fraction_numerator_shift_up: Em::zero(),
362            fraction_numerator_display_style_shift_up: Em::zero(),
363            fraction_denominator_shift_down: Em::zero(),
364            fraction_denominator_display_style_shift_down: Em::zero(),
365            fraction_numerator_gap_min: metrics.underline.thickness,
366            fraction_num_display_style_gap_min: 3.0 * metrics.underline.thickness,
367            fraction_rule_thickness: metrics.underline.thickness,
368            fraction_denominator_gap_min: metrics.underline.thickness,
369            fraction_denom_display_style_gap_min: 3.0 * metrics.underline.thickness,
370            skewed_fraction_vertical_gap: Em::zero(),
371            skewed_fraction_horizontal_gap: Em::new(0.5),
372            overbar_vertical_gap: 3.0 * metrics.underline.thickness,
373            overbar_rule_thickness: metrics.underline.thickness,
374            overbar_extra_ascender: metrics.underline.thickness,
375            underbar_vertical_gap: 3.0 * metrics.underline.thickness,
376            underbar_rule_thickness: metrics.underline.thickness,
377            underbar_extra_descender: metrics.underline.thickness,
378            radical_vertical_gap: 1.25 * metrics.underline.thickness,
379            radical_display_style_vertical_gap: metrics.underline.thickness
380                + 0.25 * metrics.x_height,
381            radical_rule_thickness: metrics.underline.thickness,
382            radical_extra_ascender: metrics.underline.thickness,
383            radical_kern_before_degree: Em::new(5.0 / 18.0),
384            radical_kern_after_degree: Em::new(-10.0 / 18.0),
385            radical_degree_bottom_raise_percent: 0.6,
386        })
387    }
388}
389
390/// Identifies a vertical metric of a font.
391#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
392pub enum VerticalFontMetric {
393    /// The font's ascender, which typically exceeds the height of all glyphs.
394    Ascender,
395    /// The approximate height of uppercase letters.
396    CapHeight,
397    /// The approximate height of non-ascending lowercase letters.
398    XHeight,
399    /// The baseline on which the letters rest.
400    Baseline,
401    /// The font's ascender, which typically exceeds the depth of all glyphs.
402    Descender,
403}
404
405/// Defines how to resolve a `Bounds` text edge.
406#[derive(Debug, Copy, Clone)]
407pub enum TextEdgeBounds<'a> {
408    /// Set the bounds to zero.
409    Zero,
410    /// Use the bounding box of the given glyph for the bounds.
411    Glyph(u16),
412    /// Use the dimension of the given frame for the bounds.
413    Frame(&'a Frame),
414}