Skip to main content

mathtex_font/
lib.rs

1//! XeTeX font spec parsing, the host font loader trait, and OpenType metrics and math access.
2#![forbid(unsafe_code)]
3
4use std::collections::BTreeMap;
5use std::fmt;
6use std::sync::{Arc, OnceLock};
7
8pub use mathtex_ir::FontKey;
9use mathtex_ir::{GlyphId, GlyphOutline, Length, OutlineCommand};
10
11// SharedFace and the face accessors name these crates' types, so hosts must use these exact versions.
12pub use rustybuzz;
13pub use ttf_parser;
14
15/// Loads font faces for the engine, the only font trait a host implements.
16pub trait FontLoader {
17    /// Returns the face a spec names, with a key the host chooses to identify it in the IR.
18    fn load(&self, spec: &FontSpec) -> Result<FontData, FontError>;
19}
20
21impl<T> FontLoader for &T
22where
23    T: FontLoader + ?Sized,
24{
25    fn load(&self, spec: &FontSpec) -> Result<FontData, FontError> {
26        (**self).load(spec)
27    }
28}
29
30/// A parsed XeTeX font spec, the engine shapes with its name, script, language and features only.
31#[derive(Clone, Debug, PartialEq)]
32pub struct FontSpec {
33    spec: String,
34    name: String,
35    file: bool,
36    face_index: u32,
37    variants: Vec<String>,
38    size: Length,
39    script: Option<[u8; 4]>,
40    language: Option<String>,
41    features: Vec<ShapeFeature>,
42    options: Vec<(String, String)>,
43    vertical: bool,
44    unknown: Vec<String>,
45}
46
47impl FontSpec {
48    /// Parses a spec as XeTeX's `splitFontName` and `loadOTfont` read it, `size` is the loaded size.
49    #[must_use]
50    pub fn parse(spec: &str, size: Length) -> Self {
51        let (name, file, face_index, variant, features) = split_font_name(spec);
52        let mut parsed = Self {
53            spec: spec.to_string(),
54            name,
55            file,
56            face_index,
57            variants: variant
58                .split('/')
59                .filter(|part| !part.is_empty())
60                .map(str::to_string)
61                .collect(),
62            size,
63            script: None,
64            language: None,
65            features: Vec::new(),
66            options: Vec::new(),
67            vertical: false,
68            unknown: Vec::new(),
69        };
70        for option in features.split([':', ';', ',']) {
71            parsed.read_option(option.trim_start_matches([' ', '\t']));
72        }
73        parsed
74    }
75
76    fn read_option(&mut self, option: &str) {
77        if option.is_empty() {
78            return;
79        }
80        if let Some((key, value)) = option.split_once('=') {
81            match key {
82                "script" => return self.script = Some(ot_tag(value)),
83                "language" => return self.language = Some(value.to_string()),
84                "mapping" | "extend" | "slant" | "embolden" | "letterspace" | "color"
85                | "shaper" => return self.options.push((key.to_string(), value.to_string())),
86                _ => {}
87            }
88        }
89        if let Some(rest) = option.strip_prefix('+') {
90            // XeTeX's read_tag_with_param bumps a nonnegative `+tag=n` so alternate 0 selects the first one.
91            let (tag, param) = rest
92                .split_once('=')
93                .map_or((rest, 0i64), |(tag, value)| (tag, leading_int(value)));
94            let value = if param >= 0 { param + 1 } else { param };
95            self.features.push(ShapeFeature {
96                tag: ot_tag(tag),
97                value: value as u32,
98            });
99        } else if let Some(tag) = option.strip_prefix('-') {
100            self.features.push(ShapeFeature {
101                tag: ot_tag(tag),
102                value: 0,
103            });
104        } else if let Some((tag, value)) = option.split_once('=') {
105            match value.trim().parse::<u32>() {
106                Ok(value) => self.features.push(ShapeFeature {
107                    tag: ot_tag(tag),
108                    value,
109                }),
110                Err(_) => self.unknown.push(option.to_string()),
111            }
112        } else if option.trim_end() == "vertical" {
113            self.vertical = true;
114        } else {
115            self.unknown.push(option.to_string());
116        }
117    }
118
119    /// The spec exactly as TeX gave it.
120    #[must_use]
121    pub fn as_str(&self) -> &str {
122        &self.spec
123    }
124
125    /// File name inside `[...]` without the face index, or the bare font name.
126    #[must_use]
127    pub fn name(&self) -> &str {
128        &self.name
129    }
130
131    /// Whether the spec names a file with `[...]`, otherwise it is a XeTeX name lookup.
132    #[must_use]
133    pub fn is_file(&self) -> bool {
134        self.file
135    }
136
137    /// Face index of a `[file:N]` spec, 0 otherwise.
138    #[must_use]
139    pub fn face_index(&self) -> u32 {
140        self.face_index
141    }
142
143    /// Style and renderer suffixes after `/`, such as `B`, `I`, `OT` or `AAT`, which the engine ignores.
144    #[must_use]
145    pub fn variants(&self) -> &[String] {
146        &self.variants
147    }
148
149    /// Size the font is loaded at.
150    #[must_use]
151    pub fn size(&self) -> Length {
152        self.size
153    }
154
155    /// OpenType script tag from `script=`.
156    #[must_use]
157    pub fn script(&self) -> Option<[u8; 4]> {
158        self.script
159    }
160
161    /// Language from `language=`.
162    #[must_use]
163    pub fn language(&self) -> Option<&str> {
164        self.language.as_deref()
165    }
166
167    /// OpenType features from `+tag`, `-tag` and `tag=value`, in spec order.
168    #[must_use]
169    pub fn features(&self) -> &[ShapeFeature] {
170        &self.features
171    }
172
173    /// Value of an option the engine ignores, such as `mapping`, `color`, `embolden` or `letterspace`.
174    #[must_use]
175    pub fn option(&self, key: &str) -> Option<&str> {
176        self.options
177            .iter()
178            .find(|(name, _)| name == key)
179            .map(|(_, value)| value.as_str())
180    }
181
182    /// Whether the spec asks for `vertical` layout, which the engine ignores.
183    #[must_use]
184    pub fn vertical(&self) -> bool {
185        self.vertical
186    }
187
188    /// Options XeTeX would warn about as unknown.
189    #[must_use]
190    pub fn unknown_options(&self) -> &[String] {
191        &self.unknown
192    }
193
194    /// The name, then with `.otf` and `.ttf` when extensionless, so bare TU default names resolve as files.
195    #[must_use]
196    pub fn file_candidates(&self) -> Vec<String> {
197        let mut candidates = vec![self.name.clone()];
198        let base = self.name.rsplit(['/', '\\']).next().unwrap_or(&self.name);
199        if !base.contains('.') {
200            candidates.push(format!("{}.otf", self.name));
201            candidates.push(format!("{}.ttf", self.name));
202        }
203        candidates
204    }
205}
206
207/// Splits a spec into name, file flag, face index, variant and feature text, as XeTeX's `splitFontName`.
208fn split_font_name(spec: &str) -> (String, bool, u32, &str, &str) {
209    if let Some(inner) = spec.strip_prefix('[') {
210        let (path, after) = inner.split_once(']').unwrap_or((inner, ""));
211        let (path, face_index) = match path.rsplit_once(':') {
212            Some((path, index)) if index.bytes().all(|byte| byte.is_ascii_digit()) => {
213                (path, index.parse().unwrap_or(0))
214            }
215            _ => (path, 0),
216        };
217        let (variant, features) = after.split_once(':').unwrap_or((after, ""));
218        return (path.to_string(), true, face_index, variant, features);
219    }
220    let (head, features) = spec.split_once(':').unwrap_or((spec, ""));
221    let (name, variant) = head.split_once('/').unwrap_or((head, ""));
222    (name.to_string(), false, 0, variant, features)
223}
224
225/// Pads or truncates a tag to four bytes with spaces, as HarfBuzz's `hb_tag_from_string`.
226fn ot_tag(tag: &str) -> [u8; 4] {
227    let bytes = tag.trim().as_bytes();
228    std::array::from_fn(|index| bytes.get(index).copied().unwrap_or(b' '))
229}
230
231/// Reads an optionally negative leading decimal, as XeTeX's feature parameter loop.
232fn leading_int(text: &str) -> i64 {
233    let (negative, digits) = text
234        .strip_prefix('-')
235        .map_or((false, text), |rest| (true, rest));
236    let value = digits
237        .bytes()
238        .take_while(u8::is_ascii_digit)
239        .fold(0i64, |value, digit| {
240            value
241                .saturating_mul(10)
242                .saturating_add(i64::from(digit - b'0'))
243        });
244    if negative {
245        -value
246    } else {
247        value
248    }
249}
250
251/// An OpenType feature setting, such as `ssty=1` for math script size variants.
252#[derive(Clone, Copy, Debug, PartialEq, Eq)]
253pub struct ShapeFeature {
254    /// Four byte OpenType feature tag.
255    pub tag: [u8; 4],
256    /// Feature value, 0 disables and 1 selects the first alternate.
257    pub value: u32,
258}
259
260/// Loads fonts from an in process map keyed by file name.
261#[derive(Clone, Debug, Default)]
262pub struct InMemoryFontLoader {
263    fonts: BTreeMap<String, FontData>,
264    next_key: u64,
265}
266
267impl InMemoryFontLoader {
268    /// Creates an empty loader.
269    #[must_use]
270    pub fn new() -> Self {
271        Self::default()
272    }
273
274    /// Adds a font by file name and returns the loader.
275    #[must_use]
276    pub fn with_font(mut self, file: impl Into<String>, bytes: impl Into<Arc<[u8]>>) -> Self {
277        self.insert(file, bytes);
278        self
279    }
280
281    /// Adds or replaces a font by file name and returns its key, keys are never reused.
282    pub fn insert(&mut self, file: impl Into<String>, bytes: impl Into<Arc<[u8]>>) -> FontKey {
283        self.next_key += 1;
284        let key = FontKey(self.next_key);
285        self.fonts.insert(file.into(), FontData::new(key, bytes));
286        key
287    }
288
289    /// Adds or replaces a font the host built, keeping the host's key.
290    pub fn insert_font_data(&mut self, file: impl Into<String>, font: FontData) {
291        self.fonts.insert(file.into(), font);
292    }
293}
294
295impl FontLoader for InMemoryFontLoader {
296    fn load(&self, spec: &FontSpec) -> Result<FontData, FontError> {
297        spec.file_candidates()
298            .iter()
299            .find_map(|name| self.fonts.get(name))
300            .cloned()
301            .ok_or_else(|| FontError::NotFound {
302                name: spec.name().to_string(),
303            })
304    }
305}
306
307/// Corner of an OpenType `MathKernInfo` record, as HarfBuzz's `hb_ot_math_kern_t`.
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
309pub enum MathKernCorner {
310    /// Superscript on the right.
311    TopRight,
312    /// Superscript on the left.
313    TopLeft,
314    /// Subscript on the right.
315    BottomRight,
316    /// Subscript on the left.
317    BottomLeft,
318}
319
320/// A larger OpenType math glyph variant.
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub struct MathVariant {
323    /// The variant glyph.
324    pub glyph: GlyphId,
325    /// Advance along the stretch axis in scaled points.
326    pub advance: i32,
327}
328
329/// One part of an OpenType math glyph assembly, measurements in scaled points.
330#[derive(Clone, Copy, Debug, PartialEq, Eq)]
331pub struct MathAssemblyPart {
332    /// The part's glyph.
333    pub glyph: GlyphId,
334    /// Connector overlap at the leading edge.
335    pub start_connector: i32,
336    /// Connector overlap at the trailing edge.
337    pub end_connector: i32,
338    /// Advance along the assembly axis.
339    pub full_advance: i32,
340    /// Whether the part repeats to reach the target size.
341    pub extender: bool,
342}
343
344/// A face the host parsed and owns, the library borrows it and never parses or copies it.
345pub trait SharedFace: Send + Sync {
346    /// Returns the rustybuzz face the host parsed.
347    fn rustybuzz_face(&self) -> &rustybuzz::Face<'_>;
348
349    /// Returns the ttf view of the face, by default the one inside the rustybuzz face.
350    fn ttf_face(&self) -> &ttf_parser::Face<'_> {
351        self.rustybuzz_face()
352    }
353}
354
355/// Where a font's parsed faces come from.
356#[derive(Clone)]
357enum FaceSource {
358    Bytes {
359        bytes: Arc<[u8]>,
360        // Clones share these caches, so a face is parsed once per font rather than once per clone.
361        ttf: Arc<OnceLock<ParsedFace>>,
362        rustybuzz: Arc<OnceLock<ParsedRustybuzzFace>>,
363    },
364    Shared(Arc<dyn SharedFace>),
365}
366
367/// A loaded font face and the key the host chose for it.
368#[derive(Clone)]
369pub struct FontData {
370    /// Host chosen identity, carried into the IR's font references.
371    pub key: FontKey,
372    source: FaceSource,
373}
374
375impl PartialEq for FontData {
376    fn eq(&self, other: &Self) -> bool {
377        let same_face = match (&self.source, &other.source) {
378            (FaceSource::Bytes { bytes: a, .. }, FaceSource::Bytes { bytes: b, .. }) => a == b,
379            (FaceSource::Shared(a), FaceSource::Shared(b)) => Arc::ptr_eq(a, b),
380            _ => false,
381        };
382        self.key == other.key && same_face
383    }
384}
385
386impl Eq for FontData {}
387
388impl fmt::Debug for FontData {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        f.debug_struct("FontData")
391            .field("key", &self.key)
392            .finish_non_exhaustive()
393    }
394}
395
396impl FontData {
397    /// Wraps raw font bytes, parsed lazily and at most once.
398    #[must_use]
399    pub fn new(key: FontKey, bytes: impl Into<Arc<[u8]>>) -> Self {
400        Self {
401            key,
402            source: FaceSource::Bytes {
403                bytes: bytes.into(),
404                ttf: Arc::default(),
405                rustybuzz: Arc::default(),
406            },
407        }
408    }
409
410    /// Wraps a face the host already parsed, the library never parses it.
411    #[must_use]
412    pub fn from_shared_face(key: FontKey, face: Arc<dyn SharedFace>) -> Self {
413        Self {
414            key,
415            source: FaceSource::Shared(face),
416        }
417    }
418
419    /// Returns the raw bytes the library owns, `None` for host shared faces.
420    #[must_use]
421    pub fn bytes(&self) -> Option<&Arc<[u8]>> {
422        match &self.source {
423            FaceSource::Bytes { bytes, .. } => Some(bytes),
424            FaceSource::Shared(_) => None,
425        }
426    }
427
428    /// Runs `f` with the ttf face, parsed at most once and shared across clones.
429    pub fn with_ttf_face<R>(
430        &self,
431        f: impl FnOnce(&ttf_parser::Face<'_>) -> R,
432    ) -> Result<R, FontError> {
433        Ok(f(self.ttf_face()?))
434    }
435
436    /// Runs `f` with the rustybuzz face, parsed at most once and shared across clones.
437    pub fn with_rustybuzz_face<R>(
438        &self,
439        f: impl FnOnce(&rustybuzz::Face<'_>) -> R,
440    ) -> Result<R, FontError> {
441        Ok(f(self.rustybuzz_face()?))
442    }
443
444    /// Overall metrics at `size`, as XeTeX reads them for a native font.
445    pub fn metrics(&self, size: Length) -> Result<FontMetrics, FontError> {
446        let face = self.ttf_face()?;
447        let upem = units_per_em(face);
448        // XeTeX's getSlant is D2Fix(tan(-italicAngle)), italicAngle is the post table angle in degrees.
449        let angle = f64::from(face.italic_angle());
450        Ok(FontMetrics {
451            ascent: scale_font_units(i32::from(face.ascender()), size, upem),
452            descent: -scale_font_units(i32::from(face.descender()), size, upem),
453            xheight: scale_font_units(i32::from(face.x_height().unwrap_or(0)), size, upem),
454            capheight: scale_font_units(i32::from(face.capital_height().unwrap_or(0)), size, upem),
455            slant: d2fix((-angle).to_radians().tan()),
456        })
457    }
458
459    /// Width, height and depth of a glyph at `size`, from its advance and bounding box.
460    pub fn glyph_metrics(
461        &self,
462        glyph: GlyphId,
463        size: Length,
464    ) -> Result<FontGlyphMetrics, FontError> {
465        let face = self.ttf_face()?;
466        let upem = units_per_em(face);
467        let Some(glyph) = ttf_glyph(glyph) else {
468            return Ok(FontGlyphMetrics::default());
469        };
470        let width = face.glyph_hor_advance(glyph).map_or(0, |advance| {
471            scale_font_units(i32::from(advance), size, upem)
472        });
473        let (height, depth) = face.glyph_bounding_box(glyph).map_or((0, 0), |bbox| {
474            (
475                scale_font_units(i32::from(bbox.y_max).max(0), size, upem),
476                scale_font_units((-i32::from(bbox.y_min)).max(0), size, upem),
477            )
478        });
479        Ok(FontGlyphMetrics {
480            width,
481            height,
482            depth,
483        })
484    }
485
486    /// Advance and control box of a glyph in points at `size`, as XeTeX's `getGlyphBounds` reads them.
487    pub fn glyph_bounds_points(
488        &self,
489        glyph: GlyphId,
490        size: Length,
491    ) -> Result<GlyphBoundsPoints, FontError> {
492        let face = self.ttf_face()?;
493        // XeTeX's unitsToPoints multiplies by the f32 point size before dividing by units per em.
494        let upem = f32::from(face.units_per_em().max(1));
495        let point_size = point_size(size);
496        let points = |units: f32| (units * point_size) / upem;
497        let Some(glyph) = ttf_glyph(glyph) else {
498            return Ok(GlyphBoundsPoints::default());
499        };
500        let advance = face
501            .glyph_hor_advance(glyph)
502            .map_or(0.0, |advance| points(f32::from(advance)));
503        Ok(face.glyph_bounding_box(glyph).map_or(
504            GlyphBoundsPoints {
505                advance,
506                ..GlyphBoundsPoints::default()
507            },
508            |bbox| GlyphBoundsPoints {
509                advance,
510                x_min: points(f32::from(bbox.x_min)),
511                y_min: points(f32::from(bbox.y_min)),
512                x_max: points(f32::from(bbox.x_max)),
513                y_max: points(f32::from(bbox.y_max)),
514            },
515        ))
516    }
517
518    /// Smallest and largest code points the Unicode cmaps map to a glyph, as FreeType's first and last char.
519    pub fn char_code_range(&self) -> Result<Option<(u32, u32)>, FontError> {
520        let face = self.ttf_face()?;
521        let Some(cmap) = face.tables().cmap else {
522            return Ok(None);
523        };
524        let mut range: Option<(u32, u32)> = None;
525        for subtable in cmap.subtables {
526            if !subtable.is_unicode() {
527                continue;
528            }
529            subtable.codepoints(|codepoint| {
530                if subtable
531                    .glyph_index(codepoint)
532                    .is_some_and(|glyph| glyph.0 != 0)
533                {
534                    range = Some(range.map_or((codepoint, codepoint), |(low, high)| {
535                        (low.min(codepoint), high.max(codepoint))
536                    }));
537                }
538            });
539        }
540        Ok(range)
541    }
542
543    /// Outlines in font design units with y up, one entry per glyph, `None` for blank glyphs.
544    pub fn glyph_outlines(
545        &self,
546        glyphs: &[GlyphId],
547    ) -> Result<Vec<Option<GlyphOutline>>, FontError> {
548        let face = self.ttf_face()?;
549        let units_per_em = face.units_per_em();
550        Ok(glyphs
551            .iter()
552            .map(|&glyph| {
553                let glyph = ttf_glyph(glyph)?;
554                let mut collector = OutlineCollector {
555                    commands: Vec::new(),
556                };
557                face.outline_glyph(glyph, &mut collector)?;
558                Some(GlyphOutline {
559                    units_per_em,
560                    commands: collector.commands,
561                })
562            })
563            .collect())
564    }
565
566    /// Looks a glyph up by Unicode code point.
567    pub fn glyph_index(&self, codepoint: char) -> Result<Option<GlyphId>, FontError> {
568        Ok(self
569            .ttf_face()?
570            .glyph_index(codepoint)
571            .map(|glyph| GlyphId(u32::from(glyph.0))))
572    }
573
574    /// Looks a glyph up by glyph name.
575    pub fn glyph_index_by_name(&self, name: &str) -> Result<Option<GlyphId>, FontError> {
576        Ok(self
577            .ttf_face()?
578            .glyph_index_by_name(name)
579            .map(|glyph| GlyphId(u32::from(glyph.0))))
580    }
581
582    /// Whether the font has an OpenType math table.
583    pub fn has_opentype_math(&self) -> Result<bool, FontError> {
584        Ok(self.ttf_face()?.tables().math.is_some())
585    }
586
587    /// Number of glyphs in the font, `\XeTeXcountglyphs`.
588    pub fn ot_glyph_count(&self) -> Result<u32, FontError> {
589        Ok(u32::from(self.ttf_face()?.number_of_glyphs()))
590    }
591
592    /// Number of scripts in the larger of the GSUB and GPOS script lists.
593    pub fn ot_script_count(&self) -> Result<u32, FontError> {
594        Ok(larger_script_list(self.ttf_face()?).map_or(0, |scripts| u32::from(scripts.len())))
595    }
596
597    /// Script tag at `index`, packed big endian as XeTeX's `hb_tag_t`.
598    pub fn ot_script_tag(&self, index: u32) -> Result<u32, FontError> {
599        let Ok(index) = u16::try_from(index) else {
600            return Ok(0);
601        };
602        Ok(larger_script_list(self.ttf_face()?)
603            .and_then(|scripts| scripts.get(index))
604            .map_or(0, |script| script.tag.0))
605    }
606
607    /// Number of languages under `script_tag`, summed across GSUB and GPOS.
608    pub fn ot_language_count(&self, script_tag: u32) -> Result<u32, FontError> {
609        let face = self.ttf_face()?;
610        let script_tag = ttf_parser::Tag(script_tag);
611        let mut count = 0u32;
612        for table in layout_tables(face) {
613            if let Some(script) = table
614                .scripts
615                .index(script_tag)
616                .and_then(|index| table.scripts.get(index))
617            {
618                count += u32::from(script.languages.len());
619            }
620        }
621        Ok(count)
622    }
623
624    /// Language tag at `index` under `script_tag`, `\XeTeXOTlanguagetag`.
625    pub fn ot_language_tag(&self, script_tag: u32, index: u32) -> Result<u32, FontError> {
626        let Ok(index) = u16::try_from(index) else {
627            return Ok(0);
628        };
629        let face = self.ttf_face()?;
630        let script_tag = ttf_parser::Tag(script_tag);
631        for table in layout_tables(face) {
632            if let Some(script) = table
633                .scripts
634                .index(script_tag)
635                .and_then(|script_index| table.scripts.get(script_index))
636            {
637                if index < script.languages.len() {
638                    return Ok(script.languages.get(index).map_or(0, |lang| lang.tag.0));
639                }
640            }
641        }
642        Ok(0)
643    }
644
645    /// Number of features under `script_tag` and `language_tag`, summed across GSUB and GPOS.
646    pub fn ot_feature_count(&self, script_tag: u32, language_tag: u32) -> Result<u32, FontError> {
647        let face = self.ttf_face()?;
648        let script_tag = ttf_parser::Tag(script_tag);
649        let mut count = 0u32;
650        for table in layout_tables(face) {
651            if let Some(langsys) = language_system(table, script_tag, language_tag) {
652                count += u32::from(langsys.feature_indices.len());
653            }
654        }
655        Ok(count)
656    }
657
658    /// Feature tag at `index` under `script_tag` and `language_tag`, `\XeTeXOTfeaturetag`.
659    pub fn ot_feature_tag(
660        &self,
661        script_tag: u32,
662        language_tag: u32,
663        index: u32,
664    ) -> Result<u32, FontError> {
665        let Ok(index) = u16::try_from(index) else {
666            return Ok(0);
667        };
668        let face = self.ttf_face()?;
669        let script_tag = ttf_parser::Tag(script_tag);
670        for table in layout_tables(face) {
671            if let Some(langsys) = language_system(table, script_tag, language_tag) {
672                if let Some(feature_index) = langsys.feature_indices.get(index) {
673                    return Ok(table
674                        .features
675                        .get(feature_index)
676                        .map_or(0, |feature| feature.tag.0));
677                }
678            }
679        }
680        Ok(0)
681    }
682
683    /// OpenType math constant by XeTeX and HarfBuzz index, in scaled points or percent.
684    pub fn opentype_math_constant(&self, constant: i32, size: Length) -> Result<i32, FontError> {
685        let face = self.ttf_face()?;
686        let Some(constants) = face.tables().math.and_then(|table| table.constants) else {
687            return Ok(0);
688        };
689        let Some(value) = math_constant_value(constants, constant) else {
690            return Ok(0);
691        };
692        if is_math_constant_percentage(constant) {
693            Ok(value)
694        } else {
695            Ok(scale_font_units(value, size, units_per_em(face)))
696        }
697    }
698
699    /// OpenType math italic correction of a glyph, in scaled points.
700    pub fn math_italic_correction(&self, glyph: GlyphId, size: Length) -> Result<i32, FontError> {
701        let face = self.ttf_face()?;
702        let value = ttf_glyph(glyph).and_then(|glyph| {
703            face.tables()
704                .math?
705                .glyph_info?
706                .italic_corrections?
707                .get(glyph)
708        });
709        Ok(value.map_or(0, |value| {
710            scale_font_units(i32::from(value.value), size, units_per_em(face))
711        }))
712    }
713
714    /// One `MathKernInfo` corner at a correction height in scaled points, in scaled points.
715    pub fn math_kern_at(
716        &self,
717        glyph: GlyphId,
718        corner: MathKernCorner,
719        correction_height: i32,
720        size: Length,
721    ) -> Result<i32, FontError> {
722        let height = self.points_to_units((correction_height as f32) / 65536.0, size)? as i32;
723        let kern = self.math_kern_units(glyph, corner, height)?;
724        self.units_to_scaled(kern, size)
725    }
726
727    /// One `MathKernInfo` corner in font design units, XeTeX sums kerns in these units before scaling.
728    pub fn math_kern_units(
729        &self,
730        glyph: GlyphId,
731        corner: MathKernCorner,
732        correction_height: i32,
733    ) -> Result<i32, FontError> {
734        let face = self.ttf_face()?;
735        let Some(kern_info) = ttf_glyph(glyph)
736            .and_then(|glyph| face.tables().math?.glyph_info?.kern_infos?.get(glyph))
737        else {
738            return Ok(0);
739        };
740        let kern = match corner {
741            MathKernCorner::TopRight => kern_info.top_right,
742            MathKernCorner::TopLeft => kern_info.top_left,
743            MathKernCorner::BottomRight => kern_info.bottom_right,
744            MathKernCorner::BottomLeft => kern_info.bottom_left,
745        };
746        let Some(kern) = kern else {
747            return Ok(0);
748        };
749        let mut index = 0u16;
750        while index < kern.count() {
751            match kern.height(index) {
752                Some(height) if correction_height < i32::from(height.value) => break,
753                _ => index += 1,
754            }
755        }
756        Ok(kern.kern(index).map_or(0, |value| i32::from(value.value)))
757    }
758
759    /// The larger math glyph variant at `index`, `None` past the last one.
760    pub fn math_variant(
761        &self,
762        glyph: GlyphId,
763        index: u16,
764        horizontal: bool,
765        size: Length,
766    ) -> Result<Option<MathVariant>, FontError> {
767        let face = self.ttf_face()?;
768        let variant = ttf_glyph(glyph).and_then(|glyph| {
769            math_constructions(face, horizontal)?
770                .get(glyph)?
771                .variants
772                .get(index)
773        });
774        Ok(variant.map(|variant| MathVariant {
775            glyph: GlyphId(u32::from(variant.variant_glyph.0)),
776            advance: scale_font_units(
777                i32::from(variant.advance_measurement),
778                size,
779                units_per_em(face),
780            ),
781        }))
782    }
783
784    /// Math glyph assembly parts, measurements in scaled points, empty when the glyph has none.
785    pub fn math_assembly(
786        &self,
787        glyph: GlyphId,
788        horizontal: bool,
789        size: Length,
790    ) -> Result<Vec<MathAssemblyPart>, FontError> {
791        let face = self.ttf_face()?;
792        let upem = units_per_em(face);
793        let Some(assembly) = ttf_glyph(glyph)
794            .and_then(|glyph| math_constructions(face, horizontal)?.get(glyph)?.assembly)
795        else {
796            return Ok(Vec::new());
797        };
798        let scale = |units: u16| scale_font_units(i32::from(units), size, upem);
799        Ok(assembly
800            .parts
801            .into_iter()
802            .map(|part| MathAssemblyPart {
803                glyph: GlyphId(u32::from(part.glyph_id.0)),
804                start_connector: scale(part.start_connector_length),
805                end_connector: scale(part.end_connector_length),
806                full_advance: scale(part.full_advance),
807                extender: part.part_flags.extender(),
808            })
809            .collect())
810    }
811
812    /// Minimum connector overlap between assembly parts, in scaled points.
813    pub fn math_min_connector_overlap(&self, size: Length) -> Result<i32, FontError> {
814        let face = self.ttf_face()?;
815        let overlap = face
816            .tables()
817            .math
818            .and_then(|table| table.variants)
819            .map_or(0, |variants| i32::from(variants.min_connector_overlap));
820        Ok(scale_font_units(overlap, size, units_per_em(face)))
821    }
822
823    /// Converts points to design units in f32, as XeTeX's `pointsToUnits`.
824    pub fn points_to_units(&self, points: f32, size: Length) -> Result<f32, FontError> {
825        let upem = units_per_em(self.ttf_face()?);
826        let point_size = point_size(size);
827        if point_size == 0.0 {
828            return Ok(0.0);
829        }
830        Ok((points * upem as f32) / point_size)
831    }
832
833    /// Converts design units to scaled points, as XeTeX's `D2Fix(unitsToPoints(units))`.
834    pub fn units_to_scaled(&self, units: i32, size: Length) -> Result<i32, FontError> {
835        Ok(scale_font_units(
836            units,
837            size,
838            units_per_em(self.ttf_face()?),
839        ))
840    }
841
842    /// Top accent attachment of a glyph in scaled points, `\XeTeXmathaccent`.
843    pub fn opentype_math_accent_position(
844        &self,
845        glyph: GlyphId,
846        size: Length,
847    ) -> Result<i32, FontError> {
848        let face = self.ttf_face()?;
849        let value = ttf_glyph(glyph).and_then(|glyph| {
850            face.tables()
851                .math?
852                .glyph_info?
853                .top_accent_attachments?
854                .get(glyph)
855        });
856        Ok(value.map_or(0, |value| {
857            scale_font_units(i32::from(value.value), size, units_per_em(face))
858        }))
859    }
860
861    /// Symbol font parameter mapped to OpenType math constants, as XeTeX's `get_native_mathsy_param`.
862    pub fn math_symbol_parameter(&self, parameter: i32, size: Length) -> Result<i32, FontError> {
863        match parameter {
864            5 => self.opentype_math_constant(6, size),
865            6 => Ok(size.0),
866            8 => self.opentype_math_constant(33, size),
867            9 => self.opentype_math_constant(32, size),
868            10 => self.opentype_math_constant(22, size),
869            11 => self.opentype_math_constant(35, size),
870            12 => self.opentype_math_constant(34, size),
871            13 | 14 => self.opentype_math_constant(11, size),
872            15 => self.opentype_math_constant(12, size),
873            16 | 17 => self.opentype_math_constant(8, size),
874            18 => self.opentype_math_constant(14, size),
875            19 => self.opentype_math_constant(10, size),
876            20 => self.opentype_math_constant(2, size),
877            21 => {
878                let delim1 = self.math_symbol_parameter(20, size)?;
879                Ok(((i64::from(size.0) * 3) / 2)
880                    .min(i64::from(delim1))
881                    .clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32)
882            }
883            22 => self.opentype_math_constant(5, size),
884            _ => Ok(0),
885        }
886    }
887
888    /// Extension font parameter mapped to OpenType math constants, as XeTeX's `get_native_mathex_param`.
889    pub fn math_extension_parameter(&self, parameter: i32, size: Length) -> Result<i32, FontError> {
890        match parameter {
891            5 => self.opentype_math_constant(6, size),
892            6 => Ok(size.0),
893            8 => self.opentype_math_constant(38, size),
894            9 => self.opentype_math_constant(18, size),
895            10 => self.opentype_math_constant(20, size),
896            11 => self.opentype_math_constant(19, size),
897            12 => self.opentype_math_constant(21, size),
898            13 => self.opentype_math_constant(26, size),
899            _ => Ok(0),
900        }
901    }
902
903    fn invalid(&self, message: String) -> FontError {
904        FontError::Invalid {
905            name: format!("font key {}", self.key.0),
906            message,
907        }
908    }
909
910    fn ttf_face(&self) -> Result<&ttf_parser::Face<'_>, FontError> {
911        match &self.source {
912            FaceSource::Shared(face) => Ok(face.ttf_face()),
913            FaceSource::Bytes { bytes, ttf, .. } => {
914                if ttf.get().is_none() {
915                    let parsed = ParsedFace::try_new(Arc::clone(bytes), |bytes| {
916                        ttf_parser::Face::parse(bytes, 0)
917                            .map_err(|error| self.invalid(format!("invalid font data: {error}")))
918                    })?;
919                    // A racing initializer may win, either way the cell is populated afterwards.
920                    let _ = ttf.set(parsed);
921                }
922                Ok(ttf
923                    .get()
924                    .expect("ttf cache populated above")
925                    .borrow_dependent())
926            }
927        }
928    }
929
930    fn rustybuzz_face(&self) -> Result<&rustybuzz::Face<'_>, FontError> {
931        match &self.source {
932            FaceSource::Shared(face) => Ok(face.rustybuzz_face()),
933            FaceSource::Bytes {
934                bytes, rustybuzz, ..
935            } => {
936                if rustybuzz.get().is_none() {
937                    let parsed = ParsedRustybuzzFace::try_new(Arc::clone(bytes), |bytes| {
938                        rustybuzz::Face::from_slice(bytes, 0)
939                            .ok_or_else(|| self.invalid("invalid font data".to_string()))
940                    })?;
941                    let _ = rustybuzz.set(parsed);
942                }
943                Ok(rustybuzz
944                    .get()
945                    .expect("rustybuzz cache populated above")
946                    .borrow_dependent())
947            }
948        }
949    }
950}
951
952/// Overall font metrics in scaled points.
953#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
954pub struct FontMetrics {
955    /// Distance from the baseline up to the ascender line.
956    pub ascent: i32,
957    /// Distance from the baseline down to the descender line, positive like a TeX depth.
958    pub descent: i32,
959    /// Height of a lowercase x.
960    pub xheight: i32,
961    /// Height of a capital letter.
962    pub capheight: i32,
963    /// Slant as a 16.16 fixed point ratio, `\fontdimen1`.
964    pub slant: i32,
965}
966
967/// Metrics of one glyph in scaled points.
968#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
969pub struct FontGlyphMetrics {
970    /// Horizontal advance.
971    pub width: i32,
972    /// Height above the baseline.
973    pub height: i32,
974    /// Depth below the baseline.
975    pub depth: i32,
976}
977
978/// A glyph's advance and control box in points, y up.
979#[derive(Clone, Copy, Debug, Default, PartialEq)]
980pub struct GlyphBoundsPoints {
981    /// Horizontal advance.
982    pub advance: f32,
983    /// Left edge of the control box.
984    pub x_min: f32,
985    /// Bottom edge of the control box.
986    pub y_min: f32,
987    /// Right edge of the control box.
988    pub x_max: f32,
989    /// Top edge of the control box.
990    pub y_max: f32,
991}
992
993/// Font loading or parsing failure.
994#[derive(Clone, Debug, PartialEq, Eq)]
995#[non_exhaustive]
996pub enum FontError {
997    /// The loader has no font for the spec.
998    NotFound {
999        /// The name the spec asked for.
1000        name: String,
1001    },
1002    /// The font exists but is unusable.
1003    Invalid {
1004        /// The font's name or key.
1005        name: String,
1006        /// Why the font is unusable.
1007        message: String,
1008    },
1009}
1010
1011impl fmt::Display for FontError {
1012    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1013        match self {
1014            Self::NotFound { name } => write!(f, "font not found: {name}"),
1015            Self::Invalid { name, message } => write!(f, "font {name} is unusable: {message}"),
1016        }
1017    }
1018}
1019
1020impl std::error::Error for FontError {}
1021
1022/// Names the self_cell dependent's lifetime.
1023type TtfFace<'a> = ttf_parser::Face<'a>;
1024
1025self_cell::self_cell!(
1026    /// Owns font bytes together with the ttf face parsed from them.
1027    struct ParsedFace {
1028        owner: Arc<[u8]>,
1029        #[covariant]
1030        dependent: TtfFace,
1031    }
1032);
1033
1034/// Names the self_cell dependent's lifetime.
1035type RbFace<'a> = rustybuzz::Face<'a>;
1036
1037self_cell::self_cell!(
1038    /// Owns font bytes together with the rustybuzz face parsed from them.
1039    struct ParsedRustybuzzFace {
1040        owner: Arc<[u8]>,
1041        #[covariant]
1042        dependent: RbFace,
1043    }
1044);
1045
1046fn units_per_em(face: &ttf_parser::Face<'_>) -> i32 {
1047    i32::from(face.units_per_em()).max(1)
1048}
1049
1050fn ttf_glyph(glyph: GlyphId) -> Option<ttf_parser::GlyphId> {
1051    u16::try_from(glyph.0).ok().map(ttf_parser::GlyphId)
1052}
1053
1054/// XeTeX keeps the point size as f32 in `XeTeXFontInst`.
1055fn point_size(size: Length) -> f32 {
1056    (f64::from(size.0) / 65536.0) as f32
1057}
1058
1059/// XeTeX's D2Fix, a C cast truncates toward zero after adding one half.
1060fn d2fix(value: f64) -> i32 {
1061    (value * 65536.0 + 0.5)
1062        .trunc()
1063        .clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
1064}
1065
1066/// Scales design units to scaled points as XeTeX's `D2Fix(unitsToPoints(value))`.
1067fn scale_font_units(value: i32, size: Length, units_per_em: i32) -> i32 {
1068    let points = (value as f32 * point_size(size)) / (units_per_em.max(1) as f32);
1069    d2fix(f64::from(points))
1070}
1071
1072fn layout_tables<'a>(
1073    face: &ttf_parser::Face<'a>,
1074) -> impl Iterator<Item = ttf_parser::opentype_layout::LayoutTable<'a>> {
1075    [face.tables().gsub, face.tables().gpos]
1076        .into_iter()
1077        .flatten()
1078}
1079
1080/// The larger of the GSUB and GPOS script lists, as XeTeX's `getLargerScriptListTable`.
1081fn larger_script_list<'a>(
1082    face: &ttf_parser::Face<'a>,
1083) -> Option<ttf_parser::opentype_layout::ScriptList<'a>> {
1084    let gsub = face.tables().gsub.map(|table| table.scripts);
1085    let gpos = face.tables().gpos.map(|table| table.scripts);
1086    match (gsub, gpos) {
1087        (Some(sub), Some(pos)) => Some(if pos.len() > sub.len() { pos } else { sub }),
1088        (sub, pos) => sub.or(pos),
1089    }
1090}
1091
1092/// Resolves a language system, `language_tag == 0` selects the script default.
1093fn language_system<'a>(
1094    table: ttf_parser::opentype_layout::LayoutTable<'a>,
1095    script_tag: ttf_parser::Tag,
1096    language_tag: u32,
1097) -> Option<ttf_parser::opentype_layout::LanguageSystem<'a>> {
1098    let script = table
1099        .scripts
1100        .index(script_tag)
1101        .and_then(|index| table.scripts.get(index))?;
1102    if language_tag == 0 {
1103        script.default_language
1104    } else {
1105        script
1106            .languages
1107            .index(ttf_parser::Tag(language_tag))
1108            .and_then(|index| script.languages.get(index))
1109            .or(script.default_language)
1110    }
1111}
1112
1113fn math_constructions<'a>(
1114    face: &ttf_parser::Face<'a>,
1115    horizontal: bool,
1116) -> Option<ttf_parser::math::GlyphConstructions<'a>> {
1117    let variants = face.tables().math?.variants?;
1118    Some(if horizontal {
1119        variants.horizontal_constructions
1120    } else {
1121        variants.vertical_constructions
1122    })
1123}
1124
1125/// Collects ttf-parser contour callbacks into [`OutlineCommand`]s.
1126struct OutlineCollector {
1127    commands: Vec<OutlineCommand>,
1128}
1129
1130impl ttf_parser::OutlineBuilder for OutlineCollector {
1131    fn move_to(&mut self, x: f32, y: f32) {
1132        self.commands.push(OutlineCommand::MoveTo { x, y });
1133    }
1134
1135    fn line_to(&mut self, x: f32, y: f32) {
1136        self.commands.push(OutlineCommand::LineTo { x, y });
1137    }
1138
1139    fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
1140        self.commands.push(OutlineCommand::QuadTo { cx, cy, x, y });
1141    }
1142
1143    fn curve_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) {
1144        self.commands.push(OutlineCommand::CurveTo {
1145            c1x,
1146            c1y,
1147            c2x,
1148            c2y,
1149            x,
1150            y,
1151        });
1152    }
1153
1154    fn close(&mut self) {
1155        self.commands.push(OutlineCommand::Close);
1156    }
1157}
1158
1159fn math_constant_value(constants: ttf_parser::math::Constants<'_>, constant: i32) -> Option<i32> {
1160    let value = match constant {
1161        0 => i32::from(constants.script_percent_scale_down()),
1162        1 => i32::from(constants.script_script_percent_scale_down()),
1163        2 => i32::from(constants.delimited_sub_formula_min_height()),
1164        3 => i32::from(constants.display_operator_min_height()),
1165        4 => i32::from(constants.math_leading().value),
1166        5 => i32::from(constants.axis_height().value),
1167        6 => i32::from(constants.accent_base_height().value),
1168        7 => i32::from(constants.flattened_accent_base_height().value),
1169        8 => i32::from(constants.subscript_shift_down().value),
1170        9 => i32::from(constants.subscript_top_max().value),
1171        10 => i32::from(constants.subscript_baseline_drop_min().value),
1172        11 => i32::from(constants.superscript_shift_up().value),
1173        12 => i32::from(constants.superscript_shift_up_cramped().value),
1174        13 => i32::from(constants.superscript_bottom_min().value),
1175        14 => i32::from(constants.superscript_baseline_drop_max().value),
1176        15 => i32::from(constants.sub_superscript_gap_min().value),
1177        16 => i32::from(constants.superscript_bottom_max_with_subscript().value),
1178        17 => i32::from(constants.space_after_script().value),
1179        18 => i32::from(constants.upper_limit_gap_min().value),
1180        19 => i32::from(constants.upper_limit_baseline_rise_min().value),
1181        20 => i32::from(constants.lower_limit_gap_min().value),
1182        21 => i32::from(constants.lower_limit_baseline_drop_min().value),
1183        22 => i32::from(constants.stack_top_shift_up().value),
1184        23 => i32::from(constants.stack_top_display_style_shift_up().value),
1185        24 => i32::from(constants.stack_bottom_shift_down().value),
1186        25 => i32::from(constants.stack_bottom_display_style_shift_down().value),
1187        26 => i32::from(constants.stack_gap_min().value),
1188        27 => i32::from(constants.stack_display_style_gap_min().value),
1189        28 => i32::from(constants.stretch_stack_top_shift_up().value),
1190        29 => i32::from(constants.stretch_stack_bottom_shift_down().value),
1191        30 => i32::from(constants.stretch_stack_gap_above_min().value),
1192        31 => i32::from(constants.stretch_stack_gap_below_min().value),
1193        32 => i32::from(constants.fraction_numerator_shift_up().value),
1194        33 => i32::from(constants.fraction_numerator_display_style_shift_up().value),
1195        34 => i32::from(constants.fraction_denominator_shift_down().value),
1196        35 => i32::from(
1197            constants
1198                .fraction_denominator_display_style_shift_down()
1199                .value,
1200        ),
1201        36 => i32::from(constants.fraction_numerator_gap_min().value),
1202        37 => i32::from(constants.fraction_num_display_style_gap_min().value),
1203        38 => i32::from(constants.fraction_rule_thickness().value),
1204        39 => i32::from(constants.fraction_denominator_gap_min().value),
1205        40 => i32::from(constants.fraction_denom_display_style_gap_min().value),
1206        41 => i32::from(constants.skewed_fraction_horizontal_gap().value),
1207        42 => i32::from(constants.skewed_fraction_vertical_gap().value),
1208        43 => i32::from(constants.overbar_vertical_gap().value),
1209        44 => i32::from(constants.overbar_rule_thickness().value),
1210        45 => i32::from(constants.overbar_extra_ascender().value),
1211        46 => i32::from(constants.underbar_vertical_gap().value),
1212        47 => i32::from(constants.underbar_rule_thickness().value),
1213        48 => i32::from(constants.underbar_extra_descender().value),
1214        49 => i32::from(constants.radical_vertical_gap().value),
1215        50 => i32::from(constants.radical_display_style_vertical_gap().value),
1216        51 => i32::from(constants.radical_rule_thickness().value),
1217        52 => i32::from(constants.radical_extra_ascender().value),
1218        53 => i32::from(constants.radical_kern_before_degree().value),
1219        54 => i32::from(constants.radical_kern_after_degree().value),
1220        55 => i32::from(constants.radical_degree_bottom_raise_percent()),
1221        _ => return None,
1222    };
1223    Some(value)
1224}
1225
1226fn is_math_constant_percentage(constant: i32) -> bool {
1227    matches!(constant, 0 | 1 | 55)
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232    use super::*;
1233    use std::path::Path;
1234
1235    const PT: i32 = 65_536;
1236    const LM_MATH: &str = "fonts/opentype/public/lm-math/latinmodern-math.otf";
1237    const LM_ITALIC: &str = "fonts/opentype/public/lm/lmroman10-italic.otf";
1238    const STIX_MATH: &str = "fonts/opentype/public/stix2-otf/STIXTwoMath-Regular.otf";
1239
1240    /// Reads a font under `MATHTEX_TEXMF_ROOT`, `None` when the variable is unset.
1241    fn texlive_font(relative: &str) -> Option<FontData> {
1242        let Some(root) = std::env::var_os("MATHTEX_TEXMF_ROOT") else {
1243            assert!(
1244                std::env::var("MATHTEX_REQUIRE_TEXLIVE").as_deref() != Ok("1"),
1245                "MATHTEX_REQUIRE_TEXLIVE=1 but MATHTEX_TEXMF_ROOT is not set"
1246            );
1247            eprintln!("skipping: MATHTEX_TEXMF_ROOT is not set");
1248            return None;
1249        };
1250        let path = Path::new(&root).join(relative);
1251        let bytes = std::fs::read(&path).unwrap_or_else(|error| {
1252            panic!(
1253                "MATHTEX_TEXMF_ROOT is set but {} is unreadable: {error}",
1254                path.display()
1255            )
1256        });
1257        Some(FontData::new(FontKey(1), bytes))
1258    }
1259
1260    /// Reads a font from the texlive-source fixtures, `None` after a skip notice.
1261    fn fixture_font(relative: &str) -> Option<FontData> {
1262        mathtex_test_fixtures::read(relative).map(|bytes| FontData::new(FontKey(2), bytes))
1263    }
1264
1265    fn ten_pt() -> Length {
1266        Length(10 * PT)
1267    }
1268
1269    #[test]
1270    fn spec_parses_files_features_and_suffixes() {
1271        let spec = FontSpec::parse(
1272            "[latinmodern-math.otf]:script=math;+ssty=0;-liga,language=DEU;mapping=tex-text;kern=2",
1273            ten_pt(),
1274        );
1275        assert!(spec.is_file());
1276        assert_eq!(spec.name(), "latinmodern-math.otf");
1277        assert_eq!(spec.script(), Some(*b"math"));
1278        assert_eq!(spec.language(), Some("DEU"));
1279        assert_eq!(spec.option("mapping"), Some("tex-text"));
1280        assert_eq!(
1281            spec.features(),
1282            [
1283                ShapeFeature {
1284                    tag: *b"ssty",
1285                    value: 1
1286                },
1287                ShapeFeature {
1288                    tag: *b"liga",
1289                    value: 0
1290                },
1291                ShapeFeature {
1292                    tag: *b"kern",
1293                    value: 2
1294                },
1295            ]
1296        );
1297        assert_eq!(spec.file_candidates(), ["latinmodern-math.otf"]);
1298
1299        let spec = FontSpec::parse("Latin Modern Roman/B/OT:+smcp;vertical;bogus", ten_pt());
1300        assert!(!spec.is_file());
1301        assert_eq!(spec.name(), "Latin Modern Roman");
1302        assert_eq!(spec.variants(), ["B", "OT"]);
1303        assert_eq!(
1304            spec.features(),
1305            [ShapeFeature {
1306                tag: *b"smcp",
1307                value: 1
1308            }]
1309        );
1310        assert!(spec.vertical());
1311        assert_eq!(spec.unknown_options(), ["bogus"]);
1312
1313        let spec = FontSpec::parse("[fonts/collection.ttc:2]/AAT:color=FF0000", ten_pt());
1314        assert_eq!(spec.name(), "fonts/collection.ttc");
1315        assert_eq!(spec.face_index(), 2);
1316        assert_eq!(spec.variants(), ["AAT"]);
1317        assert_eq!(spec.option("color"), Some("FF0000"));
1318        assert_eq!(spec.size(), ten_pt());
1319
1320        let spec = FontSpec::parse("lmroman10-regular:+tlig=-1", ten_pt());
1321        assert_eq!(
1322            spec.file_candidates(),
1323            [
1324                "lmroman10-regular",
1325                "lmroman10-regular.otf",
1326                "lmroman10-regular.ttf"
1327            ]
1328        );
1329        assert_eq!(spec.features()[0].value, u32::MAX);
1330    }
1331
1332    #[test]
1333    fn in_memory_loader_resolves_candidates_and_never_reuses_keys() {
1334        let mut fonts = InMemoryFontLoader::new();
1335        let first = fonts.insert("a.otf", b"one".to_vec());
1336        let second = fonts.insert("b.otf", b"two".to_vec());
1337        let replaced = fonts.insert("a.otf", b"three".to_vec());
1338        assert_ne!(first, second);
1339        assert_ne!(replaced, first);
1340        assert_ne!(replaced, second);
1341
1342        let font = fonts.load(&FontSpec::parse("a", ten_pt())).expect("a.otf");
1343        assert_eq!(font.key, replaced);
1344        assert_eq!(&**font.bytes().expect("owned bytes"), b"three");
1345        assert_eq!(
1346            fonts.load(&FontSpec::parse("[missing.otf]", ten_pt())),
1347            Err(FontError::NotFound {
1348                name: "missing.otf".into()
1349            })
1350        );
1351    }
1352
1353    #[test]
1354    fn scale_font_units_matches_xetex_d2fix_rounding() {
1355        // The latinmodern-math `2` rounds 436469.76sp up to 436470 as XeTeX does.
1356        assert_eq!(scale_font_units(666, ten_pt(), 1000), 436_470);
1357        assert_eq!(scale_font_units(431, ten_pt(), 1000), 282_460);
1358        assert_eq!(scale_font_units(528, ten_pt(), 1000), 346_030);
1359        assert_eq!(scale_font_units(16, ten_pt(), 1000), 10_486);
1360        // D2Fix truncates after adding one half, so minus 10485.26 becomes minus 10485.
1361        assert_eq!(scale_font_units(-16, ten_pt(), 1000), -10_485);
1362        assert_eq!(scale_font_units(0, ten_pt(), 1000), 0);
1363    }
1364
1365    #[test]
1366    fn font_data_clones_share_both_parsed_faces() {
1367        let Some(font) = fixture_font(mathtex_test_fixtures::DEJAVU_SANS) else {
1368            return;
1369        };
1370        let clone = font.clone();
1371        let ttf = |font: &FontData| {
1372            font.with_ttf_face(|face| face as *const ttf_parser::Face<'_> as usize)
1373                .expect("ttf parse")
1374        };
1375        let rb = |font: &FontData| {
1376            font.with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1377                .expect("rustybuzz parse")
1378        };
1379        assert_eq!(ttf(&font), ttf(&clone));
1380        assert_eq!(rb(&font), rb(&clone));
1381        assert!(Arc::ptr_eq(
1382            font.bytes().expect("owned bytes"),
1383            clone.bytes().expect("owned bytes")
1384        ));
1385    }
1386
1387    self_cell::self_cell!(
1388        struct AppOwnedFace {
1389            owner: Arc<[u8]>,
1390            #[covariant]
1391            dependent: RbFace,
1392        }
1393    );
1394
1395    struct AppShared(AppOwnedFace);
1396
1397    impl SharedFace for AppShared {
1398        fn rustybuzz_face(&self) -> &rustybuzz::Face<'_> {
1399            self.0.borrow_dependent()
1400        }
1401    }
1402
1403    #[test]
1404    fn host_owned_face_is_borrowed_and_never_parsed_by_the_library() {
1405        let Some(owned) = fixture_font(mathtex_test_fixtures::LM_MONO) else {
1406            return;
1407        };
1408        let bytes = Arc::clone(owned.bytes().expect("owned bytes"));
1409        let app_face = AppOwnedFace::try_new(bytes, |bytes| {
1410            rustybuzz::Face::from_slice(bytes, 0).ok_or("host parse failed")
1411        })
1412        .expect("host parses its own font");
1413        let app_face: Arc<dyn SharedFace> = Arc::new(AppShared(app_face));
1414        let app_ptr = app_face.rustybuzz_face() as *const rustybuzz::Face<'_> as usize;
1415        let font = FontData::from_shared_face(FontKey(3), Arc::clone(&app_face));
1416
1417        assert!(font.bytes().is_none());
1418        let lib_ptr = font
1419            .with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1420            .expect("borrow shared face");
1421        assert_eq!(lib_ptr, app_ptr);
1422        assert!(font.metrics(ten_pt()).expect("metrics").ascent > 0);
1423    }
1424
1425    #[test]
1426    fn font_data_is_send_and_sync() {
1427        fn assert_send_sync<T: Send + Sync>() {}
1428        assert_send_sync::<FontData>();
1429    }
1430
1431    #[test]
1432    fn metrics_slant_is_the_tangent_of_the_italic_angle_and_descent_is_positive() {
1433        let (Some(italic), Some(math)) = (texlive_font(LM_ITALIC), texlive_font(LM_MATH)) else {
1434            return;
1435        };
1436        // xetex reports \fontdimen1 of lmroman10-italic.otf as 0.25pt.
1437        assert_eq!(italic.metrics(ten_pt()).expect("metrics").slant, PT / 4);
1438        let metrics = math.metrics(ten_pt()).expect("metrics");
1439        assert_eq!(metrics.slant, 0);
1440        assert!(metrics.descent > 0, "descent {}", metrics.descent);
1441    }
1442
1443    #[test]
1444    fn glyph_outlines_extract_design_unit_contours_from_cff_and_truetype() {
1445        for (file, units_per_em) in [
1446            (mathtex_test_fixtures::LM_MONO, 1000),
1447            (mathtex_test_fixtures::DEJAVU_SANS, 2048),
1448        ] {
1449            let Some(font) = fixture_font(file) else {
1450                return;
1451            };
1452            let x = font.glyph_index('x').unwrap().unwrap();
1453            let space = font.glyph_index(' ').unwrap().unwrap();
1454            let outlines = font.glyph_outlines(&[x, space]).unwrap();
1455            let x_outline = outlines[0].as_ref().expect("x has an outline");
1456            assert_eq!(x_outline.units_per_em, units_per_em, "{file}");
1457            assert!(matches!(
1458                x_outline.commands[0],
1459                OutlineCommand::MoveTo { .. }
1460            ));
1461            assert!(outlines[1].is_none(), "space has no outline in {file}");
1462            // CFF draws curves as cubics and TrueType as quadratics.
1463            let o = font.glyph_index('o').unwrap().unwrap();
1464            let o_outline = font.glyph_outlines(&[o]).unwrap().remove(0).expect("o");
1465            let cubic =
1466                |command: &OutlineCommand| matches!(command, OutlineCommand::CurveTo { .. });
1467            let quadratic =
1468                |command: &OutlineCommand| matches!(command, OutlineCommand::QuadTo { .. });
1469            let cff = file == mathtex_test_fixtures::LM_MONO;
1470            assert_eq!(o_outline.commands.iter().any(cubic), cff, "{file}");
1471            assert_eq!(o_outline.commands.iter().any(quadratic), !cff, "{file}");
1472        }
1473    }
1474
1475    #[test]
1476    fn math_variant_returns_larger_paren_glyphs_from_latinmodern() {
1477        let Some(font) = texlive_font(LM_MATH) else {
1478            return;
1479        };
1480        // Glyph 9 vertical variant 4 is glyph 2433 advancing 1175061sp at 10pt.
1481        let paren = font.glyph_index('(').unwrap().unwrap();
1482        assert_eq!(paren, GlyphId(9));
1483        assert_eq!(
1484            font.math_variant(paren, 4, false, ten_pt()).unwrap(),
1485            Some(MathVariant {
1486                glyph: GlyphId(2433),
1487                advance: 1_175_061
1488            })
1489        );
1490        assert_eq!(
1491            font.math_variant(paren, 0, false, ten_pt()).unwrap(),
1492            Some(MathVariant {
1493                glyph: GlyphId(9),
1494                advance: 653_394
1495            })
1496        );
1497        assert_eq!(font.math_variant(paren, 99, false, ten_pt()).unwrap(), None);
1498    }
1499
1500    #[test]
1501    fn math_assembly_returns_paren_parts_from_latinmodern() {
1502        let Some(font) = texlive_font(LM_MATH) else {
1503            return;
1504        };
1505        let paren = font.glyph_index('(').unwrap().unwrap();
1506        let parts = font.math_assembly(paren, false, ten_pt()).unwrap();
1507        let part =
1508            |glyph, start_connector, end_connector, full_advance, extender| MathAssemblyPart {
1509                glyph: GlyphId(glyph),
1510                start_connector,
1511                end_connector,
1512                full_advance,
1513                extender,
1514            };
1515        // Bottom 2503, extender 2504, top 2505, connectors of 249 and 498 units, advances of 1495 and 498.
1516        assert_eq!(
1517            parts,
1518            [
1519                part(2503, 0, 163_185, 979_763, false),
1520                part(2504, 326_369, 326_369, 326_369, true),
1521                part(2505, 163_185, 0, 979_763, false),
1522            ]
1523        );
1524        // A minimum overlap of 20 units is 13107sp at 10pt.
1525        assert_eq!(font.math_min_connector_overlap(ten_pt()).unwrap(), 13_107);
1526    }
1527
1528    #[test]
1529    fn math_kern_reads_stix_cut_ins_in_units_and_scaled_points() {
1530        let Some(font) = texlive_font(STIX_MATH) else {
1531            return;
1532        };
1533        // `F` has one TopRight kern of 44 units, so every height returns it.
1534        let f = font.glyph_index('F').unwrap().unwrap();
1535        assert_eq!(
1536            font.math_kern_units(f, MathKernCorner::TopRight, 0)
1537                .unwrap(),
1538            44
1539        );
1540        assert_eq!(
1541            font.math_kern_units(f, MathKernCorner::TopRight, 100_000)
1542                .unwrap(),
1543            44
1544        );
1545        // `V` has BottomRight heights 126 and 280 with kerns -193, -119 and 56.
1546        let v = font.glyph_index('V').unwrap().unwrap();
1547        let bottom_right = |height| {
1548            font.math_kern_units(v, MathKernCorner::BottomRight, height)
1549                .unwrap()
1550        };
1551        assert_eq!(bottom_right(0), -193);
1552        assert_eq!(bottom_right(200), -119);
1553        assert_eq!(bottom_right(300), 56);
1554        assert_eq!(
1555            font.math_kern_units(v, MathKernCorner::TopRight, 0)
1556                .unwrap(),
1557            0
1558        );
1559        // 200 units is 2pt at 10pt in a 1000 unit font, and -119 units scales like any other length.
1560        let scaled = font
1561            .math_kern_at(v, MathKernCorner::BottomRight, 2 * PT, ten_pt())
1562            .unwrap();
1563        assert_eq!(scaled, scale_font_units(-119, ten_pt(), 1000));
1564    }
1565
1566    #[test]
1567    fn latinmodern_has_no_math_kern_info() {
1568        let Some(font) = texlive_font(LM_MATH) else {
1569            return;
1570        };
1571        let x = font.glyph_index('x').unwrap().unwrap();
1572        for corner in [
1573            MathKernCorner::TopRight,
1574            MathKernCorner::TopLeft,
1575            MathKernCorner::BottomRight,
1576            MathKernCorner::BottomLeft,
1577        ] {
1578            assert_eq!(font.math_kern_units(x, corner, 0).unwrap(), 0);
1579        }
1580    }
1581}