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#[derive(Debug, Clone)]
9pub struct FontMetrics {
10 pub units_per_em: f64,
12 pub ascender: Em,
14 pub cap_height: Em,
16 pub x_height: Em,
18 pub descender: Em,
20 pub strikethrough: LineMetrics,
22 pub underline: LineMetrics,
24 pub overline: LineMetrics,
26 pub subscript: Option<ScriptMetrics>,
28 pub superscript: Option<ScriptMetrics>,
30 pub math: OnceLock<Box<MathConstants>>,
32}
33
34impl FontMetrics {
35 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 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#[derive(Debug, Copy, Clone)]
110pub struct LineMetrics {
111 pub position: Em,
114 pub thickness: Em,
116}
117
118#[derive(Debug, Copy, Clone)]
120pub struct ScriptMetrics {
121 pub width: Em,
123 pub height: Em,
125 pub horizontal_offset: Em,
130 pub vertical_offset: Em,
134}
135
136#[derive(Debug, Copy, Clone)]
140pub struct MathConstants {
141 pub space_width: Em,
143 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 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#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
392pub enum VerticalFontMetric {
393 Ascender,
395 CapHeight,
397 XHeight,
399 Baseline,
401 Descender,
403}
404
405#[derive(Debug, Copy, Clone)]
407pub enum TextEdgeBounds<'a> {
408 Zero,
410 Glyph(u16),
412 Frame(&'a Frame),
414}