Skip to main content

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}