Skip to main content

mathtex_font/
lib.rs

1//! Font loading, shaping, metrics, and OpenType math access for mathtex.
2#![cfg_attr(not(feature = "std"), no_std)]
3#![forbid(unsafe_code)]
4
5extern crate alloc;
6
7use core::cell::RefCell;
8
9use alloc::collections::BTreeMap;
10use alloc::format;
11use alloc::string::{String, ToString};
12use alloc::sync::Arc;
13use alloc::vec::Vec;
14
15use mathtex_ir::{
16    ByteSpan, Direction, FontId, GlyphId, GlyphOutline, Length, OutlineCommand, Point,
17    PositionedGlyph,
18};
19
20// Re exported so applications share one parser version and faces interoperate with FontData.
21pub use rustybuzz;
22pub use ttf_parser;
23
24/// Font loading and shaping boundary used by engine profiles.
25pub trait FontSystem {
26    /// Load font data that matches the query.
27    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError>;
28
29    /// Shape text into positioned glyphs.
30    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError>;
31}
32
33/// Font loading half of a font system.
34pub trait FontLoader {
35    /// Load font data that matches the query.
36    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError>;
37}
38
39/// Text shaping half of a font system, decoupled from any concrete shaper crate.
40pub trait TextShaper {
41    /// Shape text into positioned glyphs.
42    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError>;
43}
44
45impl<T> FontSystem for &T
46where
47    T: FontSystem,
48{
49    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
50        (*self).load_font(query)
51    }
52
53    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
54        (*self).shape_text(request)
55    }
56}
57
58impl<T> FontLoader for &T
59where
60    T: FontLoader,
61{
62    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
63        (*self).load_font(query)
64    }
65}
66
67impl<T> TextShaper for &T
68where
69    T: TextShaper,
70{
71    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
72        (*self).shape_text(request)
73    }
74}
75
76/// Font system assembled from independent loading and shaping services.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct ComposedFontSystem<L, S> {
79    loader: L,
80    shaper: S,
81}
82
83impl<L, S> ComposedFontSystem<L, S> {
84    /// Create a font system from a loader and shaper.
85    #[must_use]
86    pub fn new(loader: L, shaper: S) -> Self {
87        Self { loader, shaper }
88    }
89
90    /// Borrow the font loader.
91    #[must_use]
92    pub fn loader(&self) -> &L {
93        &self.loader
94    }
95
96    /// Borrow the text shaper.
97    #[must_use]
98    pub fn shaper(&self) -> &S {
99        &self.shaper
100    }
101
102    /// Split this font system into its loader and shaper.
103    #[must_use]
104    pub fn into_parts(self) -> (L, S) {
105        (self.loader, self.shaper)
106    }
107}
108
109impl<L, S> FontSystem for ComposedFontSystem<L, S>
110where
111    L: FontLoader,
112    S: TextShaper,
113{
114    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
115        self.loader.load_font(query)
116    }
117
118    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
119        self.shaper.shape_text(request)
120    }
121}
122
123/// Font system backed by `ttf-parser` and `rustybuzz`, with font bytes cached by [`FontId`].
124#[derive(Debug)]
125pub struct RustybuzzFontSystem<L> {
126    loader: L,
127    loaded_fonts: RefCell<BTreeMap<u32, CachedFont>>,
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
131struct CachedFont {
132    data: FontData,
133    size: Length,
134}
135
136impl<L> RustybuzzFontSystem<L> {
137    /// Create a Rustybuzz font system from a loader.
138    #[must_use]
139    pub fn new(loader: L) -> Self {
140        Self {
141            loader,
142            loaded_fonts: RefCell::new(BTreeMap::new()),
143        }
144    }
145
146    /// Borrow the wrapped loader.
147    #[must_use]
148    pub fn loader(&self) -> &L {
149        &self.loader
150    }
151
152    /// Return the wrapped loader.
153    #[must_use]
154    pub fn into_loader(self) -> L {
155        self.loader
156    }
157}
158
159impl<L> FontSystem for RustybuzzFontSystem<L>
160where
161    L: FontLoader,
162{
163    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
164        let font = self.loader.load_font(query)?;
165        validate_font(&font)?;
166        self.loaded_fonts.borrow_mut().insert(
167            font.id.0,
168            CachedFont {
169                data: font.clone(),
170                size: query.size,
171            },
172        );
173        Ok(font)
174    }
175
176    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
177        let cached = self
178            .loaded_fonts
179            .borrow()
180            .get(&request.font.0)
181            .cloned()
182            .ok_or_else(|| FontError::NotFound {
183                family: format!("font id {}", request.font.0),
184            })?;
185        shape_with_rustybuzz(&cached, request)
186    }
187}
188
189impl<L> FontLoader for RustybuzzFontSystem<L>
190where
191    L: FontLoader,
192{
193    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
194        FontSystem::load_font(self, query)
195    }
196}
197
198impl<L> TextShaper for RustybuzzFontSystem<L>
199where
200    L: FontLoader,
201{
202    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
203        FontSystem::shape_text(self, request)
204    }
205}
206
207/// Font lookup request.
208#[derive(Clone, Debug, Default, PartialEq, Eq)]
209pub struct FontQuery {
210    /// Family name as seen by the font resolver.
211    pub family: String,
212    /// Size in scaled points.
213    pub size: Length,
214    /// Whether math font tables are required.
215    pub math: bool,
216}
217
218/// Corner of an OpenType `MathKernInfo` record (mirrors HarfBuzz `hb_ot_math_kern_t`).
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub enum MathKernCorner {
221    /// Superscript on the right.
222    TopRight,
223    /// Superscript on the left.
224    TopLeft,
225    /// Subscript on the right.
226    BottomRight,
227    /// Subscript on the left.
228    BottomLeft,
229}
230
231/// One part of an OpenType math glyph assembly, with all measurements in scaled points.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub struct MathAssemblyPart {
234    /// Glyph id for this assembly part.
235    pub glyph: u32,
236    /// Start connector overlap at the leading edge, in scaled points.
237    pub start_connector: i32,
238    /// End connector overlap at the trailing edge, in scaled points.
239    pub end_connector: i32,
240    /// Full advance along the assembly axis, in scaled points.
241    pub full_advance: i32,
242    /// Whether this part is an extender (repeatable to reach the target size).
243    pub extender: bool,
244}
245
246/// Face the application parsed and owns; the library borrows it and never parses or copies.
247pub trait SharedFace: Send + Sync {
248    /// Returns the rustybuzz face the application parsed.
249    fn rustybuzz_face(&self) -> &rustybuzz::Face<'_>;
250
251    /// Returns the ttf view of the face; defaults to the ttf face inside the rustybuzz face.
252    fn ttf_face(&self) -> &ttf_parser::Face<'_> {
253        self.rustybuzz_face()
254    }
255}
256
257/// Where the parsed faces come from: library cached bytes, or a face the application owns.
258enum FaceSource {
259    Bytes {
260        bytes: Arc<[u8]>,
261        // Parse caches are shared across clones, so a face is parsed once per font, not once per clone.
262        ttf: Arc<once_cell::race::OnceBox<ParsedFace>>,
263        rustybuzz: Arc<once_cell::race::OnceBox<ParsedRustybuzzFace>>,
264    },
265    Shared(Arc<dyn SharedFace>),
266}
267
268impl Clone for FaceSource {
269    fn clone(&self) -> Self {
270        match self {
271            Self::Bytes {
272                bytes,
273                ttf,
274                rustybuzz,
275            } => Self::Bytes {
276                bytes: Arc::clone(bytes),
277                ttf: Arc::clone(ttf),
278                rustybuzz: Arc::clone(rustybuzz),
279            },
280            Self::Shared(face) => Self::Shared(Arc::clone(face)),
281        }
282    }
283}
284
285/// Loaded font identity plus its face source.
286pub struct FontData {
287    /// Stable font identity.
288    pub id: FontId,
289    /// Canonical font family or face name.
290    pub canonical_name: String,
291    source: FaceSource,
292}
293
294impl Clone for FontData {
295    fn clone(&self) -> Self {
296        Self {
297            id: self.id,
298            canonical_name: self.canonical_name.clone(),
299            source: self.source.clone(),
300        }
301    }
302}
303
304impl PartialEq for FontData {
305    fn eq(&self, other: &Self) -> bool {
306        let sources_equal = match (&self.source, &other.source) {
307            (FaceSource::Bytes { bytes: a, .. }, FaceSource::Bytes { bytes: b, .. }) => a == b,
308            (FaceSource::Shared(a), FaceSource::Shared(b)) => Arc::ptr_eq(a, b),
309            _ => false,
310        };
311        self.id == other.id && self.canonical_name == other.canonical_name && sources_equal
312    }
313}
314
315impl Eq for FontData {}
316
317impl core::fmt::Debug for FontData {
318    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
319        f.debug_struct("FontData")
320            .field("id", &self.id)
321            .field("canonical_name", &self.canonical_name)
322            .finish_non_exhaustive()
323    }
324}
325
326impl FontData {
327    /// Create font data from an id, canonical name, and raw bytes; the library parses lazily, once.
328    #[must_use]
329    pub fn new(id: FontId, canonical_name: impl Into<String>, bytes: impl Into<Arc<[u8]>>) -> Self {
330        Self {
331            id,
332            canonical_name: canonical_name.into(),
333            source: FaceSource::Bytes {
334                bytes: bytes.into(),
335                ttf: Arc::new(once_cell::race::OnceBox::new()),
336                rustybuzz: Arc::new(once_cell::race::OnceBox::new()),
337            },
338        }
339    }
340
341    /// Create font data from a face the application already parsed; the library never parses it.
342    #[must_use]
343    pub fn from_shared_face(
344        id: FontId,
345        canonical_name: impl Into<String>,
346        face: Arc<dyn SharedFace>,
347    ) -> Self {
348        Self {
349            id,
350            canonical_name: canonical_name.into(),
351            source: FaceSource::Shared(face),
352        }
353    }
354
355    /// Returns the raw bytes when the library owns them; None for application shared faces.
356    #[must_use]
357    pub fn bytes(&self) -> Option<&Arc<[u8]>> {
358        match &self.source {
359            FaceSource::Bytes { bytes, .. } => Some(bytes),
360            FaceSource::Shared(_) => None,
361        }
362    }
363
364    /// Runs `f` with the ttf face; parsed at most once, shared across clones.
365    pub fn with_ttf_face<R>(
366        &self,
367        f: impl FnOnce(&ttf_parser::Face<'_>) -> R,
368    ) -> Result<R, FontError> {
369        Ok(f(parse_face(self)?))
370    }
371
372    /// Runs `f` with the rustybuzz face; parsed at most once, shared across clones and with shaping.
373    pub fn with_rustybuzz_face<R>(
374        &self,
375        f: impl FnOnce(&rustybuzz::Face<'_>) -> R,
376    ) -> Result<R, FontError> {
377        Ok(f(parse_rustybuzz_face(self)?))
378    }
379
380    /// Parse overall font metrics at the requested size.
381    pub fn metrics(&self, size: Length) -> Result<FontMetrics, FontError> {
382        let face = parse_face(self)?;
383        let units_per_em = i32::from(face.units_per_em()).max(1);
384        Ok(FontMetrics {
385            ascent: scale_font_units(i32::from(face.ascender()), size, units_per_em),
386            descent: scale_font_units(i32::from(face.descender()), size, units_per_em),
387            xheight: scale_font_units(i32::from(face.x_height().unwrap_or(0)), size, units_per_em),
388            capheight: scale_font_units(
389                i32::from(face.capital_height().unwrap_or(0)),
390                size,
391                units_per_em,
392            ),
393            slant: (face.italic_angle() * 65_536.0) as i32,
394        })
395    }
396
397    /// Return metrics for a glyph at the requested size.
398    pub fn glyph_metrics(
399        &self,
400        glyph: GlyphId,
401        size: Length,
402    ) -> Result<FontGlyphMetrics, FontError> {
403        let face = parse_face(self)?;
404        let units_per_em = i32::from(face.units_per_em()).max(1);
405        let Ok(glyph_id) = u16::try_from(glyph.0) else {
406            return Ok(FontGlyphMetrics::default());
407        };
408        let glyph_id = ttf_parser::GlyphId(glyph_id);
409        let width = face
410            .glyph_hor_advance(glyph_id)
411            .map(|advance| scale_font_units(i32::from(advance), size, units_per_em))
412            .unwrap_or(0);
413        let (height, depth) = face
414            .glyph_bounding_box(glyph_id)
415            .map(|bbox| {
416                (
417                    scale_font_units(i32::from(bbox.y_max).max(0), size, units_per_em),
418                    scale_font_units((-i32::from(bbox.y_min)).max(0), size, units_per_em),
419                )
420            })
421            .unwrap_or((0, 0));
422        Ok(FontGlyphMetrics {
423            width,
424            height,
425            depth,
426        })
427    }
428
429    /// Return outlines in font design units with y pointing up, one entry per input glyph.
430    pub fn glyph_outlines(
431        &self,
432        glyphs: &[GlyphId],
433    ) -> Result<Vec<Option<GlyphOutline>>, FontError> {
434        let face = parse_face(self)?;
435        let units_per_em = face.units_per_em();
436        let mut out = Vec::with_capacity(glyphs.len());
437        for glyph in glyphs.iter().copied() {
438            let Ok(glyph_id) = u16::try_from(glyph.0) else {
439                out.push(None);
440                continue;
441            };
442            let mut collector = OutlineCollector {
443                commands: Vec::new(),
444            };
445            // `outline_glyph` returns `None` for blank or bitmap only glyphs; treat as absent.
446            if face
447                .outline_glyph(ttf_parser::GlyphId(glyph_id), &mut collector)
448                .is_none()
449            {
450                out.push(None);
451            } else {
452                out.push(Some(GlyphOutline {
453                    units_per_em,
454                    commands: collector.commands,
455                }));
456            }
457        }
458        Ok(out)
459    }
460
461    /// Look up a glyph id by Unicode codepoint.
462    pub fn glyph_index(&self, codepoint: char) -> Result<Option<GlyphId>, FontError> {
463        let face = parse_face(self)?;
464        Ok(face
465            .glyph_index(codepoint)
466            .map(|glyph| GlyphId(u32::from(glyph.0))))
467    }
468
469    /// Look up a glyph id by glyph name.
470    pub fn glyph_index_by_name(&self, name: &str) -> Result<Option<GlyphId>, FontError> {
471        let face = parse_face(self)?;
472        Ok(face
473            .glyph_index_by_name(name)
474            .map(|glyph| GlyphId(u32::from(glyph.0))))
475    }
476
477    /// Report whether the font has an OpenType math table.
478    pub fn has_opentype_math(&self) -> Result<bool, FontError> {
479        Ok(parse_face(self)?.tables().math.is_some())
480    }
481
482    /// Number of glyphs in the font (`\XeTeXcountglyphs`).
483    pub fn ot_glyph_count(&self) -> Result<u32, FontError> {
484        Ok(u32::from(parse_face(self)?.number_of_glyphs()))
485    }
486
487    /// Number of OpenType layout scripts from the larger layout script list.
488    pub fn ot_script_count(&self) -> Result<u32, FontError> {
489        let face = parse_face(self)?;
490        Ok(larger_script_list(face).map_or(0, |scripts| u32::from(scripts.len())))
491    }
492
493    /// OpenType script tag at `index`, packed big endian to match XeTeX `hb_tag_t` encoding.
494    pub fn ot_script_tag(&self, index: u32) -> Result<u32, FontError> {
495        let Ok(index) = u16::try_from(index) else {
496            return Ok(0);
497        };
498        let face = parse_face(self)?;
499        Ok(larger_script_list(face)
500            .and_then(|scripts| scripts.get(index))
501            .map_or(0, |script| script.tag.0))
502    }
503
504    /// Number of languages under `script_tag`, summed across OpenType layout tables.
505    pub fn ot_language_count(&self, script_tag: u32) -> Result<u32, FontError> {
506        let face = parse_face(self)?;
507        let script_tag = ttf_parser::Tag(script_tag);
508        let mut count = 0u32;
509        for table in [face.tables().gsub, face.tables().gpos].into_iter().flatten() {
510            if let Some(script) = table
511                .scripts
512                .index(script_tag)
513                .and_then(|index| table.scripts.get(index))
514            {
515                count += u32::from(script.languages.len());
516            }
517        }
518        Ok(count)
519    }
520
521    /// Language tag at `index` under `script_tag` (`\XeTeXOTlanguagetag`).
522    pub fn ot_language_tag(&self, script_tag: u32, index: u32) -> Result<u32, FontError> {
523        let Ok(index) = u16::try_from(index) else {
524            return Ok(0);
525        };
526        let face = parse_face(self)?;
527        let script_tag = ttf_parser::Tag(script_tag);
528        for table in [face.tables().gsub, face.tables().gpos].into_iter().flatten() {
529            if let Some(script) = table
530                .scripts
531                .index(script_tag)
532                .and_then(|script_index| table.scripts.get(script_index))
533            {
534                if index < script.languages.len() {
535                    return Ok(script.languages.get(index).map_or(0, |lang| lang.tag.0));
536                }
537            }
538        }
539        Ok(0)
540    }
541
542    /// Number of features under `script_tag` and `language_tag`, summed across OpenType layout tables.
543    pub fn ot_feature_count(&self, script_tag: u32, language_tag: u32) -> Result<u32, FontError> {
544        let face = parse_face(self)?;
545        let script_tag = ttf_parser::Tag(script_tag);
546        let mut count = 0u32;
547        for table in [face.tables().gsub, face.tables().gpos].into_iter().flatten() {
548            if let Some(langsys) = language_system(table, script_tag, language_tag) {
549                count += u32::from(langsys.feature_indices.len());
550            }
551        }
552        Ok(count)
553    }
554
555    /// Feature tag at `index` under `script_tag`/`language_tag` (`\XeTeXOTfeaturetag`).
556    pub fn ot_feature_tag(
557        &self,
558        script_tag: u32,
559        language_tag: u32,
560        index: u32,
561    ) -> Result<u32, FontError> {
562        let Ok(index) = u16::try_from(index) else {
563            return Ok(0);
564        };
565        let face = parse_face(self)?;
566        let script_tag = ttf_parser::Tag(script_tag);
567        for table in [face.tables().gsub, face.tables().gpos].into_iter().flatten() {
568            if let Some(langsys) = language_system(table, script_tag, language_tag) {
569                if index < langsys.feature_indices.len() {
570                    if let Some(feature_index) = langsys.feature_indices.get(index) {
571                        return Ok(table
572                            .features
573                            .get(feature_index)
574                            .map_or(0, |feature| feature.tag.0));
575                    }
576                }
577            }
578        }
579        Ok(0)
580    }
581
582    /// OpenType math constant by XeTeX and HarfBuzz constant index.
583    pub fn opentype_math_constant(&self, constant: i32, size: Length) -> Result<i32, FontError> {
584        let face = parse_face(self)?;
585        let Some(constants) = face.tables().math.and_then(|table| table.constants) else {
586            return Ok(0);
587        };
588        let units_per_em = i32::from(face.units_per_em()).max(1);
589        let Some(value) = math_constant_value(constants, constant) else {
590            return Ok(0);
591        };
592        if is_math_constant_percentage(constant) {
593            Ok(value)
594        } else {
595            Ok(scale_font_units(value, size, units_per_em))
596        }
597    }
598
599    /// OpenType math italic correction for a glyph, scaled to scaled points.
600    pub fn math_italic_correction(&self, glyph: GlyphId, size: Length) -> Result<i32, FontError> {
601        let face = parse_face(self)?;
602        let units_per_em = i32::from(face.units_per_em()).max(1);
603        let Ok(glyph_id) = u16::try_from(glyph.0) else {
604            return Ok(0);
605        };
606        let Some(corrections) = face
607            .tables()
608            .math
609            .and_then(|table| table.glyph_info)
610            .and_then(|info| info.italic_corrections)
611        else {
612            return Ok(0);
613        };
614        let Some(value) = corrections.get(ttf_parser::GlyphId(glyph_id)) else {
615            return Ok(0);
616        };
617        Ok(scale_font_units(i32::from(value.value), size, units_per_em))
618    }
619
620    /// Evaluate one OpenType math `MathKernInfo` corner at a correction height in font design units.
621    pub fn math_kern_at(
622        &self,
623        glyph: GlyphId,
624        corner: MathKernCorner,
625        correction_height: i32,
626    ) -> Result<i32, FontError> {
627        let face = parse_face(self)?;
628        let Ok(glyph_id) = u16::try_from(glyph.0) else {
629            return Ok(0);
630        };
631        let Some(kern_info) = face
632            .tables()
633            .math
634            .and_then(|table| table.glyph_info)
635            .and_then(|info| info.kern_infos)
636            .and_then(|infos| infos.get(ttf_parser::GlyphId(glyph_id)))
637        else {
638            return Ok(0);
639        };
640        let kern = match corner {
641            MathKernCorner::TopRight => kern_info.top_right,
642            MathKernCorner::TopLeft => kern_info.top_left,
643            MathKernCorner::BottomRight => kern_info.bottom_right,
644            MathKernCorner::BottomLeft => kern_info.bottom_left,
645        };
646        let Some(kern) = kern else {
647            return Ok(0);
648        };
649        let count = kern.count();
650        let mut i = 0u16;
651        while i < count {
652            match kern.height(i) {
653                Some(h) if correction_height < i32::from(h.value) => break,
654                _ => i += 1,
655            }
656        }
657        Ok(kern.kern(i).map(|v| i32::from(v.value)).unwrap_or(0))
658    }
659
660    /// Return the larger OpenType math glyph variant at `index`, with its advance scaled to points.
661    pub fn math_variant(
662        &self,
663        glyph: GlyphId,
664        index: u16,
665        horizontal: bool,
666        size: Length,
667    ) -> Result<Option<(u32, i32)>, FontError> {
668        let face = parse_face(self)?;
669        let units_per_em = i32::from(face.units_per_em()).max(1);
670        let Ok(glyph_id) = u16::try_from(glyph.0) else {
671            return Ok(None);
672        };
673        let Some(variants) = face.tables().math.and_then(|table| table.variants) else {
674            return Ok(None);
675        };
676        let constructions = if horizontal {
677            variants.horizontal_constructions
678        } else {
679            variants.vertical_constructions
680        };
681        let Some(construction) = constructions.get(ttf_parser::GlyphId(glyph_id)) else {
682            return Ok(None);
683        };
684        let Some(variant) = construction.variants.get(index) else {
685            return Ok(None);
686        };
687        let advance = scale_font_units(
688            i32::from(variant.advance_measurement),
689            size,
690            units_per_em,
691        );
692        Ok(Some((u32::from(variant.variant_glyph.0), advance)))
693    }
694
695    /// OpenType math glyph assembly parts, each metric scaled to scaled points.
696    pub fn math_assembly(
697        &self,
698        glyph: GlyphId,
699        horizontal: bool,
700        size: Length,
701    ) -> Result<Vec<MathAssemblyPart>, FontError> {
702        let face = parse_face(self)?;
703        let units_per_em = i32::from(face.units_per_em()).max(1);
704        let Ok(glyph_id) = u16::try_from(glyph.0) else {
705            return Ok(Vec::new());
706        };
707        let Some(variants) = face.tables().math.and_then(|table| table.variants) else {
708            return Ok(Vec::new());
709        };
710        let constructions = if horizontal {
711            variants.horizontal_constructions
712        } else {
713            variants.vertical_constructions
714        };
715        let Some(assembly) = constructions
716            .get(ttf_parser::GlyphId(glyph_id))
717            .and_then(|construction| construction.assembly)
718        else {
719            return Ok(Vec::new());
720        };
721        let parts = assembly
722            .parts
723            .into_iter()
724            .map(|part| MathAssemblyPart {
725                glyph: u32::from(part.glyph_id.0),
726                start_connector: scale_font_units(
727                    i32::from(part.start_connector_length),
728                    size,
729                    units_per_em,
730                ),
731                end_connector: scale_font_units(
732                    i32::from(part.end_connector_length),
733                    size,
734                    units_per_em,
735                ),
736                full_advance: scale_font_units(
737                    i32::from(part.full_advance),
738                    size,
739                    units_per_em,
740                ),
741                extender: part.part_flags.extender(),
742            })
743            .collect();
744        Ok(parts)
745    }
746
747    /// OpenType math minimum connector overlap between assembly parts, in scaled points.
748    pub fn math_min_connector_overlap(&self, size: Length) -> Result<i32, FontError> {
749        let face = parse_face(self)?;
750        let units_per_em = i32::from(face.units_per_em()).max(1);
751        let overlap = face
752            .tables()
753            .math
754            .and_then(|table| table.variants)
755            .map(|variants| i32::from(variants.min_connector_overlap))
756            .unwrap_or(0);
757        Ok(scale_font_units(overlap, size, units_per_em))
758    }
759
760    /// Convert points to font design units using XeTeX `pointsToUnits` in `f32`.
761    pub fn points_to_units(&self, points: f32, size: Length) -> Result<f32, FontError> {
762        let face = parse_face(self)?;
763        let units_per_em = i32::from(face.units_per_em()).max(1);
764        let point_size = (f64::from(size.0) / 65536.0) as f32;
765        if point_size == 0.0 {
766            return Ok(0.0);
767        }
768        Ok((points * units_per_em as f32) / point_size)
769    }
770
771    /// Convert font design units to scaled points using XeTeX `D2Fix(unitsToPoints(...))`.
772    pub fn units_to_scaled(&self, units: i32, size: Length) -> Result<i32, FontError> {
773        let face = parse_face(self)?;
774        let units_per_em = i32::from(face.units_per_em()).max(1);
775        Ok(scale_font_units(units, size, units_per_em))
776    }
777
778    /// Top accent attachment position for `glyph`, scaled to scaled points (`\XeTeXmathaccent`).
779    pub fn opentype_math_accent_position(
780        &self,
781        glyph: GlyphId,
782        size: Length,
783    ) -> Result<i32, FontError> {
784        let face = parse_face(self)?;
785        let units_per_em = i32::from(face.units_per_em()).max(1);
786        let Ok(glyph_id) = u16::try_from(glyph.0) else {
787            return Ok(0);
788        };
789        let Some(attachments) = face
790            .tables()
791            .math
792            .and_then(|table| table.glyph_info)
793            .and_then(|info| info.top_accent_attachments)
794        else {
795            return Ok(0);
796        };
797        let Some(value) = attachments.get(ttf_parser::GlyphId(glyph_id)) else {
798            return Ok(0);
799        };
800        Ok(scale_font_units(i32::from(value.value), size, units_per_em))
801    }
802
803    /// Return the symbol font parameter at `parameter` index, mapping to OpenType math constants.
804    pub fn math_symbol_parameter(&self, parameter: i32, size: Length) -> Result<i32, FontError> {
805        match parameter {
806            5 => self.opentype_math_constant(6, size),
807            6 => Ok(size.0),
808            8 => self.opentype_math_constant(33, size),
809            9 => self.opentype_math_constant(32, size),
810            10 => self.opentype_math_constant(22, size),
811            11 => self.opentype_math_constant(35, size),
812            12 => self.opentype_math_constant(34, size),
813            13 | 14 => self.opentype_math_constant(11, size),
814            15 => self.opentype_math_constant(12, size),
815            16 | 17 => self.opentype_math_constant(8, size),
816            18 => self.opentype_math_constant(14, size),
817            19 => self.opentype_math_constant(10, size),
818            20 => self.opentype_math_constant(2, size),
819            21 => {
820                let delim1 = self.math_symbol_parameter(20, size)?;
821                Ok(((i64::from(size.0) * 3) / 2)
822                    .min(i64::from(delim1))
823                    .clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32)
824            }
825            22 => self.opentype_math_constant(5, size),
826            _ => Ok(0),
827        }
828    }
829
830    /// Return the extension font parameter at `parameter` index, mapping to OpenType math constants.
831    pub fn math_extension_parameter(&self, parameter: i32, size: Length) -> Result<i32, FontError> {
832        match parameter {
833            5 => self.opentype_math_constant(6, size),
834            6 => Ok(size.0),
835            8 => self.opentype_math_constant(38, size),
836            9 => self.opentype_math_constant(18, size),
837            10 => self.opentype_math_constant(20, size),
838            11 => self.opentype_math_constant(19, size),
839            12 => self.opentype_math_constant(21, size),
840            13 => self.opentype_math_constant(26, size),
841            _ => Ok(0),
842        }
843    }
844}
845
846/// Overall font metrics in TeX scaled point units.
847#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
848pub struct FontMetrics {
849    /// Distance from baseline to top of typical ascenders.
850    pub ascent: i32,
851    /// Distance from baseline to bottom of typical descenders (negative).
852    pub descent: i32,
853    /// Height of a lowercase x.
854    pub xheight: i32,
855    /// Height of a capital letter.
856    pub capheight: i32,
857    /// Italic slant as a fixed point angle.
858    pub slant: i32,
859}
860
861/// Metrics for a single glyph in TeX scaled point units.
862#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
863pub struct FontGlyphMetrics {
864    /// Horizontal advance width.
865    pub width: i32,
866    /// Height above baseline.
867    pub height: i32,
868    /// Depth below baseline.
869    pub depth: i32,
870}
871
872/// OpenType feature request (tag + value), e.g. `ssty=1` for math script size substitution.
873#[derive(Clone, Copy, Debug, PartialEq, Eq)]
874pub struct ShapeFeature {
875    /// Four byte OpenType feature tag, e.g. `*b"ssty"`.
876    pub tag: [u8; 4],
877    /// One based alternate index for `ssty`; `0` disables the feature.
878    pub value: u32,
879}
880
881/// Text shaping request.
882#[derive(Clone, Debug, PartialEq, Eq)]
883pub struct ShapeRequest<'a> {
884    /// Font to use for shaping, identified by its `FontId`.
885    pub font: FontId,
886    /// Input text to shape.
887    pub text: &'a str,
888    /// Text direction for the buffer.
889    pub direction: Direction,
890    /// Source span for cluster mapping.
891    pub source: Option<ByteSpan>,
892    /// OpenType script tag override, e.g. `*b"math"`, for features under the `math` script.
893    pub script: Option<[u8; 4]>,
894    /// OpenType features to apply during shaping, e.g. `ssty=1`.
895    pub features: Vec<ShapeFeature>,
896}
897
898/// Shaped text result.
899#[derive(Clone, Debug, Default, PartialEq, Eq)]
900pub struct ShapedText {
901    /// Glyphs in visual order.
902    pub glyphs: Vec<PositionedGlyph>,
903}
904
905impl ShapedText {
906    /// Construct a `ShapedText` from a glyph vec.
907    #[must_use]
908    pub fn new(glyphs: impl Into<Vec<PositionedGlyph>>) -> Self {
909        Self {
910            glyphs: glyphs.into(),
911        }
912    }
913}
914
915/// Font system failure.
916#[derive(Clone, Debug, PartialEq, Eq)]
917#[non_exhaustive]
918pub enum FontError {
919    /// Font was not found by the loader.
920    NotFound {
921        /// Family name that was requested.
922        family: String,
923    },
924    /// Font exists but cannot satisfy the request.
925    Invalid {
926        /// Family name.
927        family: String,
928        /// Reason the font is unusable.
929        message: String,
930    },
931    /// Text shaping is not supported by this font system.
932    ShapingUnsupported {
933        /// Reason shaping is unsupported.
934        message: String,
935    },
936}
937
938/// Alias so the self_cell dependent type names its lifetime explicitly.
939type TtfFace<'a> = ttf_parser::Face<'a>;
940
941self_cell::self_cell!(
942    /// Owns `Arc<[u8]>` together with the `ttf_parser::Face` parsed from it.
943    struct ParsedFace {
944        owner: Arc<[u8]>,
945        #[covariant]
946        dependent: TtfFace,
947    }
948);
949
950/// Alias so the self_cell dependent type names its lifetime explicitly.
951type RbFace<'a> = rustybuzz::Face<'a>;
952
953self_cell::self_cell!(
954    /// Owns `Arc<[u8]>` together with the `rustybuzz::Face` parsed from it.
955    struct ParsedRustybuzzFace {
956        owner: Arc<[u8]>,
957        #[covariant]
958        dependent: RbFace,
959    }
960);
961
962/// Returns the rustybuzz face: the application face when shared, one cached parse otherwise.
963fn parse_rustybuzz_face(font: &FontData) -> Result<&rustybuzz::Face<'_>, FontError> {
964    match &font.source {
965        FaceSource::Shared(face) => Ok(face.rustybuzz_face()),
966        FaceSource::Bytes {
967            bytes, rustybuzz, ..
968        } => {
969            if rustybuzz.get().is_none() {
970                let parsed = ParsedRustybuzzFace::try_new(Arc::clone(bytes), |bytes| {
971                    rustybuzz::Face::from_slice(bytes, 0).ok_or_else(|| FontError::Invalid {
972                        family: font.canonical_name.clone(),
973                        message: "invalid font data".to_string(),
974                    })
975                })?;
976                let _ = rustybuzz.set(alloc::boxed::Box::new(parsed));
977            }
978            Ok(rustybuzz
979                .get()
980                .expect("rustybuzz cache populated above")
981                .borrow_dependent())
982        }
983    }
984}
985
986fn validate_font(font: &FontData) -> Result<(), FontError> {
987    parse_face(font).map(|_| ())
988}
989
990/// Larger OpenType layout script list, mirroring XeTeX `getLargerScriptListTable`.
991fn larger_script_list<'a>(
992    face: &ttf_parser::Face<'a>,
993) -> Option<ttf_parser::opentype_layout::ScriptList<'a>> {
994    let gsub = face.tables().gsub.map(|table| table.scripts);
995    let gpos = face.tables().gpos.map(|table| table.scripts);
996    match (gsub, gpos) {
997        (Some(sub), Some(pos)) => Some(if pos.len() > sub.len() { pos } else { sub }),
998        (sub, pos) => sub.or(pos),
999    }
1000}
1001
1002/// Resolve a language system; `language_tag == 0` selects the script default.
1003fn language_system<'a>(
1004    table: ttf_parser::opentype_layout::LayoutTable<'a>,
1005    script_tag: ttf_parser::Tag,
1006    language_tag: u32,
1007) -> Option<ttf_parser::opentype_layout::LanguageSystem<'a>> {
1008    let script = table
1009        .scripts
1010        .index(script_tag)
1011        .and_then(|index| table.scripts.get(index))?;
1012    if language_tag == 0 {
1013        script.default_language
1014    } else {
1015        script
1016            .languages
1017            .index(ttf_parser::Tag(language_tag))
1018            .and_then(|index| script.languages.get(index))
1019            .or(script.default_language)
1020    }
1021}
1022
1023/// Returns the ttf face: the application face when shared, one cached parse otherwise.
1024fn parse_face(font: &FontData) -> Result<&ttf_parser::Face<'_>, FontError> {
1025    let invalid = |error: ttf_parser::FaceParsingError| FontError::Invalid {
1026        family: font.canonical_name.clone(),
1027        message: format!("invalid font data: {error:?}"),
1028    };
1029
1030    match &font.source {
1031        FaceSource::Shared(face) => Ok(face.ttf_face()),
1032        FaceSource::Bytes { bytes, ttf, .. } => {
1033            if ttf.get().is_none() {
1034                let parsed = ParsedFace::try_new(Arc::clone(bytes), |bytes| {
1035                    ttf_parser::Face::parse(bytes, 0).map_err(invalid)
1036                })?;
1037                // `set` only fails when another initializer raced; `get` is populated either way.
1038                let _ = ttf.set(alloc::boxed::Box::new(parsed));
1039            }
1040            Ok(ttf.get().expect("ttf cache populated above").borrow_dependent())
1041        }
1042    }
1043}
1044
1045/// Collects `ttf_parser` contour callbacks into [`OutlineCommand`]s.
1046struct OutlineCollector {
1047    commands: Vec<OutlineCommand>,
1048}
1049
1050impl ttf_parser::OutlineBuilder for OutlineCollector {
1051    fn move_to(&mut self, x: f32, y: f32) {
1052        self.commands.push(OutlineCommand::MoveTo { x, y });
1053    }
1054
1055    fn line_to(&mut self, x: f32, y: f32) {
1056        self.commands.push(OutlineCommand::LineTo { x, y });
1057    }
1058
1059    fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
1060        self.commands.push(OutlineCommand::QuadTo { cx, cy, x, y });
1061    }
1062
1063    fn curve_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) {
1064        self.commands.push(OutlineCommand::CurveTo {
1065            c1x,
1066            c1y,
1067            c2x,
1068            c2y,
1069            x,
1070            y,
1071        });
1072    }
1073
1074    fn close(&mut self) {
1075        self.commands.push(OutlineCommand::Close);
1076    }
1077}
1078
1079fn math_constant_value(constants: ttf_parser::math::Constants<'_>, constant: i32) -> Option<i32> {
1080    let value = match constant {
1081        0 => i32::from(constants.script_percent_scale_down()),
1082        1 => i32::from(constants.script_script_percent_scale_down()),
1083        2 => i32::from(constants.delimited_sub_formula_min_height()),
1084        3 => i32::from(constants.display_operator_min_height()),
1085        4 => i32::from(constants.math_leading().value),
1086        5 => i32::from(constants.axis_height().value),
1087        6 => i32::from(constants.accent_base_height().value),
1088        7 => i32::from(constants.flattened_accent_base_height().value),
1089        8 => i32::from(constants.subscript_shift_down().value),
1090        9 => i32::from(constants.subscript_top_max().value),
1091        10 => i32::from(constants.subscript_baseline_drop_min().value),
1092        11 => i32::from(constants.superscript_shift_up().value),
1093        12 => i32::from(constants.superscript_shift_up_cramped().value),
1094        13 => i32::from(constants.superscript_bottom_min().value),
1095        14 => i32::from(constants.superscript_baseline_drop_max().value),
1096        15 => i32::from(constants.sub_superscript_gap_min().value),
1097        16 => i32::from(constants.superscript_bottom_max_with_subscript().value),
1098        17 => i32::from(constants.space_after_script().value),
1099        18 => i32::from(constants.upper_limit_gap_min().value),
1100        19 => i32::from(constants.upper_limit_baseline_rise_min().value),
1101        20 => i32::from(constants.lower_limit_gap_min().value),
1102        21 => i32::from(constants.lower_limit_baseline_drop_min().value),
1103        22 => i32::from(constants.stack_top_shift_up().value),
1104        23 => i32::from(constants.stack_top_display_style_shift_up().value),
1105        24 => i32::from(constants.stack_bottom_shift_down().value),
1106        25 => i32::from(constants.stack_bottom_display_style_shift_down().value),
1107        26 => i32::from(constants.stack_gap_min().value),
1108        27 => i32::from(constants.stack_display_style_gap_min().value),
1109        28 => i32::from(constants.stretch_stack_top_shift_up().value),
1110        29 => i32::from(constants.stretch_stack_bottom_shift_down().value),
1111        30 => i32::from(constants.stretch_stack_gap_above_min().value),
1112        31 => i32::from(constants.stretch_stack_gap_below_min().value),
1113        32 => i32::from(constants.fraction_numerator_shift_up().value),
1114        33 => i32::from(constants.fraction_numerator_display_style_shift_up().value),
1115        34 => i32::from(constants.fraction_denominator_shift_down().value),
1116        35 => i32::from(
1117            constants
1118                .fraction_denominator_display_style_shift_down()
1119                .value,
1120        ),
1121        36 => i32::from(constants.fraction_numerator_gap_min().value),
1122        37 => i32::from(constants.fraction_num_display_style_gap_min().value),
1123        38 => i32::from(constants.fraction_rule_thickness().value),
1124        39 => i32::from(constants.fraction_denominator_gap_min().value),
1125        40 => i32::from(constants.fraction_denom_display_style_gap_min().value),
1126        41 => i32::from(constants.skewed_fraction_horizontal_gap().value),
1127        42 => i32::from(constants.skewed_fraction_vertical_gap().value),
1128        43 => i32::from(constants.overbar_vertical_gap().value),
1129        44 => i32::from(constants.overbar_rule_thickness().value),
1130        45 => i32::from(constants.overbar_extra_ascender().value),
1131        46 => i32::from(constants.underbar_vertical_gap().value),
1132        47 => i32::from(constants.underbar_rule_thickness().value),
1133        48 => i32::from(constants.underbar_extra_descender().value),
1134        49 => i32::from(constants.radical_vertical_gap().value),
1135        50 => i32::from(constants.radical_display_style_vertical_gap().value),
1136        51 => i32::from(constants.radical_rule_thickness().value),
1137        52 => i32::from(constants.radical_extra_ascender().value),
1138        53 => i32::from(constants.radical_kern_before_degree().value),
1139        54 => i32::from(constants.radical_kern_after_degree().value),
1140        55 => i32::from(constants.radical_degree_bottom_raise_percent()),
1141        _ => return None,
1142    };
1143    Some(value)
1144}
1145
1146fn is_math_constant_percentage(constant: i32) -> bool {
1147    matches!(constant, 0 | 1 | 55)
1148}
1149
1150fn shape_with_rustybuzz(
1151    cached: &CachedFont,
1152    request: &ShapeRequest<'_>,
1153) -> Result<ShapedText, FontError> {
1154    let face = parse_rustybuzz_face(&cached.data)?;
1155    let mut buffer = rustybuzz::UnicodeBuffer::new();
1156    buffer.push_str(request.text);
1157    buffer.set_direction(match request.direction {
1158        Direction::LeftToRight => rustybuzz::Direction::LeftToRight,
1159        Direction::RightToLeft => rustybuzz::Direction::RightToLeft,
1160        Direction::TopToBottom => rustybuzz::Direction::TopToBottom,
1161        _ => rustybuzz::Direction::LeftToRight,
1162    });
1163    buffer.guess_segment_properties();
1164    if let Some(script_tag) = request.script {
1165        // `from_iso15924_tag` maps `math` so rustybuzz selects the script with `ssty` lookups.
1166        if let Some(script) =
1167            rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(&script_tag))
1168        {
1169            buffer.set_script(script);
1170        }
1171    }
1172
1173    let features: Vec<rustybuzz::Feature> = request
1174        .features
1175        .iter()
1176        .map(|feature| {
1177            rustybuzz::Feature::new(ttf_parser::Tag::from_bytes(&feature.tag), feature.value, ..)
1178        })
1179        .collect();
1180    let shaped = rustybuzz::shape(face, &features, buffer);
1181    let infos = shaped.glyph_infos();
1182    let positions = shaped.glyph_positions();
1183    let units_per_em = face.units_per_em().max(1);
1184    let glyphs = infos
1185        .iter()
1186        .zip(positions.iter())
1187        .map(|(info, position)| PositionedGlyph {
1188            glyph_id: GlyphId(info.glyph_id),
1189            advance: Point {
1190                x: scaled_font_units(position.x_advance, cached.size, units_per_em),
1191                y: scaled_font_units(position.y_advance, cached.size, units_per_em),
1192            },
1193            offset: Point {
1194                x: scaled_font_units(position.x_offset, cached.size, units_per_em),
1195                y: scaled_font_units(position.y_offset, cached.size, units_per_em),
1196            },
1197            cluster: request
1198                .source
1199                .map(|source| cluster_source_span(request.text, source, info.cluster)),
1200        })
1201        .collect();
1202
1203    Ok(ShapedText { glyphs })
1204}
1205
1206fn scaled_font_units(value: i32, size: Length, units_per_em: i32) -> Length {
1207    Length::from_scaled_points(scale_font_units(value, size, units_per_em))
1208}
1209
1210    /// Scale font design units to scaled points, reproducing XeTeX arithmetic.
1211fn scale_font_units(value: i32, size: Length, units_per_em: i32) -> i32 {
1212    // m_pointSize is f32 in XeTeXFontInst; unitsToPoints runs in f32 before D2Fix promotes to f64.
1213    let point_size = (f64::from(size.0) / 65536.0) as f32;
1214    let points = (value as f32 * point_size) / (units_per_em.max(1) as f32);
1215    // D2Fix uses truncation because C integer casts truncate toward zero.
1216    let fixed = (f64::from(points) * 65536.0 + 0.5).trunc();
1217    fixed.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
1218}
1219
1220fn cluster_source_span(text: &str, source: ByteSpan, cluster: u32) -> ByteSpan {
1221    let start = usize::try_from(cluster)
1222        .ok()
1223        .map(|cluster| cluster.min(text.len()))
1224        .unwrap_or(text.len());
1225    let mut end = text.len();
1226    for (index, _) in text.char_indices() {
1227        if index > start {
1228            end = index;
1229            break;
1230        }
1231    }
1232    ByteSpan {
1233        start: source.start.saturating_add(start as u32),
1234        end: source.start.saturating_add(end as u32).min(source.end),
1235    }
1236}
1237
1238/// Deterministic empty font system.
1239#[derive(Clone, Copy, Debug, Default)]
1240pub struct NoFontSystem;
1241
1242impl FontSystem for NoFontSystem {
1243    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
1244        Err(FontError::NotFound {
1245            family: query.family.clone(),
1246        })
1247    }
1248
1249    fn shape_text(&self, _request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
1250        Err(FontError::ShapingUnsupported {
1251            message: "no font shaper configured".to_string(),
1252        })
1253    }
1254}
1255
1256/// Font system backed by an in process font map, for embedded and browser environments.
1257#[derive(Clone, Debug, Default, PartialEq, Eq)]
1258pub struct InMemoryFontSystem {
1259    fonts: BTreeMap<String, FontData>,
1260    shape_without_native_engine: bool,
1261}
1262
1263impl InMemoryFontSystem {
1264    /// Create an empty in memory font system.
1265    #[must_use]
1266    pub fn new() -> Self {
1267        Self::default()
1268    }
1269
1270    /// Enable deterministic fallback shaping for tests and bootstrap, without a native shaping engine.
1271    #[must_use]
1272    pub fn with_fallback_shaping(mut self) -> Self {
1273        self.shape_without_native_engine = true;
1274        self
1275    }
1276
1277    /// Add a font by family name and return `self`.
1278    #[must_use]
1279    pub fn with_font(mut self, family: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
1280        self.insert(family, bytes);
1281        self
1282    }
1283
1284    /// Insert or replace a font.
1285    pub fn insert(&mut self, family: impl Into<String>, bytes: impl Into<Vec<u8>>) {
1286        let family = family.into();
1287        let id = FontId(self.fonts.len() as u32);
1288        let bytes: Vec<u8> = bytes.into();
1289        self.fonts
1290            .insert(family.clone(), FontData::new(id, family, bytes));
1291    }
1292
1293    /// Add a font the application already constructed, sharing its parsed faces, and return `self`.
1294    #[must_use]
1295    pub fn with_font_data(mut self, font: FontData) -> Self {
1296        self.insert_font_data(font);
1297        self
1298    }
1299
1300    /// Insert or replace an application owned font keyed by its canonical name; parses stay shared.
1301    pub fn insert_font_data(&mut self, font: FontData) {
1302        self.fonts.insert(font.canonical_name.clone(), font);
1303    }
1304}
1305
1306impl FontSystem for InMemoryFontSystem {
1307    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
1308        self.fonts
1309            .get(&query.family)
1310            .cloned()
1311            .ok_or_else(|| FontError::NotFound {
1312                family: query.family.clone(),
1313            })
1314    }
1315
1316    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
1317        if !self.shape_without_native_engine {
1318            return Err(FontError::ShapingUnsupported {
1319                message: "fallback shaping is disabled".to_string(),
1320            });
1321        }
1322
1323        let glyphs = request
1324            .text
1325            .char_indices()
1326            .enumerate()
1327            .map(|(index, (byte_offset, ch))| {
1328                let start = request
1329                    .source
1330                    .map_or(byte_offset as u32, |span| span.start + byte_offset as u32);
1331                PositionedGlyph {
1332                    glyph_id: GlyphId(ch as u32),
1333                    offset: Point {
1334                        x: Length::from_scaled_points((index as i32) * 65_536),
1335                        y: Length::ZERO,
1336                    },
1337                    advance: Point {
1338                        x: Length::from_scaled_points(65_536),
1339                        y: Length::ZERO,
1340                    },
1341                    cluster: Some(ByteSpan {
1342                        start,
1343                        end: start + ch.len_utf8() as u32,
1344                    }),
1345                }
1346            })
1347            .collect();
1348
1349        Ok(ShapedText { glyphs })
1350    }
1351}
1352
1353impl FontLoader for InMemoryFontSystem {
1354    fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
1355        FontSystem::load_font(self, query)
1356    }
1357}
1358
1359impl TextShaper for InMemoryFontSystem {
1360    fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
1361        FontSystem::shape_text(self, request)
1362    }
1363}
1364
1365#[cfg(test)]
1366mod tests {
1367    use super::*;
1368
1369    const DEJAVU: &[u8] =
1370        include_bytes!("../../../vendor/texlive-source/libs/gd/libgd-src/tests/freetype/DejaVuSans.ttf");
1371
1372    #[test]
1373    fn font_data_clones_share_both_parsed_faces() {
1374        let font = FontData::new(FontId(7), "DejaVu Sans", DEJAVU.to_vec());
1375        let clone = font.clone();
1376
1377        let ttf_ptr = font
1378            .with_ttf_face(|face| face as *const ttf_parser::Face<'_> as usize)
1379            .expect("ttf parse");
1380        let clone_ttf_ptr = clone
1381            .with_ttf_face(|face| face as *const ttf_parser::Face<'_> as usize)
1382            .expect("ttf parse via clone");
1383        assert_eq!(ttf_ptr, clone_ttf_ptr, "clone must reuse the same ttf parse");
1384
1385        let rb_ptr = font
1386            .with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1387            .expect("rustybuzz parse");
1388        let clone_rb_ptr = clone
1389            .with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1390            .expect("rustybuzz parse via clone");
1391        assert_eq!(rb_ptr, clone_rb_ptr, "clone must reuse the same rustybuzz parse");
1392
1393        assert!(Arc::ptr_eq(
1394            font.bytes().expect("library owned bytes"),
1395            clone.bytes().expect("library owned bytes"),
1396        ));
1397    }
1398
1399    self_cell::self_cell!(
1400        struct AppOwnedFace {
1401            owner: Arc<[u8]>,
1402            #[covariant]
1403            dependent: RbFace,
1404        }
1405    );
1406
1407    struct AppShared(AppOwnedFace);
1408
1409    impl SharedFace for AppShared {
1410        fn rustybuzz_face(&self) -> &rustybuzz::Face<'_> {
1411            self.0.borrow_dependent()
1412        }
1413    }
1414
1415    #[test]
1416    fn application_owned_face_is_borrowed_and_never_parsed_by_the_library() {
1417        let bytes: Arc<[u8]> = DEJAVU.to_vec().into();
1418        let app_face = AppShared(
1419            AppOwnedFace::try_new(Arc::clone(&bytes), |bytes| {
1420                rustybuzz::Face::from_slice(bytes, 0).ok_or("app parse failed")
1421            })
1422            .expect("application parses its own font"),
1423        );
1424        let app_face: Arc<dyn SharedFace> = Arc::new(app_face);
1425        let app_ptr = app_face.rustybuzz_face() as *const rustybuzz::Face<'_> as usize;
1426
1427        let font = FontData::from_shared_face(FontId(3), "DejaVu Sans", Arc::clone(&app_face));
1428
1429        assert!(font.bytes().is_none(), "the library holds no bytes");
1430        let lib_ptr = font
1431            .with_rustybuzz_face(|face| face as *const rustybuzz::Face<'_> as usize)
1432            .expect("borrow shared face");
1433        assert_eq!(lib_ptr, app_ptr, "the library must borrow the application face");
1434
1435        let metrics = font.metrics(Length::from_scaled_points(655_360)).expect("metrics");
1436        assert!(metrics.ascent > 0);
1437        let glyph = font.glyph_index('A').expect("glyph lookup");
1438        assert!(glyph.is_some());
1439    }
1440
1441    #[test]
1442    fn font_data_is_send_and_sync() {
1443        fn assert_send_sync<T: Send + Sync>() {}
1444        assert_send_sync::<FontData>();
1445    }
1446
1447    #[test]
1448    fn fallback_shaping_preserves_source_clusters() {
1449        let fonts = InMemoryFontSystem::new().with_fallback_shaping();
1450        let shaped = FontSystem::shape_text(
1451            &fonts,
1452            &ShapeRequest {
1453                font: FontId(0),
1454                text: "ab",
1455                direction: Direction::LeftToRight,
1456                source: Some(ByteSpan { start: 4, end: 6 }),
1457                script: None,
1458                features: Vec::new(),
1459            },
1460        )
1461        .expect("fallback shaping should work");
1462
1463        assert_eq!(shaped.glyphs.len(), 2);
1464        assert_eq!(shaped.glyphs[0].glyph_id, GlyphId('a' as u32));
1465        assert_eq!(
1466            shaped.glyphs[0].cluster,
1467            Some(ByteSpan { start: 4, end: 5 })
1468        );
1469        assert_eq!(
1470            shaped.glyphs[1].cluster,
1471            Some(ByteSpan { start: 5, end: 6 })
1472        );
1473    }
1474
1475    #[test]
1476    fn no_font_system_makes_missing_font_explicit() {
1477        let error = NoFontSystem
1478            .load_font(&FontQuery {
1479                family: "missing".to_string(),
1480                size: Length::ZERO,
1481                math: false,
1482            })
1483            .expect_err("font should be missing");
1484
1485        assert_eq!(
1486            error,
1487            FontError::NotFound {
1488                family: "missing".to_string(),
1489            }
1490        );
1491    }
1492
1493    #[derive(Clone, Debug, Default, PartialEq, Eq)]
1494    struct TestLoader;
1495
1496    impl FontLoader for TestLoader {
1497        fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
1498            Ok(FontData::new(
1499                FontId(9),
1500                query.family.clone(),
1501                b"parsed-by-rust-loader".to_vec(),
1502            ))
1503        }
1504    }
1505
1506    #[derive(Clone, Debug, Default, PartialEq, Eq)]
1507    struct TestShaper;
1508
1509    impl TextShaper for TestShaper {
1510        fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
1511            Ok(ShapedText::new(alloc::vec![PositionedGlyph {
1512                glyph_id: GlyphId(100),
1513                offset: Point::default(),
1514                advance: Point {
1515                    x: Length::from_scaled_points(request.text.len() as i32),
1516                    y: Length::ZERO,
1517                },
1518                cluster: request.source,
1519            }]))
1520        }
1521    }
1522
1523    #[test]
1524    fn composed_font_system_splits_loading_from_shaping() {
1525        let fonts = ComposedFontSystem::new(TestLoader, TestShaper);
1526
1527        let font = fonts
1528            .load_font(&FontQuery {
1529                family: "Latin Modern Math".to_string(),
1530                size: Length::from_scaled_points(12 * 65_536),
1531                math: true,
1532            })
1533            .expect("loader should resolve font");
1534        let shaped = fonts
1535            .shape_text(&ShapeRequest {
1536                font: font.id,
1537                text: "xy",
1538                direction: Direction::LeftToRight,
1539                source: Some(ByteSpan { start: 2, end: 4 }),
1540                script: None,
1541                features: Vec::new(),
1542            })
1543            .expect("shaper should shape text");
1544
1545        assert_eq!(font.id, FontId(9));
1546        assert_eq!(
1547            &**font.bytes().expect("library owned bytes"),
1548            b"parsed-by-rust-loader"
1549        );
1550        assert_eq!(shaped.glyphs[0].glyph_id, GlyphId(100));
1551        assert_eq!(
1552            shaped.glyphs[0].cluster,
1553            Some(ByteSpan { start: 2, end: 4 })
1554        );
1555    }
1556
1557    #[test]
1558    fn scale_font_units_matches_xetex_d2fix_rounding() {
1559        // The '2' glyph in latinmodern-math rounds 436469.76sp to 436470 like XeTeX.
1560        let ten_pt = Length::from_scaled_points(10 * 65_536);
1561        assert_eq!(
1562            scale_font_units(666, ten_pt, 1000),
1563            436_470,
1564            "D2Fix must round 436469.76sp up to 436470 (xetex parity)"
1565        );
1566        // 'x' glyph height of 431 units gives 282460.16sp, rounded to 282460.
1567        assert_eq!(scale_font_units(431, ten_pt, 1000), 282_460);
1568        // 'x' advance of 528 units gives 346030.08sp, rounded to 346030.
1569        assert_eq!(scale_font_units(528, ten_pt, 1000), 346_030);
1570        // Glyph 89 italic correction of 16 units gives 10485.76sp, rounded to 10486.
1571        assert_eq!(scale_font_units(16, ten_pt, 1000), 10_486);
1572        // Negative quantities use C cast truncation, so minus 10485.26 becomes minus 10485.
1573        assert_eq!(scale_font_units(-16, ten_pt, 1000), -10_485);
1574        assert_eq!(scale_font_units(0, ten_pt, 1000), 0);
1575    }
1576
1577    #[test]
1578    fn rustybuzz_font_system_shapes_loaded_ttf_and_reads_metrics() {
1579        let fonts = RustybuzzFontSystem::new(InMemoryFontSystem::new().with_font(
1580            "DejaVu Sans",
1581            include_bytes!("../../../vendor/texlive-source/libs/gd/libgd-src/tests/freetype/DejaVuSans.ttf")
1582                .as_slice(),
1583        ));
1584        let font = FontSystem::load_font(
1585            &fonts,
1586            &FontQuery {
1587                family: "DejaVu Sans".to_string(),
1588                size: Length::from_scaled_points(10 * 65_536),
1589                math: false,
1590            },
1591        )
1592        .expect("real TTF should parse");
1593
1594        let metrics = font
1595            .metrics(Length::from_scaled_points(10 * 65_536))
1596            .expect("metrics should parse");
1597        let shaped = FontSystem::shape_text(
1598            &fonts,
1599            &ShapeRequest {
1600                font: font.id,
1601                text: "AV",
1602                direction: Direction::LeftToRight,
1603                source: Some(ByteSpan { start: 7, end: 9 }),
1604                script: None,
1605                features: Vec::new(),
1606            },
1607        )
1608        .expect("rustybuzz should shape cached font");
1609
1610        assert!(metrics.ascent > 0);
1611        assert!(shaped.glyphs.len() >= 2);
1612        assert!(shaped.glyphs[0].advance.x > Length::ZERO);
1613        assert_eq!(
1614            shaped.glyphs[0].cluster,
1615            Some(ByteSpan { start: 7, end: 8 })
1616        );
1617    }
1618
1619    const LM_MATH: &str = "/usr/local/texlive/2025/texmf-dist/fonts/opentype/public/lm-math/latinmodern-math.otf";
1620    const STIX_MATH: &str =
1621        "/usr/local/texlive/2025/texmf-dist/fonts/opentype/public/stix2-otf/STIXTwoMath-Regular.otf";
1622
1623    fn load_system_font(path: &str) -> Option<FontData> {
1624        let bytes = std::fs::read(path).ok()?;
1625        Some(FontData::new(FontId(1), path, bytes))
1626    }
1627
1628    fn ten_pt() -> Length {
1629        Length::from_scaled_points(10 * 65_536)
1630    }
1631
1632    #[test]
1633    fn glyph_outlines_extract_design_unit_contours_from_latinmodern() {
1634        let Some(font) = load_system_font(LM_MATH) else {
1635            eprintln!("SKIP: {LM_MATH} not found");
1636            return;
1637        };
1638        let x = font.glyph_index('x').unwrap().unwrap();
1639        // The run is parsed once; 'x' has an outline and the space glyph does not.
1640        let space = font.glyph_index(' ').unwrap().unwrap();
1641        let outlines = font.glyph_outlines(&[x, space]).unwrap();
1642        assert_eq!(outlines.len(), 2);
1643
1644        let x_outline = outlines[0].as_ref().expect("'x' has an outline");
1645        assert_eq!(x_outline.units_per_em, 1000, "lm-math upem");
1646        assert!(
1647            !x_outline.commands.is_empty(),
1648            "'x' outline has contour commands"
1649        );
1650        assert!(
1651            matches!(x_outline.commands[0], OutlineCommand::MoveTo { .. }),
1652            "a contour starts with MoveTo"
1653        );
1654
1655        assert!(outlines[1].is_none(), "space has no outline");
1656    }
1657
1658    #[test]
1659    fn math_variant_returns_larger_paren_glyphs_from_latinmodern() {
1660        let Some(font) = load_system_font(LM_MATH) else {
1661            eprintln!("SKIP: {LM_MATH} not found");
1662            return;
1663        };
1664        // In latinmodern-math, glyph 9 vertical variant 4 is glyph 2433 with advance 1175061 sp.
1665        let paren = font.glyph_index('(').unwrap().unwrap();
1666        assert_eq!(paren.0, 9);
1667        let variant = font
1668            .math_variant(paren, 4, false, ten_pt())
1669            .unwrap()
1670            .expect("'(' has a 5th vertical variant");
1671        assert_eq!(variant.0, 2433, "variant glyph id");
1672        assert_eq!(variant.1, 1_175_061, "variant advance sp");
1673        // variant[0] is the base glyph itself with advance 653394 sp.
1674        let base = font.math_variant(paren, 0, false, ten_pt()).unwrap().unwrap();
1675        assert_eq!(base, (9, 653_394));
1676        // Out of range index yields None.
1677        assert!(font.math_variant(paren, 99, false, ten_pt()).unwrap().is_none());
1678    }
1679
1680    #[test]
1681    fn math_assembly_returns_paren_parts_from_latinmodern() {
1682        let Some(font) = load_system_font(LM_MATH) else {
1683            eprintln!("SKIP: {LM_MATH} not found");
1684            return;
1685        };
1686        let paren = font.glyph_index('(').unwrap().unwrap();
1687        let parts = font.math_assembly(paren, false, ten_pt()).unwrap();
1688        // The glyph 9 vertical assembly uses bottom 2503, extender 2504, and top 2505.
1689        assert_eq!(parts.len(), 3, "'(' assembly has 3 parts");
1690        assert_eq!(
1691            parts[0],
1692            MathAssemblyPart {
1693                glyph: 2503,
1694                start_connector: 0,
1695                end_connector: 163_185,  // 249 units
1696                full_advance: 979_763,   // 1495 units
1697                extender: false,
1698            }
1699        );
1700        assert_eq!(
1701            parts[1],
1702            MathAssemblyPart {
1703                glyph: 2504,
1704                start_connector: 326_369, // 498 units
1705                end_connector: 326_369,
1706                full_advance: 326_369,
1707                extender: true,
1708            }
1709        );
1710        assert_eq!(
1711            parts[2],
1712            MathAssemblyPart {
1713                glyph: 2505,
1714                start_connector: 163_185,
1715                end_connector: 0,
1716                full_advance: 979_763,
1717                extender: false,
1718            }
1719        );
1720        // Minimum connector overlap of 20 units gives 13107 sp at 10pt.
1721        assert_eq!(font.math_min_connector_overlap(ten_pt()).unwrap(), 13_107);
1722    }
1723
1724    #[test]
1725    fn math_kern_reads_stix_cut_ins() {
1726        let Some(font) = load_system_font(STIX_MATH) else {
1727            eprintln!("SKIP: {STIX_MATH} not found");
1728            return;
1729        };
1730        // 'F' glyph 8 has one TopRight superscript kern value, so any correction height returns 44.
1731        let f = font.glyph_index('F').unwrap().unwrap();
1732        assert_eq!(f.0, 8);
1733        assert_eq!(
1734            font.math_kern_at(f, MathKernCorner::TopRight, 0).unwrap(),
1735            44
1736        );
1737        assert_eq!(
1738            font.math_kern_at(f, MathKernCorner::TopRight, 100_000).unwrap(),
1739            44
1740        );
1741        // 'V' glyph 24 has BottomRight heights [126, 280] and kerns [-193, -119, 56].
1742        let v = font.glyph_index('V').unwrap().unwrap();
1743        assert_eq!(v.0, 24);
1744        assert_eq!(
1745            font.math_kern_at(v, MathKernCorner::BottomRight, 0).unwrap(),
1746            -193,
1747            "below first height -> kern[0]"
1748        );
1749        assert_eq!(
1750            font.math_kern_at(v, MathKernCorner::BottomRight, 200).unwrap(),
1751            -119,
1752            "between heights -> kern[1]"
1753        );
1754        assert_eq!(
1755            font.math_kern_at(v, MathKernCorner::BottomRight, 300).unwrap(),
1756            56,
1757            "above last height -> kern[last]"
1758        );
1759        // A glyph or corner with no kern record contributes zero.
1760        assert_eq!(
1761            font.math_kern_at(v, MathKernCorner::TopRight, 0).unwrap(),
1762            0
1763        );
1764    }
1765
1766    #[test]
1767    fn latinmodern_has_no_math_kern_info() {
1768        // latinmodern-math has no MathKernInfo, so all math kern values are zero.
1769        let Some(font) = load_system_font(LM_MATH) else {
1770            eprintln!("SKIP: {LM_MATH} not found");
1771            return;
1772        };
1773        let x = font.glyph_index('x').unwrap().unwrap();
1774        for corner in [
1775            MathKernCorner::TopRight,
1776            MathKernCorner::TopLeft,
1777            MathKernCorner::BottomRight,
1778            MathKernCorner::BottomLeft,
1779        ] {
1780            assert_eq!(font.math_kern_at(x, corner, 0).unwrap(), 0);
1781        }
1782    }
1783}