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