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