rpic_render/math.rs
1//! RaTeX-backed math renderer for the rpic `texlabels` extension.
2//!
3//! Register with [`rpic_core::set_math_renderer(render_math)`]. The hook in
4//! the core is backend-neutral; `docs/tex-labels.md` records Typst + mitex as
5//! the documented alternative should this backend ever need replacing.
6
7use ratex_layout::engine::layout;
8use ratex_layout::layout_options::LayoutOptions;
9use ratex_layout::to_display::to_display_list;
10use ratex_parser::parse;
11use ratex_svg::{SvgOptions, render_to_svg};
12use ratex_types::math_style::MathStyle;
13use rpic_core::MathSpan;
14
15/// Typeset `tex` (inline math, no `$` delimiters) at `font_pt` points into a
16/// self-contained SVG fragment (KaTeX fonts embedded as glyph paths) plus
17/// exact metrics in inches.
18pub fn render_math(tex: &str, font_pt: f64) -> Result<MathSpan, String> {
19 let nodes = parse(tex).map_err(|e| {
20 // strip RaTeX's "ParseError at position N:" prefix — the diagnostic
21 // already names the offending label
22 let msg = format!("{e}");
23 match msg.split_once(": ") {
24 Some((head, tail)) if head.starts_with("ParseError") => tail.to_string(),
25 _ => msg,
26 }
27 })?;
28 // `$…$` is inline math: use TeX text style, like LaTeX would.
29 let opts = LayoutOptions {
30 style: MathStyle::Text,
31 ..Default::default()
32 };
33 let lb = layout(&nodes, &opts);
34 let dl = to_display_list(&lb);
35 // rpic's SVG space is 96 px/inch; one em of label text is font_pt points.
36 let px_per_em = font_pt * 96.0 / 72.0;
37 let svg = render_to_svg(
38 &dl,
39 &SvgOptions {
40 font_size: px_per_em,
41 padding: 0.0,
42 embed_glyphs: true,
43 ..Default::default()
44 },
45 );
46 // RaTeX stamps the root width/height with a `pt` unit even though the
47 // numbers are already in our px space (font_size user units) — an SVG pt
48 // is 4/3 px, which would render the fragment 33% larger than its
49 // metrics. Strip the unit from the root tag only.
50 let svg = match svg.find('>') {
51 Some(end) => format!("{}{}", svg[..end].replace("pt\"", "\""), &svg[end..]),
52 None => svg,
53 };
54 let em_in = font_pt / 72.0;
55 Ok(MathSpan {
56 svg,
57 width: dl.width * em_in,
58 height: dl.height * em_in,
59 depth: dl.depth * em_in,
60 })
61}