Skip to main content

mathtex_engine/
generated.rs

1use core::fmt;
2
3use alloc::collections::BTreeMap;
4use alloc::format;
5use alloc::string::String;
6use alloc::vec::Vec;
7use mathtex_font::{FontData, FontQuery, FontSystem, MathKernCorner, ShapeFeature, ShapeRequest};
8use mathtex_ir::{
9    BoxKind, BoxMetrics, ByteSpan, Direction, FontId, FontRef, Fragment, FragmentMetadata, Glue,
10    GlyphId, GlyphRun, Kern, LayoutBox, LayoutNode, LayoutNodeKind, Length, NodeId, Point,
11    PositionedGlyph, Rect, Rule, Size, SourceRange, SourceRole, Style, Surface,
12};
13
14use crate::platform::{HostBoxRequest, HostBoxStyle, LinebreakRequest, Platform};
15use crate::resource::{ResourceKind, ResourceProvider, ResourceRequest as EngineResourceRequest};
16
17/// Reexport of the portable engine node handle type.
18pub type GeneratedNodeHandle = mathtex_portable_engine_generated::PortableNodeHandle;
19
20/// Enclosing box dimensions used to resolve `null_flag` rule dimensions during shipout.
21#[derive(Clone, Copy)]
22struct ParentBox {
23    vertical: bool,
24    width: i32,
25    height: i32,
26    depth: i32,
27}
28
29/// Reexport of the portable engine node kind discriminant.
30pub type GeneratedNodeKind = mathtex_portable_engine_generated::PortableNodeKind;
31
32/// Reexport of the portable engine node snapshot type.
33pub type GeneratedNodeSnapshot = mathtex_portable_engine_generated::PortableNodeSnapshot;
34
35/// Reexport of the portable engine source span type.
36pub type GeneratedSourceSpan = mathtex_portable_engine_generated::PortableSourceSpan;
37
38/// Reexport of the portable engine node source span type.
39pub type GeneratedNodeSourceSpan = mathtex_portable_engine_generated::PortableNodeSourceSpan;
40
41/// Reexport of the portable engine resource request record type.
42pub type GeneratedResourceRequestRecord =
43    mathtex_portable_engine_generated::PortableResourceRequestRecord;
44
45/// Reexport of the portable engine native font handle type.
46pub type GeneratedFontHandle = mathtex_portable_engine_generated::PortableFontHandle;
47
48/// Reexport of the portable engine font metrics type.
49pub type GeneratedFontMetrics = mathtex_portable_engine_generated::PortableFontMetrics;
50
51/// Reexport of the portable engine shaped glyph descriptor.
52pub type GeneratedNativeGlyph = mathtex_portable_engine_generated::PortableNativeGlyph;
53
54/// Reexport of the portable engine shaped text metrics type.
55pub type GeneratedNativeTextMetrics = mathtex_portable_engine_generated::PortableNativeTextMetrics;
56
57/// Reexport of the portable engine glyph metrics type.
58pub type GeneratedNativeGlyphMetrics =
59    mathtex_portable_engine_generated::PortableNativeGlyphMetrics;
60
61/// Reexport of the portable engine math variant type.
62pub type GeneratedMathVariant = mathtex_portable_engine_generated::PortableMathVariant;
63
64/// Reexport of the portable engine math assembly part type.
65pub type GeneratedMathAssemblyPart =
66    mathtex_portable_engine_generated::PortableMathAssemblyPart;
67
68/// Reexport of the portable engine math kern corner enum.
69pub type GeneratedMathKernCorner = mathtex_portable_engine_generated::PortableMathKernCorner;
70
71/// Trait alias for the portable engine's native font platform interface.
72pub trait GeneratedFontPlatform: mathtex_portable_engine_generated::FontPlatform {}
73
74// Temporary loop detector scaffold, currently disabled.
75macro_rules! trace_loop {
76    ($tag:literal) => {{
77        let _ = $tag;
78    }};
79}
80
81impl<T> GeneratedFontPlatform for T where T: mathtex_portable_engine_generated::FontPlatform {}
82
83/// Empty font platform for use when native font loading is disabled.
84pub type EmptyGeneratedFontPlatform = mathtex_portable_engine_generated::EmptyFontPlatform;
85
86/// Trait alias for the portable engine's platform interface.
87pub trait GeneratedPlatform: mathtex_portable_engine_generated::PortablePlatform {}
88
89impl<T> GeneratedPlatform for T where T: mathtex_portable_engine_generated::PortablePlatform {}
90
91/// Deterministic placeholder generated engine platform.
92pub type EmptyGeneratedPlatform = mathtex_portable_engine_generated::EmptyPlatform;
93
94/// Reexport of the portable engine clock type.
95pub type GeneratedClock = mathtex_portable_engine_generated::PortableClock;
96
97/// Reexport of the portable engine linebreak request type.
98pub type GeneratedLinebreakRequest<'a> =
99    mathtex_portable_engine_generated::PortableLinebreakRequest<'a>;
100
101/// Reexport of the portable engine host box request type.
102pub type GeneratedHostBoxRequest = mathtex_portable_engine_generated::PortableHostBoxRequest;
103
104/// Reexport of the portable engine host box style type.
105pub type GeneratedHostBoxStyle = mathtex_portable_engine_generated::PortableHostBoxStyle;
106
107/// Reexport of the portable engine host box response type.
108pub type GeneratedHostBox = mathtex_portable_engine_generated::PortableHostBox;
109
110/// Adapts a [`Platform`] to the portable engine's `PortablePlatform` interface.
111#[derive(Clone, Copy, Debug)]
112pub struct GeneratedPlatformAdapter<P> {
113    platform: P,
114}
115
116impl<P> GeneratedPlatformAdapter<P> {
117    /// Wraps `platform` in the adapter.
118    #[must_use]
119    pub fn new(platform: P) -> Self {
120        Self { platform }
121    }
122
123    /// Returns a reference to the wrapped platform.
124    #[must_use]
125    pub fn platform(&self) -> &P {
126        &self.platform
127    }
128
129    /// Unwraps and returns the inner platform.
130    #[must_use]
131    pub fn into_platform(self) -> P {
132        self.platform
133    }
134}
135
136impl<P> mathtex_portable_engine_generated::PortablePlatform for GeneratedPlatformAdapter<P>
137where
138    P: Platform,
139{
140    fn clock(&mut self) -> GeneratedClock {
141        let clock = self.platform.clock();
142        GeneratedClock {
143            seconds: clock.seconds,
144            micros: clock.micros,
145        }
146    }
147
148    fn linebreak_start(&mut self, request: GeneratedLinebreakRequest<'_>) {
149        self.platform.linebreak_start(LinebreakRequest {
150            font: request.font,
151            locale: request.locale,
152            text: request.text,
153        });
154    }
155
156    fn linebreak_next(&mut self) -> Option<i32> {
157        self.platform.linebreak_next()
158    }
159
160    fn host_box(&mut self, request: GeneratedHostBoxRequest) -> Option<GeneratedHostBox> {
161        let style = match request.style {
162            GeneratedHostBoxStyle::Text => HostBoxStyle::Text,
163            GeneratedHostBoxStyle::Script => HostBoxStyle::Script,
164            GeneratedHostBoxStyle::ScriptScript => HostBoxStyle::ScriptScript,
165        };
166        let response = self.platform.host_box(HostBoxRequest {
167            token: request.token,
168            style,
169            font_size: request.font_size,
170        })?;
171        Some(GeneratedHostBox {
172            width: response.width,
173            height: response.height,
174            depth: response.depth,
175            runs: response
176                .runs
177                .into_iter()
178                .map(|run| mathtex_portable_engine_generated::PortableHostBoxRun {
179                    font_name: run.font_name,
180                    font_size: run.font_size,
181                    glyphs: run
182                        .glyphs
183                        .into_iter()
184                        .map(|glyph| mathtex_portable_engine_generated::PortableHostBoxGlyph {
185                            glyph: glyph.glyph,
186                            x: glyph.x,
187                            y: glyph.y,
188                            advance: glyph.advance,
189                        })
190                        .collect(),
191                })
192                .collect(),
193            rules: response
194                .rules
195                .into_iter()
196                .map(|rule| mathtex_portable_engine_generated::PortableHostBoxRule {
197                    x: rule.x,
198                    y: rule.y,
199                    width: rule.width,
200                    height: rule.height,
201                })
202                .collect(),
203        })
204    }
205}
206
207/// Adapter that resolves XeTeX native font requests through a [`FontSystem`].
208#[derive(Clone, Debug)]
209pub struct GeneratedFontSystemAdapter<F> {
210    fonts: F,
211    next_handle: GeneratedFontHandle,
212    loaded_fonts: BTreeMap<GeneratedFontHandle, LoadedGeneratedFont>,
213}
214
215#[derive(Clone, Debug, PartialEq, Eq)]
216struct LoadedGeneratedFont {
217    data: FontData,
218    size: Length,
219    /// XeTeX `\font` spec the instance was loaded from (`[file]:features`).
220    spec: String,
221    /// Forced OpenType script tag from `script=` in the spec for feature lookup.
222    script: Option<[u8; 4]>,
223    /// OpenType features from the spec (e.g. `ssty=1` selects script size glyph variants).
224    features: Vec<ShapeFeature>,
225}
226
227impl<F> GeneratedFontSystemAdapter<F> {
228    /// Constructs the adapter wrapping `fonts`.
229    #[must_use]
230    pub fn new(fonts: F) -> Self {
231        Self {
232            fonts,
233            next_handle: 1,
234            loaded_fonts: BTreeMap::new(),
235        }
236    }
237
238    /// Returns a reference to the wrapped font system.
239    #[must_use]
240    pub fn font_system(&self) -> &F {
241        &self.fonts
242    }
243
244    /// Unwraps and returns the inner font system.
245    #[must_use]
246    pub fn into_font_system(self) -> F {
247        self.fonts
248    }
249
250    /// Returns the font data for a loaded handle, or `None` if not found.
251    #[must_use]
252    pub fn loaded_font(&self, handle: GeneratedFontHandle) -> Option<&FontData> {
253        self.loaded_fonts.get(&handle).map(|font| &font.data)
254    }
255}
256
257impl<F> mathtex_portable_engine_generated::FontPlatform for GeneratedFontSystemAdapter<F>
258where
259    F: FontSystem,
260{
261    fn resolve_font_handle(&mut self, name: &[i32], size: i32) -> Option<GeneratedFontHandle> {
262        trace_loop!("resolve_font_handle");
263        let spec = unicode_scalars_to_string(name);
264        let (script, features) = parse_spec_shaping(&spec);
265        let font = self
266            .fonts
267            .load_font(&FontQuery {
268                family: spec.clone(),
269                size: Length::from_scaled_points(size),
270                math: true,
271            })
272            .ok()?;
273        let next_handle = self.next_handle.checked_add(1)?;
274        let handle = self.next_handle;
275        self.next_handle = next_handle;
276        self.loaded_fonts.insert(
277            handle,
278            LoadedGeneratedFont {
279                data: font,
280                size: Length::from_scaled_points(size),
281                spec,
282                script,
283                features,
284            },
285        );
286        Some(handle)
287    }
288
289    fn release_font_handle(&mut self, font: GeneratedFontHandle, _type_flag: i32) {
290        self.loaded_fonts.remove(&font);
291    }
292
293    fn font_table(&self) -> Vec<(GeneratedFontHandle, String, i32)> {
294        self.loaded_fonts
295            .iter()
296            .map(|(&handle, font)| (handle, font.spec.clone(), font.size.0))
297            .collect()
298    }
299
300    fn restore_font_table(&mut self, table: &[(GeneratedFontHandle, String, i32)]) -> bool {
301        let mut ok = true;
302        for (handle, spec, size) in table {
303            let size = Length::from_scaled_points(*size);
304            match self.fonts.load_font(&FontQuery {
305                family: spec.clone(),
306                size,
307                math: true,
308            }) {
309                Ok(data) => {
310                    let (script, features) = parse_spec_shaping(spec);
311                    self.loaded_fonts.insert(
312                        *handle,
313                        LoadedGeneratedFont {
314                            data,
315                            size,
316                            spec: spec.clone(),
317                            script,
318                            features,
319                        },
320                    );
321                    self.next_handle = self.next_handle.max(handle.saturating_add(1));
322                }
323                Err(_) => ok = false,
324            }
325        }
326        ok
327    }
328
329    fn font_metrics(&mut self, font: GeneratedFontHandle) -> GeneratedFontMetrics {
330        self.loaded_fonts
331            .get(&font)
332            .and_then(|font| font.data.metrics(font.size).ok())
333            .map(generated_font_metrics)
334            .unwrap_or_default()
335    }
336
337    fn opentype_font_metrics(&mut self, font: GeneratedFontHandle) -> GeneratedFontMetrics {
338        self.font_metrics(font)
339    }
340
341    fn is_opentype_math_font(&mut self, font: GeneratedFontHandle) -> bool {
342        trace_loop!("is_opentype_math_font");
343        self.loaded_fonts
344            .get(&font)
345            .and_then(|font| font.data.has_opentype_math().ok())
346            .unwrap_or(false)
347    }
348
349    fn using_opentype(&mut self, font: GeneratedFontHandle) -> bool {
350        // Fonts loaded through this adapter are shaped by rustybuzz as OpenType fonts.
351        self.loaded_fonts.contains_key(&font)
352    }
353
354    fn math_symbol_parameter(&mut self, font: GeneratedFontHandle, parameter: i32) -> i32 {
355        self.loaded_fonts
356            .get(&font)
357            .and_then(|font| font.data.math_symbol_parameter(parameter, font.size).ok())
358            .unwrap_or(0)
359    }
360
361    fn math_extension_parameter(&mut self, font: GeneratedFontHandle, parameter: i32) -> i32 {
362        self.loaded_fonts
363            .get(&font)
364            .and_then(|font| {
365                font.data
366                    .math_extension_parameter(parameter, font.size)
367                    .ok()
368            })
369            .unwrap_or(0)
370    }
371
372    fn opentype_math_constant(&mut self, font: GeneratedFontHandle, constant: i32) -> i32 {
373        trace_loop!("opentype_math_constant");
374        self.loaded_fonts
375            .get(&font)
376            .and_then(|font| font.data.opentype_math_constant(constant, font.size).ok())
377            .unwrap_or(0)
378    }
379
380    fn opentype_math_accent_position(&mut self, font: GeneratedFontHandle, glyph: i32) -> i32 {
381        let Ok(glyph) = u32::try_from(glyph) else {
382            return 0;
383        };
384        self.loaded_fonts
385            .get(&font)
386            .and_then(|font| {
387                font.data
388                    .opentype_math_accent_position(GlyphId(glyph), font.size)
389                    .ok()
390            })
391            .unwrap_or(0)
392    }
393
394    fn math_glyph_italic_correction(&mut self, font: GeneratedFontHandle, glyph: i32) -> i32 {
395        trace_loop!("math_glyph_italic_correction");
396        let Ok(glyph) = u32::try_from(glyph) else {
397            return 0;
398        };
399        self.loaded_fonts
400            .get(&font)
401            .and_then(|font| {
402                font.data
403                    .math_italic_correction(GlyphId(glyph), font.size)
404                    .ok()
405            })
406            .unwrap_or(0)
407    }
408
409    fn math_glyph_variant(
410        &mut self,
411        font: GeneratedFontHandle,
412        glyph: i32,
413        index: u16,
414        horizontal: bool,
415    ) -> Option<GeneratedMathVariant> {
416        trace_loop!("math_glyph_variant");
417        let glyph = u32::try_from(glyph).ok()?;
418        let loaded = self.loaded_fonts.get(&font)?;
419        let (variant_glyph, advance) = loaded
420            .data
421            .math_variant(GlyphId(glyph), index, horizontal, loaded.size)
422            .ok()??;
423        Some(GeneratedMathVariant {
424            glyph: variant_glyph.min(i32::MAX as u32) as i32,
425            advance,
426        })
427    }
428
429    fn math_glyph_assembly(
430        &mut self,
431        font: GeneratedFontHandle,
432        glyph: i32,
433        horizontal: bool,
434    ) -> Vec<GeneratedMathAssemblyPart> {
435        trace_loop!("math_glyph_assembly");
436        let Ok(glyph) = u32::try_from(glyph) else {
437            return Vec::new();
438        };
439        let Some(loaded) = self.loaded_fonts.get(&font) else {
440            return Vec::new();
441        };
442        let Ok(parts) = loaded
443            .data
444            .math_assembly(GlyphId(glyph), horizontal, loaded.size)
445        else {
446            return Vec::new();
447        };
448        parts
449            .into_iter()
450            .map(|part| GeneratedMathAssemblyPart {
451                glyph: part.glyph.min(i32::MAX as u32) as i32,
452                start_connector: part.start_connector,
453                end_connector: part.end_connector,
454                full_advance: part.full_advance,
455                extender: part.extender,
456            })
457            .collect()
458    }
459
460    fn math_min_connector_overlap(&mut self, font: GeneratedFontHandle) -> i32 {
461        self.loaded_fonts
462            .get(&font)
463            .and_then(|font| font.data.math_min_connector_overlap(font.size).ok())
464            .unwrap_or(0)
465    }
466
467    fn math_kern_at(
468        &mut self,
469        font: GeneratedFontHandle,
470        glyph: i32,
471        corner: GeneratedMathKernCorner,
472        correction_height: i32,
473    ) -> i32 {
474        trace_loop!("math_kern_at");
475        let Ok(glyph) = u32::try_from(glyph) else {
476            return 0;
477        };
478        let corner = match corner {
479            GeneratedMathKernCorner::TopRight => MathKernCorner::TopRight,
480            GeneratedMathKernCorner::TopLeft => MathKernCorner::TopLeft,
481            GeneratedMathKernCorner::BottomRight => MathKernCorner::BottomRight,
482            GeneratedMathKernCorner::BottomLeft => MathKernCorner::BottomLeft,
483        };
484        self.loaded_fonts
485            .get(&font)
486            .and_then(|font| font.data.math_kern_at(GlyphId(glyph), corner, correction_height).ok())
487            .unwrap_or(0)
488    }
489
490    fn math_points_to_units(&mut self, font: GeneratedFontHandle, points: f32) -> f32 {
491        self.loaded_fonts
492            .get(&font)
493            .and_then(|font| font.data.points_to_units(points, font.size).ok())
494            .unwrap_or(0.0)
495    }
496
497    fn math_units_to_scaled(&mut self, font: GeneratedFontHandle, units: i32) -> i32 {
498        self.loaded_fonts
499            .get(&font)
500            .and_then(|font| font.data.units_to_scaled(units, font.size).ok())
501            .unwrap_or(0)
502    }
503
504    fn math_point_size(&mut self, font: GeneratedFontHandle) -> f32 {
505        self.loaded_fonts
506            .get(&font)
507            .map(|font| (f64::from(font.size.0) / 65536.0) as f32)
508            .unwrap_or(0.0)
509    }
510
511    fn map_char_to_glyph(&mut self, font: GeneratedFontHandle, codepoint: i32) -> i32 {
512        trace_loop!("map_char_to_glyph");
513        let Some(codepoint) = u32::try_from(codepoint).ok().and_then(char::from_u32) else {
514            return 0;
515        };
516        self.loaded_fonts
517            .get(&font)
518            .and_then(|font| font.data.glyph_index(codepoint).ok().flatten())
519            .map(|glyph| glyph.0.min(i32::MAX as u32) as i32)
520            .unwrap_or(0)
521    }
522
523    fn map_glyph_to_index(&mut self, font: GeneratedFontHandle, name: &str) -> i32 {
524        self.loaded_fonts
525            .get(&font)
526            .and_then(|font| font.data.glyph_index_by_name(name).ok().flatten())
527            .map(|glyph| glyph.0.min(i32::MAX as u32) as i32)
528            .unwrap_or(0)
529    }
530
531    fn ot_font_get(
532        &mut self,
533        font: GeneratedFontHandle,
534        what: i32,
535        param1: i32,
536        param2: i32,
537        param3: i32,
538    ) -> i32 {
539        let Some(loaded) = self.loaded_fonts.get(&font) else {
540            return 0;
541        };
542        let data = &loaded.data;
543        // `what` is the XeTeX_ext OpenType layout selector, params are script and language tags or indices.
544        let result = match what {
545            1 => data.ot_glyph_count(),                                 // XeTeX_count_glyphs
546            16 => data.ot_script_count(),                               // XeTeX_OT_count_scripts
547            17 => data.ot_language_count(param1 as u32),                // XeTeX_OT_count_languages
548            18 => data.ot_feature_count(param1 as u32, param2 as u32),  // XeTeX_OT_count_features
549            19 => data.ot_script_tag(param1 as u32),                    // XeTeX_OT_script_code
550            20 => data.ot_language_tag(param1 as u32, param2 as u32),   // XeTeX_OT_language_code
551            21 => data.ot_feature_tag(param1 as u32, param2 as u32, param3 as u32), // XeTeX_OT_feature_code
552            _ => return 0,
553        };
554        result.map_or(0, |value| value.min(i32::MAX as u32) as i32)
555    }
556
557    fn font_spec(&self, font: GeneratedFontHandle) -> Option<String> {
558        self.loaded_fonts.get(&font).map(|loaded| loaded.spec.clone())
559    }
560
561    fn shape_native_text(
562        &mut self,
563        font: GeneratedFontHandle,
564        text: &[u16],
565        _use_glyph_metrics: bool,
566    ) -> GeneratedNativeTextMetrics {
567        let Some(loaded) = self.loaded_fonts.get(&font) else {
568            return GeneratedNativeTextMetrics::default();
569        };
570        let text = utf16_to_string(text);
571        let metrics = loaded.data.metrics(loaded.size).ok();
572        let Ok(shaped) = self.fonts.shape_text(&ShapeRequest {
573            font: loaded.data.id,
574            text: text.as_str(),
575            direction: Direction::LeftToRight,
576            source: Some(ByteSpan {
577                start: 0,
578                end: text.len().min(u32::MAX as usize) as u32,
579            }),
580            script: loaded.script,
581            features: loaded.features.clone(),
582        }) else {
583            return GeneratedNativeTextMetrics {
584                height: metrics.map_or(0, |metrics| metrics.ascent),
585                depth: metrics.map_or(0, |metrics| -metrics.descent),
586                ..GeneratedNativeTextMetrics::default()
587            };
588        };
589
590        let mut cursor = 0i32;
591        let glyphs = shaped
592            .glyphs
593            .iter()
594            .map(|glyph| {
595                let native = GeneratedNativeGlyph {
596                    glyph_id: glyph_id_u16(glyph.glyph_id),
597                    x: cursor.saturating_add(glyph.offset.x.0),
598                    y: glyph.offset.y.0,
599                    advance: glyph.advance.x.0,
600                    cluster_start: glyph.cluster.map_or(0, |span| span.start),
601                    cluster_end: glyph.cluster.map_or(0, |span| span.end),
602                    // Source spans are filled later from tracked character identifiers.
603                    src_start: 0,
604                    src_end: 0,
605                };
606                cursor = cursor.saturating_add(glyph.advance.x.0);
607                native
608            })
609            .collect();
610
611        GeneratedNativeTextMetrics {
612            width: cursor,
613            height: metrics.map_or(0, |metrics| metrics.ascent),
614            depth: metrics.map_or(0, |metrics| -metrics.descent),
615            glyphs,
616        }
617    }
618
619    fn measure_native_glyph(
620        &mut self,
621        font: GeneratedFontHandle,
622        glyph: u16,
623        _use_glyph_metrics: bool,
624    ) -> GeneratedNativeGlyphMetrics {
625        self.loaded_fonts
626            .get(&font)
627            .and_then(|font| {
628                font.data
629                    .glyph_metrics(GlyphId(u32::from(glyph)), font.size)
630                    .ok()
631            })
632            .map(|metrics| GeneratedNativeGlyphMetrics {
633                width: metrics.width,
634                height: metrics.height,
635                depth: metrics.depth,
636            })
637            .unwrap_or_default()
638    }
639}
640
641fn generated_font_metrics(metrics: mathtex_font::FontMetrics) -> GeneratedFontMetrics {
642    GeneratedFontMetrics {
643        ascent: metrics.ascent,
644        descent: metrics.descent,
645        xheight: metrics.xheight,
646        capheight: metrics.capheight,
647        slant: metrics.slant,
648    }
649}
650
651/// Parses script tag and features from a XeTeX `\font` spec.
652fn parse_spec_shaping(spec: &str) -> (Option<[u8; 4]>, Vec<ShapeFeature>) {
653    let Some((_, feature_str)) = spec.split_once(':') else {
654        return (None, Vec::new());
655    };
656    let mut script = None;
657    let mut features = Vec::new();
658    for part in feature_str.split(';') {
659        let part = part.trim();
660        if part.is_empty() {
661            continue;
662        }
663        if let Some(value) = part.strip_prefix("script=") {
664            script = Some(ot_tag_bytes(value));
665        } else if part.strip_prefix("language=").is_some() {
666            // Language selection is not yet threaded into shaping.
667        } else if let Some(rest) = part.strip_prefix('+') {
668            // XeTeX increments nonnegative `+feat` params so `+ssty=0` selects the first alternate.
669            let (tag, param) = rest.split_once('=').map_or((rest, 0i64), |(tag, value)| {
670                (tag, value.trim().parse::<i64>().unwrap_or(0))
671            });
672            let value = if param >= 0 { param + 1 } else { param };
673            features.push(ShapeFeature {
674                tag: ot_tag_bytes(tag),
675                value: value as u32,
676            });
677        } else if let Some(rest) = part.strip_prefix('-') {
678            features.push(ShapeFeature {
679                tag: ot_tag_bytes(rest),
680                value: 0,
681            });
682        } else if let Some((tag, value)) = part.split_once('=') {
683            features.push(ShapeFeature {
684                tag: ot_tag_bytes(tag),
685                value: value.trim().parse().unwrap_or(1),
686            });
687        }
688    }
689    (script, features)
690}
691
692/// Pad/truncate an OpenType tag string to 4 space padded bytes (HarfBuzz rules).
693fn ot_tag_bytes(tag: &str) -> [u8; 4] {
694    let bytes = tag.trim().as_bytes();
695    [
696        bytes.first().copied().unwrap_or(b' '),
697        bytes.get(1).copied().unwrap_or(b' '),
698        bytes.get(2).copied().unwrap_or(b' '),
699        bytes.get(3).copied().unwrap_or(b' '),
700    ]
701}
702
703fn unicode_scalars_to_string(name: &[i32]) -> String {
704    name.iter()
705        .map(|codepoint| {
706            u32::try_from(*codepoint)
707                .ok()
708                .and_then(char::from_u32)
709                .unwrap_or(char::REPLACEMENT_CHARACTER)
710        })
711        .collect()
712}
713
714fn utf16_to_string(text: &[u16]) -> String {
715    char::decode_utf16(text.iter().copied())
716        .map(|codepoint| codepoint.unwrap_or(char::REPLACEMENT_CHARACTER))
717        .collect()
718}
719
720fn glyph_id_u16(glyph: GlyphId) -> u16 {
721    u16::try_from(glyph.0).unwrap_or(0)
722}
723
724/// Layout capture from stripped generated engine output.
725#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
726pub struct GeneratedLayoutCapture {
727    /// Number of times a page was built and sent to output.
728    pub page_builds: usize,
729    /// Number of completed shipout calls.
730    pub shipouts: usize,
731    /// Number of `\special` outputs recorded.
732    pub special_outputs: usize,
733    /// Number of picture file loads.
734    pub picture_loads: usize,
735    /// Number of source special whatsits emitted.
736    pub source_specials: usize,
737    /// Number of `\write` whatsit diagnostics recorded.
738    pub write_whatsit_diagnostics: usize,
739    /// Number of pdf extension calls recorded.
740    pub pdf_extensions: usize,
741    /// Total bytes written to the engine transcript.
742    pub transcript_bytes: usize,
743    /// Number of times the page top was pruned.
744    pub page_top_prunes: usize,
745    /// Node handle of the most recent shipped out box, if any.
746    pub last_shipout_box: Option<GeneratedNodeHandle>,
747    /// Node handle of the captured fragment root, if any.
748    pub captured_fragment_root: Option<GeneratedNodeHandle>,
749    /// Final abort status code from the engine, if it aborted.
750    pub last_abort_status: Option<i32>,
751}
752
753impl GeneratedLayoutCapture {
754    /// Reads capture statistics directly from a running engine.
755    #[must_use]
756    pub fn from_engine(engine: &mathtex_portable_engine_generated::PortableTexEngine<'_>) -> Self {
757        Self {
758            page_builds: engine.stripped_page_build_count(),
759            shipouts: engine.stripped_shipout_count(),
760            special_outputs: engine.stripped_special_output_count(),
761            picture_loads: engine.stripped_picture_load_count(),
762            source_specials: engine.stripped_source_special_count(),
763            write_whatsit_diagnostics: engine.stripped_write_whatsit_diagnostic_count(),
764            pdf_extensions: engine.stripped_pdf_extension_count(),
765            transcript_bytes: engine.transcript_bytes().len(),
766            page_top_prunes: engine.stripped_page_top_prune_count(),
767            last_shipout_box: engine.last_stripped_shipout_box(),
768            captured_fragment_root: engine.captured_fragment_root(),
769            last_abort_status: engine.last_abort_status(),
770        }
771    }
772
773    /// Snapshots the last shipped out box, or `None` if none was shipped.
774    #[must_use]
775    pub fn last_shipout_snapshot(
776        &self,
777        engine: &mathtex_portable_engine_generated::PortableTexEngine<'_>,
778    ) -> Option<GeneratedNodeSnapshot> {
779        engine.snapshot_node(self.last_shipout_box?)
780    }
781
782    /// Converts the captured output to an IR `Fragment`.
783    #[must_use]
784    pub fn to_fragment(
785        &self,
786        engine: &mathtex_portable_engine_generated::PortableTexEngine<'_>,
787        metadata: FragmentMetadata,
788    ) -> Option<Fragment> {
789        let root = self.captured_fragment_root.or(self.last_shipout_box)?;
790        let mut builder = GeneratedIrBuilder::new(engine, metadata);
791        let root_node = builder.emit_node(root, Point::default(), None)?;
792        builder.finish_with_root(root_node)
793    }
794
795    /// Source provenance parameters are unused because this path does not thread source spans yet.
796    #[must_use]
797    pub fn to_fragment_with_source(
798        &self,
799        engine: &mathtex_portable_engine_generated::PortableTexEngine<'_>,
800        metadata: FragmentMetadata,
801        _source_name: impl Into<String>,
802        _source_span: ByteSpan,
803    ) -> Option<Fragment> {
804        let root = self.captured_fragment_root.or(self.last_shipout_box)?;
805        let mut builder = GeneratedIrBuilder::new(engine, metadata);
806        let root_node = builder.emit_node(root, Point::default(), None)?;
807        builder.finish_with_root(root_node)
808    }
809
810    /// Converts captured output to an IR `Fragment` using source spans recorded in the engine.
811    #[must_use]
812    pub fn to_fragment_with_recorded_source(
813        &self,
814        engine: &mathtex_portable_engine_generated::PortableTexEngine<'_>,
815        metadata: FragmentMetadata,
816    ) -> Option<Fragment> {
817        let root = self.captured_fragment_root.or(self.last_shipout_box)?;
818        let mut builder = GeneratedIrBuilder::new(engine, metadata);
819        let root_node = builder.emit_node(root, Point::default(), None)?;
820        builder.finish_with_root(root_node)
821    }
822}
823
824/// Converts a single engine node and its children to an IR `Fragment`.
825#[must_use]
826pub fn generated_node_to_fragment(
827    engine: &mathtex_portable_engine_generated::PortableTexEngine<'_>,
828    root: GeneratedNodeHandle,
829    metadata: FragmentMetadata,
830) -> Option<Fragment> {
831    let mut builder = GeneratedIrBuilder::new(engine, metadata);
832    let root_node = builder.emit_node(root, Point::default(), None)?;
833    builder.finish_with_root(root_node)
834}
835
836/// Host fonts get ids from this base upward so they never collide with engine font numbers.
837const HOST_FONT_ID_BASE: u32 = 0x8000_0000;
838
839struct GeneratedIrBuilder<'a, 'resources> {
840    engine: &'a mathtex_portable_engine_generated::PortableTexEngine<'resources>,
841    fragment: Fragment,
842    next_node: u32,
843    visits_remaining: usize,
844    /// Host font name to synthetic `FontId` interning, one id per distinct name.
845    host_font_ids: BTreeMap<String, u32>,
846}
847
848impl<'a, 'resources> GeneratedIrBuilder<'a, 'resources> {
849    fn new(
850        engine: &'a mathtex_portable_engine_generated::PortableTexEngine<'resources>,
851        metadata: FragmentMetadata,
852    ) -> Self {
853        Self {
854            engine,
855            fragment: Fragment {
856                metadata,
857                ..Fragment::default()
858            },
859            next_node: 0,
860            visits_remaining: 16_384,
861            host_font_ids: BTreeMap::new(),
862        }
863    }
864
865    fn host_font_id(&mut self, name: &str) -> FontId {
866        let next = HOST_FONT_ID_BASE.saturating_add(self.host_font_ids.len() as u32);
867        FontId(*self.host_font_ids.entry(name.into()).or_insert(next))
868    }
869
870    fn finish_with_root(mut self, root: NodeId) -> Option<Fragment> {
871        let root_node = self.fragment.node(root)?;
872        if let LayoutNodeKind::Box(layout_box) = &root_node.kind {
873            self.fragment.surface = Surface {
874                width: layout_box.metrics.width,
875                height: layout_box.metrics.total_height(),
876                baseline: layout_box.metrics.height,
877            };
878        }
879        Some(self.fragment)
880    }
881
882    fn emit_node(
883        &mut self,
884        handle: GeneratedNodeHandle,
885        origin: Point,
886        parent: Option<ParentBox>,
887    ) -> Option<NodeId> {
888        self.consume_visit()?;
889        let snapshot = self.engine.snapshot_node(handle)?;
890        match snapshot.kind {
891            GeneratedNodeKind::HorizontalBox
892            | GeneratedNodeKind::VerticalBox
893            | GeneratedNodeKind::UnsetBox => self.emit_box(snapshot, origin),
894            GeneratedNodeKind::Rule => {
895                // Running rule dimensions inherit from the enclosing box.
896                const NULL_FLAG: i32 = -1_073_741_824; // -2^30
897                let mut width = snapshot.width;
898                let mut height = snapshot.height;
899                let mut depth = snapshot.depth;
900                if let Some(p) = parent {
901                    if p.vertical {
902                        if width == NULL_FLAG {
903                            width = p.width;
904                        }
905                    } else {
906                        if height == NULL_FLAG {
907                            height = p.height;
908                        }
909                        if depth == NULL_FLAG {
910                            depth = p.depth;
911                        }
912                    }
913                }
914                Some(self.emit(
915                    &snapshot,
916                    origin,
917                    LayoutNodeKind::Rule(Rule {
918                        size: Size {
919                            width: Length::from_scaled_points(width),
920                            height: Length::from_scaled_points(height + depth),
921                        },
922                        color: Default::default(),
923                    }),
924                ))
925            }
926            GeneratedNodeKind::Glue => Some(self.emit(
927                &snapshot,
928                origin,
929                LayoutNodeKind::Glue(Glue {
930                    amount: Length::from_scaled_points(snapshot.width),
931                }),
932            )),
933            GeneratedNodeKind::Kern => Some(self.emit(
934                &snapshot,
935                origin,
936                LayoutNodeKind::Kern(Kern {
937                    amount: Length::from_scaled_points(snapshot.width),
938                }),
939            )),
940            GeneratedNodeKind::Character => {
941                let cluster: Option<ByteSpan> = None;
942                Some(self.emit(
943                    &snapshot,
944                    origin,
945                    LayoutNodeKind::GlyphRun(GlyphRun {
946                        font: FontRef {
947                            id: FontId(snapshot.font as u32),
948                            name: self
949                                .engine
950                                .native_font_spec(snapshot.font)
951                                .or_else(|| self.engine.font_name(snapshot.font))
952                                .unwrap_or_else(|| format!("generated-font-{}", snapshot.font)),
953                            size: Length::from_scaled_points(self.engine.font_at_size(snapshot.font)),
954                            features: Vec::new(),
955                        },
956                        direction: Direction::LeftToRight,
957                        script: None,
958                        language: None,
959                        glyphs: Vec::from([PositionedGlyph {
960                            glyph_id: GlyphId(snapshot.character as u32),
961                            offset: Point::default(),
962                            advance: Point {
963                                x: Length::from_scaled_points(snapshot.width),
964                                y: Length::ZERO,
965                            },
966                            cluster,
967                        }]),
968                    }),
969                ))
970            }
971            GeneratedNodeKind::NativeWord | GeneratedNodeKind::NativeGlyph => {
972                Some(self.emit_native_glyph_run(snapshot, origin))
973            }
974            GeneratedNodeKind::HostBoxRef => self.emit_host_box(snapshot, origin),
975            GeneratedNodeKind::OutputWhatsit => None,
976            _ => None,
977        }
978    }
979
980    /// Expands a host box marker into a box holding the host supplied glyph runs and rules.
981    fn emit_host_box(&mut self, snapshot: GeneratedNodeSnapshot, origin: Point) -> Option<NodeId> {
982        let record = self
983            .engine
984            .host_box_record(usize::try_from(snapshot.character).ok()?)?
985            .clone();
986        let mut children = Vec::new();
987        for run in &record.runs {
988            let glyphs = run
989                .glyphs
990                .iter()
991                .map(|glyph| PositionedGlyph {
992                    glyph_id: GlyphId(u32::from(glyph.glyph)),
993                    offset: Point {
994                        x: Length::from_scaled_points(glyph.x),
995                        y: Length::from_scaled_points(glyph.y),
996                    },
997                    advance: Point {
998                        x: Length::from_scaled_points(glyph.advance),
999                        y: Length::ZERO,
1000                    },
1001                    cluster: None,
1002                })
1003                .collect();
1004            // Host fonts have no engine font table entry, so intern the name to a synthetic id.
1005            let font_id = self.host_font_id(&run.font_name);
1006            let child = self.push_host_box_child(
1007                Point::default(),
1008                Size {
1009                    width: Length::from_scaled_points(record.width),
1010                    height: Length::from_scaled_points(record.height + record.depth),
1011                },
1012                LayoutNodeKind::GlyphRun(GlyphRun {
1013                    font: FontRef {
1014                        id: font_id,
1015                        name: run.font_name.clone(),
1016                        size: Length::from_scaled_points(run.font_size),
1017                        features: Vec::new(),
1018                    },
1019                    direction: Direction::LeftToRight,
1020                    script: None,
1021                    language: None,
1022                    glyphs,
1023                }),
1024            );
1025            children.push(child);
1026        }
1027        for rule in &record.rules {
1028            let child = self.push_host_box_child(
1029                Point {
1030                    x: Length::from_scaled_points(rule.x),
1031                    y: Length::from_scaled_points(rule.y),
1032                },
1033                Size {
1034                    width: Length::from_scaled_points(rule.width),
1035                    height: Length::from_scaled_points(rule.height),
1036                },
1037                LayoutNodeKind::Rule(Rule {
1038                    size: Size {
1039                        width: Length::from_scaled_points(rule.width),
1040                        height: Length::from_scaled_points(rule.height),
1041                    },
1042                    color: Default::default(),
1043                }),
1044            );
1045            children.push(child);
1046        }
1047        Some(self.emit(
1048            &snapshot,
1049            origin,
1050            LayoutNodeKind::Box(LayoutBox {
1051                kind: BoxKind::Horizontal,
1052                metrics: BoxMetrics {
1053                    width: Length::from_scaled_points(record.width),
1054                    height: Length::from_scaled_points(record.height),
1055                    depth: Length::from_scaled_points(record.depth),
1056                    shift: Length::ZERO,
1057                },
1058                children,
1059            }),
1060        ))
1061    }
1062
1063    /// Pushes one host box child node, source coverage comes from the enclosing box.
1064    fn push_host_box_child(&mut self, origin: Point, size: Size, kind: LayoutNodeKind) -> NodeId {
1065        let id = NodeId(self.next_node);
1066        self.next_node += 1;
1067        self.fragment.nodes.push(LayoutNode {
1068            id,
1069            origin,
1070            bounds: Rect {
1071                origin: Point::default(),
1072                size,
1073            },
1074            primary_source: None,
1075            style: Style::default(),
1076            kind,
1077        });
1078        id
1079    }
1080
1081    fn emit_native_glyph_run(&mut self, snapshot: GeneratedNodeSnapshot, origin: Point) -> NodeId {
1082        let cluster: Option<ByteSpan> = None;
1083        let glyphs = if snapshot.native_glyphs.is_empty() {
1084            Vec::from([PositionedGlyph {
1085                glyph_id: GlyphId(snapshot.character as u32),
1086                offset: Point::default(),
1087                advance: Point {
1088                    x: Length::from_scaled_points(snapshot.width),
1089                    y: Length::ZERO,
1090                },
1091                cluster,
1092            }])
1093        } else {
1094            snapshot
1095                .native_glyphs
1096                .iter()
1097                .map(|glyph| PositionedGlyph {
1098                    glyph_id: GlyphId(u32::from(glyph.glyph_id)),
1099                    offset: Point {
1100                        x: Length::from_scaled_points(glyph.x),
1101                        y: Length::from_scaled_points(glyph.y),
1102                    },
1103                    advance: Point {
1104                        x: Length::from_scaled_points(glyph.advance),
1105                        y: Length::ZERO,
1106                    },
1107                    // Glyph source spans are set by `src_resolve_native_glyphs`, 0/0 means unmapped.
1108                    cluster: if glyph.src_end > glyph.src_start {
1109                        Some(ByteSpan {
1110                            start: glyph.src_start,
1111                            end: glyph.src_end,
1112                        })
1113                    } else {
1114                        None
1115                    },
1116                })
1117                .collect()
1118        };
1119
1120        self.emit(
1121            &snapshot,
1122            origin,
1123            LayoutNodeKind::GlyphRun(GlyphRun {
1124                font: FontRef {
1125                    id: FontId(snapshot.font as u32),
1126                    name: self
1127                        .engine
1128                        .native_font_spec(snapshot.font)
1129                        .or_else(|| self.engine.font_name(snapshot.font))
1130                        .unwrap_or_else(|| format!("generated-font-{}", snapshot.font)),
1131                    size: Length::from_scaled_points(self.engine.font_at_size(snapshot.font)),
1132                    features: Vec::new(),
1133                },
1134                direction: Direction::LeftToRight,
1135                script: None,
1136                language: None,
1137                glyphs,
1138            }),
1139        )
1140    }
1141
1142    fn emit_box(&mut self, snapshot: GeneratedNodeSnapshot, origin: Point) -> Option<NodeId> {
1143        let children = self.emit_children(
1144            snapshot.list,
1145            snapshot.kind,
1146            snapshot.width,
1147            snapshot.height,
1148            snapshot.depth,
1149            snapshot.glue_set,
1150            snapshot.glue_sign,
1151            snapshot.glue_order,
1152        );
1153        let kind = match snapshot.kind {
1154            GeneratedNodeKind::VerticalBox => BoxKind::Vertical,
1155            GeneratedNodeKind::HorizontalBox => BoxKind::Horizontal,
1156            GeneratedNodeKind::UnsetBox => BoxKind::Math,
1157            _ => BoxKind::Math,
1158        };
1159        Some(self.emit_with_primary_source(
1160            &snapshot,
1161            origin,
1162            LayoutNodeKind::Box(LayoutBox {
1163                kind,
1164                metrics: BoxMetrics {
1165                    width: Length::from_scaled_points(snapshot.width),
1166                    height: Length::from_scaled_points(snapshot.height),
1167                    depth: Length::from_scaled_points(snapshot.depth),
1168                    shift: Length::from_scaled_points(snapshot.shift),
1169                },
1170                children,
1171            }),
1172            None,
1173        ))
1174    }
1175
1176    fn emit_children(
1177        &mut self,
1178        first: Option<GeneratedNodeHandle>,
1179        parent_kind: GeneratedNodeKind,
1180        parent_width: i32,
1181        parent_height: i32,
1182        parent_depth: i32,
1183        glue_set: f64,
1184        glue_sign: i32,
1185        glue_order: i32,
1186    ) -> Vec<NodeId> {
1187        // Replicates TeX `hlist_out` and `vlist_out` cursor advancement with shifts folded into `origin`.
1188        let mut children = Vec::new();
1189        let vertical = matches!(parent_kind, GeneratedNodeKind::VerticalBox);
1190        let mut cursor = first;
1191        let mut cur_h = 0i32;
1192        let mut cur_v = if vertical { -parent_height } else { 0 };
1193        while let Some(handle) = cursor {
1194            let Some(snapshot) = self.engine.snapshot_node(handle) else {
1195                break;
1196            };
1197            let true_box = matches!(
1198                snapshot.kind,
1199                GeneratedNodeKind::HorizontalBox
1200                    | GeneratedNodeKind::VerticalBox
1201                    | GeneratedNodeKind::UnsetBox
1202            );
1203            let has_extent = true_box
1204                || matches!(
1205                    snapshot.kind,
1206                    GeneratedNodeKind::Rule
1207                        | GeneratedNodeKind::Character
1208                        | GeneratedNodeKind::NativeWord
1209                        | GeneratedNodeKind::NativeGlyph
1210                        | GeneratedNodeKind::Ligature
1211                );
1212            let is_spacing = matches!(
1213                snapshot.kind,
1214                GeneratedNodeKind::Kern | GeneratedNodeKind::Glue
1215            );
1216            // Glue advances by its set width after `glue_set`, other nodes advance by their own width.
1217            let advance = if matches!(snapshot.kind, GeneratedNodeKind::Glue) {
1218                let natural = snapshot.width;
1219                if glue_sign == 1 && snapshot.glue_stretch_order == glue_order {
1220                    natural + (glue_set * f64::from(snapshot.glue_stretch)).round() as i32
1221                } else if glue_sign == 2 && snapshot.glue_shrink_order == glue_order {
1222                    natural - (glue_set * f64::from(snapshot.glue_shrink)).round() as i32
1223                } else {
1224                    natural
1225                }
1226            } else {
1227                snapshot.width
1228            };
1229            let origin = if vertical {
1230                if is_spacing {
1231                    let origin = Point {
1232                        x: Length::ZERO,
1233                        y: Length::from_scaled_points(cur_v),
1234                    };
1235                    cur_v += advance;
1236                    origin
1237                } else if has_extent {
1238                    cur_v += snapshot.height;
1239                    let origin = Point {
1240                        x: Length::from_scaled_points(if true_box { snapshot.shift } else { 0 }),
1241                        y: Length::from_scaled_points(cur_v),
1242                    };
1243                    cur_v += snapshot.depth;
1244                    origin
1245                } else {
1246                    // Penalty / mark / whatsit: recorded but contributes no extent.
1247                    Point {
1248                        x: Length::ZERO,
1249                        y: Length::from_scaled_points(cur_v),
1250                    }
1251                }
1252            } else {
1253                let origin = Point {
1254                    x: Length::from_scaled_points(cur_h),
1255                    y: Length::from_scaled_points(if true_box { snapshot.shift } else { 0 }),
1256                };
1257                cur_h += advance;
1258                origin
1259            };
1260            let parent = ParentBox {
1261                vertical,
1262                width: parent_width,
1263                height: parent_height,
1264                depth: parent_depth,
1265            };
1266            if let Some(child) = self.emit_node(handle, origin, Some(parent)) {
1267                children.push(child);
1268            }
1269            cursor = snapshot.link;
1270        }
1271        children
1272    }
1273
1274    fn emit(
1275        &mut self,
1276        snapshot: &GeneratedNodeSnapshot,
1277        origin: Point,
1278        kind: LayoutNodeKind,
1279    ) -> NodeId {
1280        self.emit_with_primary_source(snapshot, origin, kind, None)
1281    }
1282
1283    fn emit_with_primary_source(
1284        &mut self,
1285        snapshot: &GeneratedNodeSnapshot,
1286        origin: Point,
1287        kind: LayoutNodeKind,
1288        _preferred_primary_source: Option<SourceRange>,
1289    ) -> NodeId {
1290        let id = NodeId(self.next_node);
1291        self.next_node += 1;
1292        // Entries are recorded as Primary so node source lookup works uniformly.
1293        let primary_source = snapshot.source.as_ref().map(|span| {
1294            let source = self.fragment.source_map.intern_source(span.name.clone());
1295            SourceRange {
1296                source,
1297                span: ByteSpan {
1298                    start: span.start,
1299                    end: span.end,
1300                },
1301            }
1302        });
1303        if let Some(range) = primary_source {
1304            self.fragment
1305                .source_map
1306                .add_entry(id, range, SourceRole::Primary);
1307            // Emit enclosing construct spans after the leaf so consumers can choose wider matches.
1308            for span in self.engine.node_enclosing_spans(snapshot.handle) {
1309                let source = self.fragment.source_map.intern_source(span.name.clone());
1310                let enclosing = SourceRange {
1311                    source,
1312                    span: ByteSpan {
1313                        start: span.start,
1314                        end: span.end,
1315                    },
1316                };
1317                self.fragment
1318                    .source_map
1319                    .add_entry(id, enclosing, SourceRole::EnclosingConstruct);
1320            }
1321        }
1322        self.fragment.nodes.push(LayoutNode {
1323            id,
1324            origin,
1325            bounds: Rect {
1326                origin: Point::default(),
1327                size: Size {
1328                    width: Length::from_scaled_points(snapshot.width),
1329                    height: Length::from_scaled_points(snapshot.height + snapshot.depth),
1330                },
1331            },
1332            primary_source,
1333            style: Style::default(),
1334            kind,
1335        });
1336        id
1337    }
1338
1339    fn consume_visit(&mut self) -> Option<()> {
1340        if self.visits_remaining == 0 {
1341            return None;
1342        }
1343        self.visits_remaining -= 1;
1344        Some(())
1345    }
1346}
1347
1348/// A cached snapshot of a generated engine format image for repeated instantiation.
1349#[derive(Clone)]
1350pub struct GeneratedFormatCache {
1351    image: mathtex_portable_engine_generated::PortableFormatImage,
1352}
1353
1354impl GeneratedFormatCache {
1355    /// Returns an empty format cache with no initialized state.
1356    #[must_use]
1357    pub fn empty() -> Self {
1358        Self {
1359            image: mathtex_portable_engine_generated::PortableFormatImage::empty(),
1360        }
1361    }
1362
1363    /// Initializes a generated engine once and snapshots it as a reusable format cache.
1364    #[must_use]
1365    pub fn initialized(profile: mathtex_portable_engine_generated::EngineProfile) -> Self {
1366        let mut engine = mathtex_portable_engine_generated::PortableTexEngine::from_format(
1367            profile,
1368            &mathtex_portable_engine_generated::PortableFormatImage::empty(),
1369            mathtex_portable_engine_generated::EmptyResourceProvider,
1370        );
1371        assert!(
1372            engine.initialize_format_state(),
1373            "generated engine base initialization failed"
1374        );
1375        Self::from_engine_owned(engine)
1376    }
1377
1378    /// Snapshots the engine's current format state into a reusable cache.
1379    #[must_use]
1380    pub fn from_engine(engine: &mathtex_portable_engine_generated::PortableTexEngine<'_>) -> Self {
1381        Self {
1382            image: engine.snapshot_format(),
1383        }
1384    }
1385
1386    /// Consumes an engine into a format cache by moving its state in place, avoiding a full clone.
1387    #[must_use]
1388    pub fn from_engine_owned(
1389        engine: mathtex_portable_engine_generated::PortableTexEngine<'_>,
1390    ) -> Self {
1391        Self {
1392            image: engine.into_format(),
1393        }
1394    }
1395
1396    /// Returns a reference to the raw format image.
1397    #[must_use]
1398    pub fn image(&self) -> &mathtex_portable_engine_generated::PortableFormatImage {
1399        &self.image
1400    }
1401
1402    /// Serializes the format image to a buffer valid for the same build target only.
1403    #[must_use]
1404    pub fn to_bytes(&self) -> Vec<u8> {
1405        self.image.to_bytes()
1406    }
1407
1408    /// Deserializes a format cache from a buffer produced by [`Self::to_bytes`].
1409    #[must_use]
1410    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
1411        Some(Self {
1412            image: mathtex_portable_engine_generated::PortableFormatImage::from_bytes(bytes)?,
1413        })
1414    }
1415
1416    /// Creates a new engine instance from this cached format image.
1417    #[must_use]
1418    pub fn instantiate<'resources, R>(
1419        &self,
1420        profile: mathtex_portable_engine_generated::EngineProfile,
1421        resources: R,
1422    ) -> mathtex_portable_engine_generated::PortableTexEngine<'resources>
1423    where
1424        R: mathtex_portable_engine_generated::ResourceProvider + 'resources,
1425    {
1426        mathtex_portable_engine_generated::PortableTexEngine::from_format(
1427            profile,
1428            &self.image,
1429            resources,
1430        )
1431    }
1432}
1433
1434impl Default for GeneratedFormatCache {
1435    fn default() -> Self {
1436        Self::empty()
1437    }
1438}
1439
1440/// Magic header for packaged format files: `MTXPKG` followed by version and compression bytes.
1441const PACKAGED_FORMAT_MAGIC: &[u8; 6] = b"MTXPKG";
1442const PACKAGED_FORMAT_VERSION: u8 = 2;
1443const PACKAGED_COMPRESSION_DEFLATE: u8 = 1;
1444/// DEFLATE level for packaging, only the compressor pays this cost, not the reader.
1445const PACKAGED_DEFLATE_LEVEL: u8 = 7;
1446
1447fn encode_packaged_payload(
1448    format_bytes: &[u8],
1449    font_table: &[(mathtex_portable_engine_generated::PortableFontHandle, String, i32)],
1450) -> Vec<u8> {
1451    let mut out = Vec::with_capacity(format_bytes.len() + 64);
1452    out.extend_from_slice(&(format_bytes.len() as u64).to_le_bytes());
1453    out.extend_from_slice(format_bytes);
1454    out.extend_from_slice(&(font_table.len() as u32).to_le_bytes());
1455    for (handle, spec, size) in font_table {
1456        out.extend_from_slice(&(*handle as u64).to_le_bytes());
1457        out.extend_from_slice(&size.to_le_bytes());
1458        let spec = spec.as_bytes();
1459        out.extend_from_slice(&(spec.len() as u32).to_le_bytes());
1460        out.extend_from_slice(spec);
1461    }
1462    out
1463}
1464
1465/// Packs format image bytes and a font table into a DEFLATE compressed buffer.
1466#[must_use]
1467pub fn pack_packaged_format(
1468    format_bytes: &[u8],
1469    font_table: &[(mathtex_portable_engine_generated::PortableFontHandle, String, i32)],
1470) -> Vec<u8> {
1471    let payload = encode_packaged_payload(format_bytes, font_table);
1472    let compressed = miniz_oxide::deflate::compress_to_vec(&payload, PACKAGED_DEFLATE_LEVEL);
1473    let mut out = Vec::with_capacity(compressed.len() + 16);
1474    out.extend_from_slice(PACKAGED_FORMAT_MAGIC);
1475    out.push(PACKAGED_FORMAT_VERSION);
1476    out.push(PACKAGED_COMPRESSION_DEFLATE);
1477    out.extend_from_slice(&(payload.len() as u64).to_le_bytes());
1478    out.extend_from_slice(&compressed);
1479    out
1480}
1481
1482/// Decompresses a buffer from [`pack_packaged_format`] into format bytes and the font table.
1483#[must_use]
1484pub fn unpack_packaged_format(
1485    bytes: &[u8],
1486) -> Option<(
1487    Vec<u8>,
1488    Vec<(mathtex_portable_engine_generated::PortableFontHandle, String, i32)>,
1489)> {
1490    fn take<'a>(b: &'a [u8], cursor: &mut usize, n: usize) -> Option<&'a [u8]> {
1491        let slice = b.get(*cursor..*cursor + n)?;
1492        *cursor += n;
1493        Some(slice)
1494    }
1495    let mut cursor = 0usize;
1496    if take(bytes, &mut cursor, 6)? != PACKAGED_FORMAT_MAGIC {
1497        return None;
1498    }
1499    if *bytes.get(cursor)? != PACKAGED_FORMAT_VERSION {
1500        return None;
1501    }
1502    cursor += 1;
1503    let compression = *bytes.get(cursor)?;
1504    cursor += 1;
1505    let payload_len = u64::from_le_bytes(take(bytes, &mut cursor, 8)?.try_into().ok()?) as usize;
1506    let compressed = bytes.get(cursor..)?;
1507    let payload = match compression {
1508        PACKAGED_COMPRESSION_DEFLATE => {
1509            miniz_oxide::inflate::decompress_to_vec(compressed).ok()?
1510        }
1511        0 => compressed.to_vec(),
1512        _ => return None,
1513    };
1514    if payload.len() != payload_len {
1515        return None;
1516    }
1517
1518    let mut cursor = 0usize;
1519    let format_len = u64::from_le_bytes(take(&payload, &mut cursor, 8)?.try_into().ok()?) as usize;
1520    let format_bytes = take(&payload, &mut cursor, format_len)?.to_vec();
1521    let font_count = u32::from_le_bytes(take(&payload, &mut cursor, 4)?.try_into().ok()?) as usize;
1522    let mut font_table = Vec::with_capacity(font_count);
1523    for _ in 0..font_count {
1524        let handle = u64::from_le_bytes(take(&payload, &mut cursor, 8)?.try_into().ok()?) as usize;
1525        let size = i32::from_le_bytes(take(&payload, &mut cursor, 4)?.try_into().ok()?);
1526        let spec_len = u32::from_le_bytes(take(&payload, &mut cursor, 4)?.try_into().ok()?) as usize;
1527        let spec = String::from_utf8(take(&payload, &mut cursor, spec_len)?.to_vec()).ok()?;
1528        font_table.push((handle, spec, size));
1529    }
1530    Some((format_bytes, font_table))
1531}
1532
1533impl fmt::Debug for GeneratedFormatCache {
1534    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1535        formatter
1536            .debug_struct("GeneratedFormatCache")
1537            .finish_non_exhaustive()
1538    }
1539}
1540
1541/// Adapter that routes generated engine resource requests through the engine [`ResourceProvider`].
1542#[derive(Clone, Debug)]
1543pub struct GeneratedResourceProvider<R> {
1544    inner: R,
1545}
1546
1547impl<R> GeneratedResourceProvider<R> {
1548    /// Wraps `inner` in the resource provider adapter.
1549    #[must_use]
1550    pub fn new(inner: R) -> Self {
1551        Self { inner }
1552    }
1553
1554    /// Returns a reference to the inner resource provider.
1555    #[must_use]
1556    pub fn inner(&self) -> &R {
1557        &self.inner
1558    }
1559
1560    /// Unwraps and returns the inner resource provider.
1561    #[must_use]
1562    pub fn into_inner(self) -> R {
1563        self.inner
1564    }
1565}
1566
1567impl<R> mathtex_portable_engine_generated::ResourceProvider for GeneratedResourceProvider<R>
1568where
1569    R: ResourceProvider,
1570{
1571    fn read(
1572        &mut self,
1573        generated_request: mathtex_portable_engine_generated::ResourceRequest<'_>,
1574    ) -> Option<Vec<u8>> {
1575        let name = normalized_generated_resource_name(generated_request.name);
1576        let kind = generated_kind_to_engine_kind(generated_request.kind)?;
1577        let mut request = if kind == ResourceKind::Asset {
1578            generated_request.package.map_or_else(
1579                || EngineResourceRequest::new(name, kind),
1580                |package| EngineResourceRequest::asset(package, name),
1581            )
1582        } else {
1583            EngineResourceRequest::new(name, kind)
1584        };
1585        if let Some(source) = request_source_from_generated(generated_request.source.as_ref()) {
1586            request = request.with_source(source.name, source.span);
1587        }
1588        let resource = read_generated_resource_request(&self.inner, &request).or_else(|| {
1589            if kind != ResourceKind::Asset {
1590                return None;
1591            }
1592            let mut fallback = EngineResourceRequest::new(name, ResourceKind::TexInput);
1593            if let Some(source) = request_source_from_generated(generated_request.source.as_ref()) {
1594                fallback = fallback.with_source(source.name, source.span);
1595            }
1596            read_generated_resource_request(&self.inner, &fallback)
1597        })?;
1598        Some(resource.bytes)
1599    }
1600}
1601
1602fn read_generated_resource_request<R>(
1603    provider: &R,
1604    request: &EngineResourceRequest,
1605) -> Option<crate::resource::Resource>
1606where
1607    R: ResourceProvider,
1608{
1609    if let Ok(resource) = provider.read_request(request) {
1610        return Some(resource);
1611    }
1612
1613    for name in generated_resource_candidate_names(request.name.as_str(), request.kind) {
1614        let mut candidate = request.clone();
1615        candidate.name = name;
1616        if let Ok(resource) = provider.read_request(&candidate) {
1617            return Some(resource);
1618        }
1619    }
1620
1621    None
1622}
1623
1624fn generated_resource_candidate_names(name: &str, kind: ResourceKind) -> Vec<String> {
1625    if resource_name_has_extension(name) {
1626        return Vec::new();
1627    }
1628
1629    let suffixes: &[&str] = match kind {
1630        ResourceKind::TexInput => &[".tex", ".ltx"],
1631        ResourceKind::Package => &[".sty"],
1632        ResourceKind::Class => &[".cls"],
1633        ResourceKind::FontDefinition => &[".fd"],
1634        ResourceKind::Font => &[".tfm", ".otf", ".ttf"],
1635        ResourceKind::Encoding => &[".enc"],
1636        ResourceKind::Map => &[".map"],
1637        ResourceKind::PackageSupport
1638        | ResourceKind::Config
1639        | ResourceKind::FormatImage
1640        | ResourceKind::Asset => &[],
1641    };
1642
1643    suffixes
1644        .iter()
1645        .map(|suffix| {
1646            let mut candidate = String::with_capacity(name.len() + suffix.len());
1647            candidate.push_str(name);
1648            candidate.push_str(suffix);
1649            candidate
1650        })
1651        .collect()
1652}
1653
1654fn resource_name_has_extension(name: &str) -> bool {
1655    let last_separator = name.rfind(['/', '\\']).map_or(0, |index| index + 1);
1656    name[last_separator..].contains('.')
1657}
1658
1659fn normalized_generated_resource_name(mut name: &str) -> &str {
1660    while let Some(stripped) = name.strip_prefix("./") {
1661        name = stripped;
1662    }
1663    name
1664}
1665
1666fn request_source_from_generated(
1667    source: Option<&mathtex_portable_engine_generated::PortableSourceSpan>,
1668) -> Option<crate::resource::ResourceRequestSource> {
1669    let source = source?;
1670    Some(crate::resource::ResourceRequestSource {
1671        name: source.name.clone(),
1672        span: ByteSpan {
1673            start: source.start,
1674            end: source.end,
1675        },
1676    })
1677}
1678
1679fn generated_kind_to_engine_kind(
1680    kind: mathtex_portable_engine_generated::ResourceKind,
1681) -> Option<ResourceKind> {
1682    match kind {
1683        mathtex_portable_engine_generated::ResourceKind::TexInput => Some(ResourceKind::TexInput),
1684        mathtex_portable_engine_generated::ResourceKind::Package => Some(ResourceKind::Package),
1685        mathtex_portable_engine_generated::ResourceKind::Class => Some(ResourceKind::Class),
1686        mathtex_portable_engine_generated::ResourceKind::FontDefinition => {
1687            Some(ResourceKind::FontDefinition)
1688        }
1689        mathtex_portable_engine_generated::ResourceKind::PackageSupport => {
1690            Some(ResourceKind::PackageSupport)
1691        }
1692        mathtex_portable_engine_generated::ResourceKind::Font => Some(ResourceKind::Font),
1693        mathtex_portable_engine_generated::ResourceKind::Encoding => Some(ResourceKind::Encoding),
1694        mathtex_portable_engine_generated::ResourceKind::Map => Some(ResourceKind::Map),
1695        mathtex_portable_engine_generated::ResourceKind::Config => Some(ResourceKind::Config),
1696        mathtex_portable_engine_generated::ResourceKind::FormatImage => {
1697            Some(ResourceKind::FormatImage)
1698        }
1699        mathtex_portable_engine_generated::ResourceKind::Asset => Some(ResourceKind::Asset),
1700        mathtex_portable_engine_generated::ResourceKind::Other(_) => Some(ResourceKind::TexInput),
1701    }
1702}
1703
1704#[cfg(test)]
1705mod tests {
1706    use super::*;
1707    use crate::resource::InMemoryResourceProvider;
1708
1709    #[test]
1710    fn generated_provider_adapter_reads_package_bytes() {
1711        let mut resources = InMemoryResourceProvider::new().with_resource(
1712            "amsmath.sty",
1713            ResourceKind::Package,
1714            br"\ProvidesPackage{amsmath}",
1715        );
1716        resources.insert_request(
1717            EngineResourceRequest::asset("mhchem", "arrows.dat"),
1718            b"asset",
1719        );
1720        let mut generated = GeneratedResourceProvider::new(resources);
1721
1722        let bytes = mathtex_portable_engine_generated::ResourceProvider::read(
1723            &mut generated,
1724            mathtex_portable_engine_generated::ResourceRequest {
1725                name: "amsmath.sty",
1726                kind: mathtex_portable_engine_generated::ResourceKind::Package,
1727                package: None,
1728                format: 26,
1729                mode: "rb",
1730                source: None,
1731            },
1732        )
1733        .expect("generated provider should read package");
1734
1735        assert_eq!(bytes, br"\ProvidesPackage{amsmath}");
1736
1737        let bytes = mathtex_portable_engine_generated::ResourceProvider::read(
1738            &mut generated,
1739            mathtex_portable_engine_generated::ResourceRequest {
1740                name: "arrows.dat",
1741                kind: mathtex_portable_engine_generated::ResourceKind::Asset,
1742                package: Some("mhchem"),
1743                format: 26,
1744                mode: "rb",
1745                source: None,
1746            },
1747        )
1748        .expect("generated provider should read package asset");
1749
1750        assert_eq!(bytes, b"asset");
1751    }
1752
1753    #[test]
1754    fn generated_engine_can_be_constructed_from_cached_format_state() {
1755        let resources = GeneratedResourceProvider::new(InMemoryResourceProvider::new());
1756        let format = GeneratedFormatCache::empty();
1757        let engine = format.instantiate(
1758            mathtex_portable_engine_generated::EngineProfile::xetex(),
1759            resources,
1760        );
1761
1762        assert_eq!(
1763            engine.profile(),
1764            mathtex_portable_engine_generated::EngineProfile::xetex()
1765        );
1766        assert_eq!(engine.resource_request_count(), 0);
1767        assert_eq!(
1768            GeneratedLayoutCapture::from_engine(&engine),
1769            GeneratedLayoutCapture::default()
1770        );
1771        assert_eq!(
1772            GeneratedLayoutCapture::from_engine(&engine).last_shipout_snapshot(&engine),
1773            None
1774        );
1775        assert_eq!(
1776            GeneratedLayoutCapture::from_engine(&engine).to_fragment(
1777                &engine,
1778                FragmentMetadata {
1779                    engine_profile: "xetex".into(),
1780                    format_id: "empty".into(),
1781                    fragment_kind: Default::default(),
1782                },
1783            ),
1784            None
1785        );
1786        assert_eq!(
1787            GeneratedLayoutCapture::from_engine(&engine).to_fragment_with_source(
1788                &engine,
1789                FragmentMetadata {
1790                    engine_profile: "xetex".into(),
1791                    format_id: "empty".into(),
1792                    fragment_kind: Default::default(),
1793                },
1794                "input.tex",
1795                ByteSpan { start: 0, end: 3 },
1796            ),
1797            None
1798        );
1799    }
1800
1801    #[test]
1802    fn generated_format_cache_can_snapshot_initialized_engine_state() {
1803        let cached = GeneratedFormatCache::initialized(
1804            mathtex_portable_engine_generated::EngineProfile::xetex(),
1805        );
1806        let second = cached.instantiate(
1807            mathtex_portable_engine_generated::EngineProfile::xetex(),
1808            GeneratedResourceProvider::new(InMemoryResourceProvider::new()),
1809        );
1810
1811        assert_eq!(
1812            second.profile(),
1813            mathtex_portable_engine_generated::EngineProfile::xetex()
1814        );
1815        assert_eq!(second.resource_request_count(), 0);
1816        assert_eq!(
1817            GeneratedLayoutCapture::from_engine(&second),
1818            GeneratedLayoutCapture::default()
1819        );
1820    }
1821
1822    #[test]
1823    fn generated_engine_runs_main_control_for_primary_end_input() {
1824        let format = GeneratedFormatCache::initialized(
1825            mathtex_portable_engine_generated::EngineProfile::tex(),
1826        );
1827        let mut engine = format.instantiate(
1828            mathtex_portable_engine_generated::EngineProfile::tex(),
1829            GeneratedResourceProvider::new(InMemoryResourceProvider::new()),
1830        );
1831
1832        assert!(engine.begin_primary_input("input.tex", br"\end".to_vec()));
1833        engine.run_format_initialization();
1834        assert_eq!(engine.last_abort_status(), None);
1835    }
1836
1837    #[test]
1838    fn generated_engine_loads_input_through_resource_provider_during_main_control() {
1839        let resources = InMemoryResourceProvider::new().with_resource(
1840            "child.tex",
1841            ResourceKind::TexInput,
1842            br"\relax",
1843        );
1844        let format = GeneratedFormatCache::initialized(
1845            mathtex_portable_engine_generated::EngineProfile::tex(),
1846        );
1847        let mut engine = format.instantiate(
1848            mathtex_portable_engine_generated::EngineProfile::tex(),
1849            GeneratedResourceProvider::new(resources),
1850        );
1851
1852        assert!(engine.begin_primary_input("input.tex", br"\input child.tex \end".to_vec()));
1853        engine.run_main_control();
1854
1855        assert_eq!(engine.last_abort_status(), None);
1856        assert_eq!(engine.resource_request_count(), 1);
1857        let request = &engine.resource_request_records()[0];
1858        assert_eq!(request.name, "child.tex");
1859        assert_eq!(
1860            request.kind,
1861            mathtex_portable_engine_generated::ResourceKind::TexInput
1862        );
1863        assert_eq!(request.byte_len, Some(6));
1864        // Source span tracking was removed, resource requests carry no source.
1865        assert_eq!(request.source, None);
1866    }
1867
1868    #[test]
1869    fn generated_engine_captures_fragment_root_to_ir_without_shipout() {
1870        let format = GeneratedFormatCache::initialized(
1871            mathtex_portable_engine_generated::EngineProfile::tex(),
1872        );
1873        let mut engine = format.instantiate(
1874            mathtex_portable_engine_generated::EngineProfile::tex(),
1875            GeneratedResourceProvider::new(InMemoryResourceProvider::new()),
1876        );
1877
1878        engine.begin_fragment_capture();
1879        assert!(engine.begin_primary_input(
1880            "input.tex",
1881            br"\catcode`{=1 \catcode`}=2 \hbox{\vrule width 1pt height 2pt depth 0pt}\end".to_vec()
1882        ));
1883        engine.run_main_control();
1884        engine.end_fragment_capture();
1885
1886        let capture = GeneratedLayoutCapture::from_engine(&engine);
1887        assert_eq!(capture.last_abort_status, None);
1888        assert!(!String::from_utf8_lossy(engine.transcript_bytes()).contains('!'));
1889        assert_eq!(capture.shipouts, 0);
1890        let register = engine
1891            .captured_fragment_root()
1892            .expect("fragment root should be captured");
1893        let snapshot = engine
1894            .snapshot_node(register)
1895            .expect("captured root should snapshot");
1896        assert_eq!(
1897            snapshot.kind,
1898            mathtex_portable_engine_generated::PortableNodeKind::HorizontalBox
1899        );
1900
1901        let fragment = generated_node_to_fragment(
1902            &engine,
1903            register,
1904            FragmentMetadata {
1905                engine_profile: "tex".into(),
1906                format_id: "generated".into(),
1907                fragment_kind: Default::default(),
1908            },
1909        )
1910        .expect("captured root should convert to IR");
1911
1912        let root = fragment
1913            .nodes
1914            .iter()
1915            .find_map(|node| match &node.kind {
1916                LayoutNodeKind::Box(layout_box) => Some(layout_box),
1917                _ => None,
1918            })
1919            .expect("fragment should contain a root box");
1920        assert_eq!(root.kind, BoxKind::Horizontal);
1921        assert_eq!(root.children.len(), 1);
1922        assert!(fragment
1923            .nodes
1924            .iter()
1925            .any(|node| matches!(node.kind, LayoutNodeKind::Rule(_))));
1926    }
1927
1928    #[test]
1929    fn generated_engine_loads_tfm_and_emits_glyph_ir_without_shipout() {
1930        let resources = InMemoryResourceProvider::new().with_resource(
1931            "cmr10",
1932            ResourceKind::Font,
1933            include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm").to_vec(),
1934        );
1935        let format = GeneratedFormatCache::initialized(
1936            mathtex_portable_engine_generated::EngineProfile::tex(),
1937        );
1938        let mut engine = format.instantiate(
1939            mathtex_portable_engine_generated::EngineProfile::tex(),
1940            GeneratedResourceProvider::new(resources),
1941        );
1942
1943        engine.begin_fragment_capture();
1944        assert!(engine.begin_primary_input(
1945            "input.tex",
1946            br"\catcode`{=1 \catcode`}=2 \font\tenrm=cmr10 \hbox{\tenrm A}\end".to_vec()
1947        ));
1948        engine.run_main_control();
1949        engine.end_fragment_capture();
1950
1951        let capture = GeneratedLayoutCapture::from_engine(&engine);
1952        assert_eq!(capture.last_abort_status, None);
1953        assert!(!String::from_utf8_lossy(engine.transcript_bytes()).contains('!'));
1954        assert_eq!(capture.shipouts, 0);
1955        assert_eq!(engine.resource_request_count(), 1);
1956        let request = &engine.resource_request_records()[0];
1957        assert_eq!(request.name, "cmr10");
1958        assert_eq!(
1959            request.kind,
1960            mathtex_portable_engine_generated::ResourceKind::Font
1961        );
1962        assert!(request.byte_len.is_some());
1963
1964        let fragment = generated_node_to_fragment(
1965            &engine,
1966            engine
1967                .captured_fragment_root()
1968                .expect("fragment root should be captured"),
1969            FragmentMetadata {
1970                engine_profile: "tex".into(),
1971                format_id: "generated".into(),
1972                fragment_kind: Default::default(),
1973            },
1974        )
1975        .expect("font-backed captured root should convert to IR");
1976
1977        let glyph_run = fragment
1978            .nodes
1979            .iter()
1980            .find_map(|node| match &node.kind {
1981                LayoutNodeKind::GlyphRun(run) => Some(run),
1982                _ => None,
1983            })
1984            .expect("font-backed hbox should emit a glyph run");
1985        assert_eq!(glyph_run.glyphs.len(), 1);
1986        assert_eq!(glyph_run.glyphs[0].glyph_id, GlyphId(u32::from(b'A')));
1987        assert!(glyph_run.glyphs[0].advance.x.0 > 0);
1988    }
1989}