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
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 =
66 mathtex_portable_engine_generated::PortableMathAssemblyPart;
67
68pub type GeneratedMathKernCorner = mathtex_portable_engine_generated::PortableMathKernCorner;
70
71pub trait GeneratedFontPlatform: mathtex_portable_engine_generated::FontPlatform {}
73
74macro_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
83pub type EmptyGeneratedFontPlatform = mathtex_portable_engine_generated::EmptyFontPlatform;
85
86pub trait GeneratedPlatform: mathtex_portable_engine_generated::PortablePlatform {}
88
89impl<T> GeneratedPlatform for T where T: mathtex_portable_engine_generated::PortablePlatform {}
90
91pub type EmptyGeneratedPlatform = mathtex_portable_engine_generated::EmptyPlatform;
93
94pub type GeneratedClock = mathtex_portable_engine_generated::PortableClock;
96
97pub type GeneratedLinebreakRequest<'a> =
99 mathtex_portable_engine_generated::PortableLinebreakRequest<'a>;
100
101#[derive(Clone, Copy, Debug)]
103pub struct GeneratedPlatformAdapter<P> {
104 platform: P,
105}
106
107impl<P> GeneratedPlatformAdapter<P> {
108 #[must_use]
110 pub fn new(platform: P) -> Self {
111 Self { platform }
112 }
113
114 #[must_use]
116 pub fn platform(&self) -> &P {
117 &self.platform
118 }
119
120 #[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#[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 spec: String,
166 script: Option<[u8; 4]>,
168 features: Vec<ShapeFeature>,
170}
171
172impl<F> GeneratedFontSystemAdapter<F> {
173 #[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 #[must_use]
185 pub fn font_system(&self) -> &F {
186 &self.fonts
187 }
188
189 #[must_use]
191 pub fn into_font_system(self) -> F {
192 self.fonts
193 }
194
195 #[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 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 let result = match what {
490 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,
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 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
596fn 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 } else if let Some(rest) = part.strip_prefix('+') {
613 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
637fn 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
671pub struct GeneratedLayoutCapture {
672 pub page_builds: usize,
674 pub shipouts: usize,
676 pub special_outputs: usize,
678 pub picture_loads: usize,
680 pub source_specials: usize,
682 pub write_whatsit_diagnostics: usize,
684 pub pdf_extensions: usize,
686 pub transcript_bytes: usize,
688 pub page_top_prunes: usize,
690 pub last_shipout_box: Option<GeneratedNodeHandle>,
692 pub captured_fragment_root: Option<GeneratedNodeHandle>,
694 pub last_abort_status: Option<i32>,
696}
697
698impl GeneratedLayoutCapture {
699 #[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 #[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 #[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 #[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 #[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#[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 const NULL_FLAG: i32 = -1_073_741_824; 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 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 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 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 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 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 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#[derive(Clone)]
1182pub struct GeneratedFormatCache {
1183 image: mathtex_portable_engine_generated::PortableFormatImage,
1184}
1185
1186impl GeneratedFormatCache {
1187 #[must_use]
1189 pub fn empty() -> Self {
1190 Self {
1191 image: mathtex_portable_engine_generated::PortableFormatImage::empty(),
1192 }
1193 }
1194
1195 #[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 #[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 #[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 #[must_use]
1230 pub fn image(&self) -> &mathtex_portable_engine_generated::PortableFormatImage {
1231 &self.image
1232 }
1233
1234 #[must_use]
1236 pub fn to_bytes(&self) -> Vec<u8> {
1237 self.image.to_bytes()
1238 }
1239
1240 #[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 #[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
1272const PACKAGED_FORMAT_MAGIC: &[u8; 6] = b"MTXPKG";
1274const PACKAGED_FORMAT_VERSION: u8 = 2;
1275const PACKAGED_COMPRESSION_DEFLATE: u8 = 1;
1276const 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#[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#[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#[derive(Clone, Debug)]
1375pub struct GeneratedResourceProvider<R> {
1376 inner: R,
1377}
1378
1379impl<R> GeneratedResourceProvider<R> {
1380 #[must_use]
1382 pub fn new(inner: R) -> Self {
1383 Self { inner }
1384 }
1385
1386 #[must_use]
1388 pub fn inner(&self) -> &R {
1389 &self.inner
1390 }
1391
1392 #[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 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}