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
17pub type GeneratedNodeHandle = mathtex_portable_engine_generated::PortableNodeHandle;
19
20#[derive(Clone, Copy)]
22struct ParentBox {
23 vertical: bool,
24 width: i32,
25 height: i32,
26 depth: i32,
27}
28
29pub type GeneratedNodeKind = mathtex_portable_engine_generated::PortableNodeKind;
31
32pub type GeneratedNodeSnapshot = mathtex_portable_engine_generated::PortableNodeSnapshot;
34
35pub type GeneratedSourceSpan = mathtex_portable_engine_generated::PortableSourceSpan;
37
38pub type GeneratedNodeSourceSpan = mathtex_portable_engine_generated::PortableNodeSourceSpan;
40
41pub type GeneratedResourceRequestRecord =
43 mathtex_portable_engine_generated::PortableResourceRequestRecord;
44
45pub type GeneratedFontHandle = mathtex_portable_engine_generated::PortableFontHandle;
47
48pub type GeneratedFontMetrics = mathtex_portable_engine_generated::PortableFontMetrics;
50
51pub type GeneratedNativeGlyph = mathtex_portable_engine_generated::PortableNativeGlyph;
53
54pub type GeneratedNativeTextMetrics = mathtex_portable_engine_generated::PortableNativeTextMetrics;
56
57pub type GeneratedNativeGlyphMetrics =
59 mathtex_portable_engine_generated::PortableNativeGlyphMetrics;
60
61pub type GeneratedMathVariant = mathtex_portable_engine_generated::PortableMathVariant;
63
64pub type GeneratedMathAssemblyPart = mathtex_portable_engine_generated::PortableMathAssemblyPart;
66
67pub type GeneratedMathKernCorner = mathtex_portable_engine_generated::PortableMathKernCorner;
69
70pub trait GeneratedFontPlatform: mathtex_portable_engine_generated::FontPlatform {}
72
73macro_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
82pub type EmptyGeneratedFontPlatform = mathtex_portable_engine_generated::EmptyFontPlatform;
84
85pub trait GeneratedPlatform: mathtex_portable_engine_generated::PortablePlatform {}
87
88impl<T> GeneratedPlatform for T where T: mathtex_portable_engine_generated::PortablePlatform {}
89
90pub type EmptyGeneratedPlatform = mathtex_portable_engine_generated::EmptyPlatform;
92
93pub type GeneratedClock = mathtex_portable_engine_generated::PortableClock;
95
96pub type GeneratedLinebreakRequest<'a> =
98 mathtex_portable_engine_generated::PortableLinebreakRequest<'a>;
99
100pub type GeneratedHostBoxRequest = mathtex_portable_engine_generated::PortableHostBoxRequest;
102
103pub type GeneratedHostBoxStyle = mathtex_portable_engine_generated::PortableHostBoxStyle;
105
106pub type GeneratedHostBox = mathtex_portable_engine_generated::PortableHostBox;
108
109#[derive(Clone, Copy, Debug)]
111pub struct GeneratedPlatformAdapter<P> {
112 platform: P,
113}
114
115impl<P> GeneratedPlatformAdapter<P> {
116 #[must_use]
118 pub fn new(platform: P) -> Self {
119 Self { platform }
120 }
121
122 #[must_use]
124 pub fn platform(&self) -> &P {
125 &self.platform
126 }
127
128 #[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#[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 spec: String,
226 script: Option<[u8; 4]>,
228 features: Vec<ShapeFeature>,
230}
231
232impl<F> GeneratedFontSystemAdapter<F> {
233 #[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 #[must_use]
245 pub fn font_system(&self) -> &F {
246 &self.fonts
247 }
248
249 #[must_use]
251 pub fn into_font_system(self) -> F {
252 self.fonts
253 }
254
255 #[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 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 let result = match what {
554 1 => data.ot_glyph_count(), 16 => data.ot_script_count(), 17 => data.ot_language_count(param1 as u32), 18 => data.ot_feature_count(param1 as u32, param2 as u32), 19 => data.ot_script_tag(param1 as u32), 20 => data.ot_language_tag(param1 as u32, param2 as u32), 21 => data.ot_feature_tag(param1 as u32, param2 as u32, param3 as u32), _ => 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 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
662fn 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 } else if let Some(rest) = part.strip_prefix('+') {
679 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
703fn 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
737pub struct GeneratedLayoutCapture {
738 pub page_builds: usize,
740 pub shipouts: usize,
742 pub special_outputs: usize,
744 pub picture_loads: usize,
746 pub source_specials: usize,
748 pub write_whatsit_diagnostics: usize,
750 pub pdf_extensions: usize,
752 pub transcript_bytes: usize,
754 pub page_top_prunes: usize,
756 pub last_shipout_box: Option<GeneratedNodeHandle>,
758 pub captured_fragment_root: Option<GeneratedNodeHandle>,
760 pub last_abort_status: Option<i32>,
762}
763
764impl GeneratedLayoutCapture {
765 #[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 #[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 #[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 #[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 #[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#[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
847const 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_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 const NULL_FLAG: i32 = -1_073_741_824; 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 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 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 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 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 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 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 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 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 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#[derive(Clone)]
1365pub struct GeneratedFormatCache {
1366 image: mathtex_portable_engine_generated::PortableFormatImage,
1367}
1368
1369impl GeneratedFormatCache {
1370 #[must_use]
1372 pub fn empty() -> Self {
1373 Self {
1374 image: mathtex_portable_engine_generated::PortableFormatImage::empty(),
1375 }
1376 }
1377
1378 #[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 #[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 #[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 #[must_use]
1413 pub fn image(&self) -> &mathtex_portable_engine_generated::PortableFormatImage {
1414 &self.image
1415 }
1416
1417 #[must_use]
1419 pub fn to_bytes(&self) -> Vec<u8> {
1420 self.image.to_bytes()
1421 }
1422
1423 #[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 #[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 #[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
1465const PACKAGED_FORMAT_MAGIC: &[u8; 6] = b"MTXPKG";
1467const PACKAGED_FORMAT_VERSION: u8 = 2;
1468const PACKAGED_COMPRESSION_DEFLATE: u8 = 1;
1469const 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#[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#[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
1573pub struct BakedGeneratedFormat {
1575 cache: GeneratedFormatCache,
1576 font_table_bytes: &'static [u8],
1577 font_count: usize,
1578}
1579
1580impl BakedGeneratedFormat {
1581 #[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 #[must_use]
1615 pub fn cache(&self) -> &GeneratedFormatCache {
1616 &self.cache
1617 }
1618
1619 #[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#[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 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#[derive(Clone, Debug)]
1713pub struct GeneratedResourceProvider<R> {
1714 inner: R,
1715}
1716
1717impl<R> GeneratedResourceProvider<R> {
1718 #[must_use]
1720 pub fn new(inner: R) -> Self {
1721 Self { inner }
1722 }
1723
1724 #[must_use]
1726 pub fn inner(&self) -> &R {
1727 &self.inner
1728 }
1729
1730 #[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 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}