Skip to main content

latex_rust/render/svg/
render.rs

1//! `MathBox` tree → self-contained SVG 1.1.
2
3use std::collections::HashMap;
4
5use super::outline;
6use crate::color::Color;
7use crate::dim::Dim;
8use crate::error::{Error, FontError};
9use crate::font::MathFont;
10use crate::layout::{layout, BoxContent, MathBox, MathStyle};
11use crate::parser::parse;
12
13/// Options for [`render_svg`].
14///
15/// # Examples
16///
17/// ```
18/// use latex_rust::{Color, Dim, SvgOptions};
19///
20/// let mut opt = SvgOptions::new();
21/// opt.font_size_pt = Dim::from_i64(12);
22/// opt.color = Color::rgb(0, 0, 0);
23/// opt.display = false;
24/// ```
25#[derive(Clone, Debug)]
26pub struct SvgOptions {
27    /// Em size in points (SVG `width`/`height` use `pt`).
28    pub font_size_pt: Dim,
29    /// Default glyph fill.
30    pub color: Color,
31    /// When using [`latex_to_svg`], pick display vs text style.
32    pub display: bool,
33}
34
35impl Default for SvgOptions {
36    fn default() -> Self {
37        Self {
38            font_size_pt: Dim::from_i64(12),
39            color: Color::rgb(0, 0, 0),
40            display: false,
41        }
42    }
43}
44
45impl SvgOptions {
46    /// 12 pt, black fill, text style.
47    #[must_use]
48    pub fn new() -> Self {
49        Self::default()
50    }
51}
52
53/// Parse, lay out, and render `latex` to a self-contained SVG document.
54///
55/// Glyphs are `<path>` elements from STIX Two Math outlines. Rules are `<rect>`.
56///
57/// # Arguments
58///
59/// * `latex` — math source (see [`crate::parse()`]).
60/// * `font` — face used for layout and outlines.
61/// * `options` — em size, default fill, and display vs text style.
62///
63/// # Returns
64///
65/// An SVG 1.1 document string.
66///
67/// # Errors
68///
69/// * [`Error::Parse`] — tokenizer or parser rejected `latex`.
70/// * [`Error::Font`] — a required glyph is missing.
71/// * [`Error::Unsupported`] — construct this renderer will not invent.
72///
73/// # Examples
74///
75/// ```
76/// use latex_rust::{latex_to_svg, MathFont, SvgOptions};
77///
78/// let font = MathFont::stix_two_math().unwrap();
79/// let svg = latex_to_svg(r"\frac{1}{2}", &font, &SvgOptions::new()).unwrap();
80/// assert!(svg.contains("<svg"));
81/// ```
82pub fn latex_to_svg(latex: &str, font: &MathFont, options: &SvgOptions) -> Result<String, Error> {
83    let ast = parse(latex)?;
84    let style = if options.display {
85        MathStyle::Display
86    } else {
87        MathStyle::Text
88    };
89    let tree = layout(&ast, font, style)?;
90    render_svg(&tree, font, options)
91}
92
93/// Render a laid-out [`MathBox`] to a self-contained SVG document.
94///
95/// # Arguments
96///
97/// * `tree` — box model from [`crate::layout()`].
98/// * `font` — face that supplied the glyph ids on `tree`.
99/// * `options` — em size and default fill (`display` is ignored here).
100///
101/// # Returns
102///
103/// An SVG 1.1 document string.
104///
105/// # Errors
106///
107/// * [`Error::Font`] — outline missing for a glyph id on `tree`.
108/// * [`Error::Unsupported`] — a box the SVG backend cannot emit.
109///
110/// # Examples
111///
112/// ```
113/// use latex_rust::{layout, parse, render_svg, MathFont, MathStyle, SvgOptions};
114///
115/// let ast = parse(r"x").unwrap();
116/// let font = MathFont::stix_two_math().unwrap();
117/// let tree = layout(&ast, &font, MathStyle::Text).unwrap();
118/// let svg = render_svg(&tree, &font, &SvgOptions::new()).unwrap();
119/// assert!(svg.contains("<path"));
120/// ```
121pub fn render_svg(tree: &MathBox, font: &MathFont, options: &SvgOptions) -> Result<String, Error> {
122    let em_pt = options.font_size_pt.clone();
123    let fu_pt = &em_pt / &Dim::from_i64(i64::from(font.units_per_em()));
124    let svg_w = &tree.width * &em_pt;
125    let svg_h = (&tree.height + &tree.depth) * &em_pt;
126    let baseline = &tree.height * &em_pt;
127    let mut cache: HashMap<u16, String> = HashMap::new();
128    let mut body = String::new();
129    emit(
130        tree,
131        font,
132        &em_pt,
133        &fu_pt,
134        &Dim::zero(),
135        &baseline,
136        options.color,
137        &mut cache,
138        &mut body,
139    )?;
140    let mut out = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
141    out.push('\n');
142    out.push_str(&format!(
143        r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}pt" height="{}pt" viewBox="0 0 {} {}">"#,
144        svg_w.to_svg_string(),
145        svg_h.to_svg_string(),
146        svg_w.to_svg_string(),
147        svg_h.to_svg_string()
148    ));
149    out.push('\n');
150    out.push_str(r#"<g fill=""#);
151    out.push_str(&options.color.css_hex());
152    out.push_str("\">\n");
153    out.push_str(&body);
154    out.push_str("</g>\n</svg>\n");
155    Ok(out)
156}
157
158#[allow(clippy::too_many_arguments)]
159fn emit(
160    bx: &MathBox,
161    font: &MathFont,
162    em_pt: &Dim,
163    fu_pt: &Dim,
164    origin_x: &Dim,
165    parent_baseline: &Dim,
166    fill: Color,
167    cache: &mut HashMap<u16, String>,
168    out: &mut String,
169) -> Result<(), Error> {
170    let baseline = parent_baseline - &(bx.shift.clone() * em_pt);
171    match &bx.content {
172        BoxContent::Empty | BoxContent::Kern(_) => Ok(()),
173        BoxContent::Glyph { ch, glyph_id } => {
174            let d = if let Some(s) = cache.get(glyph_id) {
175                s.clone()
176            } else {
177                let s = outline::glyph_path_d(font, *glyph_id)?;
178                if s.is_empty() {
179                    return Err(FontError::MissingGlyph { ch: *ch }.into());
180                }
181                cache.insert(*glyph_id, s.clone());
182                s
183            };
184            let sx = fu_pt.to_svg_string();
185            let nsx = (-fu_pt.clone()).to_svg_string();
186            out.push_str(&format!(
187                r#"<path d="{d}" transform="translate({} {}) scale({sx} {nsx})"/>"#,
188                origin_x.to_svg_string(),
189                baseline.to_svg_string(),
190            ));
191            out.push('\n');
192            Ok(())
193        }
194        BoxContent::Rule => {
195            let w = &bx.width * em_pt;
196            let h_ink = (&bx.height + &bx.depth) * em_pt;
197            if w.is_zero() || h_ink.is_zero() {
198                return Ok(());
199            }
200            let y = &baseline - &(bx.height.clone() * em_pt);
201            out.push_str(&format!(
202                r#"<rect x="{}" y="{}" width="{}" height="{}"/>"#,
203                origin_x.to_svg_string(),
204                y.to_svg_string(),
205                w.to_svg_string(),
206                h_ink.to_svg_string()
207            ));
208            out.push('\n');
209            Ok(())
210        }
211        BoxContent::HList(kids) => {
212            let mut x = origin_x.clone();
213            for k in kids {
214                emit(k, font, em_pt, fu_pt, &x, &baseline, fill, cache, out)?;
215                x = &x + &(k.width.clone() * em_pt);
216            }
217            Ok(())
218        }
219        BoxContent::VList(kids) => {
220            if kids.is_empty() {
221                return Ok(());
222            }
223            emit(
224                &kids[0], font, em_pt, fu_pt, origin_x, &baseline, fill, cache, out,
225            )?;
226            let mut y_below = kids[0].depth.clone() * em_pt;
227            for k in kids.iter().skip(1) {
228                let child_base = &baseline + &y_below + &(k.height.clone() * em_pt);
229                emit(
230                    k,
231                    font,
232                    em_pt,
233                    fu_pt,
234                    origin_x,
235                    &child_base,
236                    fill,
237                    cache,
238                    out,
239                )?;
240                y_below = &y_below + &((&k.height + &k.depth) * em_pt);
241            }
242            Ok(())
243        }
244        BoxContent::Overlap(kids) => {
245            for k in kids {
246                emit(k, font, em_pt, fu_pt, origin_x, &baseline, fill, cache, out)?;
247            }
248            Ok(())
249        }
250        BoxContent::Color(c, inner) => {
251            out.push_str(&format!(
252                r#"<g fill="{hex}" stroke="{hex}">"#,
253                hex = c.css_hex()
254            ));
255            out.push('\n');
256            emit(
257                inner, font, em_pt, fu_pt, origin_x, &baseline, *c, cache, out,
258            )?;
259            out.push_str("</g>\n");
260            Ok(())
261        }
262        BoxContent::BackColor(c, inner) => {
263            let w = &bx.width * em_pt;
264            let h_ink = (&bx.height + &bx.depth) * em_pt;
265            let y = &baseline - &(bx.height.clone() * em_pt);
266            out.push_str(&format!(
267                r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}"/>"#,
268                origin_x.to_svg_string(),
269                y.to_svg_string(),
270                w.to_svg_string(),
271                h_ink.to_svg_string(),
272                c.css_hex()
273            ));
274            out.push('\n');
275            emit(
276                inner, font, em_pt, fu_pt, origin_x, &baseline, fill, cache, out,
277            )?;
278            Ok(())
279        }
280        BoxContent::Line {
281            x1,
282            y1,
283            x2,
284            y2,
285            thickness,
286        } => {
287            let x1p = origin_x + &(x1.clone() * em_pt);
288            let y1p = &baseline - &(y1.clone() * em_pt);
289            let x2p = origin_x + &(x2.clone() * em_pt);
290            let y2p = &baseline - &(y2.clone() * em_pt);
291            let sw = thickness.clone() * em_pt;
292            out.push_str(&format!(
293                r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="{}"/>"#,
294                x1p.to_svg_string(),
295                y1p.to_svg_string(),
296                x2p.to_svg_string(),
297                y2p.to_svg_string(),
298                fill.css_hex(),
299                sw.to_svg_string(),
300            ));
301            out.push('\n');
302            Ok(())
303        }
304        BoxContent::Frame {
305            thickness,
306            stroke,
307            inner,
308        } => {
309            let w = &bx.width * em_pt;
310            let h_ink = (&bx.height + &bx.depth) * em_pt;
311            let t = thickness.clone() * em_pt;
312            let half = &t / &Dim::from_i64(2);
313            let x = origin_x + &half;
314            let y = &(&baseline - &(bx.height.clone() * em_pt)) + &half;
315            let rw = (&w - &t).max(&Dim::zero());
316            let rh = (&h_ink - &t).max(&Dim::zero());
317            let stroke_css = stroke.unwrap_or(fill).css_hex();
318            if !rw.is_zero() && !rh.is_zero() {
319                out.push_str(&format!(
320                    r#"<rect x="{}" y="{}" width="{}" height="{}" fill="none" stroke="{}" stroke-width="{}"/>"#,
321                    x.to_svg_string(),
322                    y.to_svg_string(),
323                    rw.to_svg_string(),
324                    rh.to_svg_string(),
325                    stroke_css,
326                    t.to_svg_string(),
327                ));
328                out.push('\n');
329            }
330            emit(
331                inner, font, em_pt, fu_pt, origin_x, &baseline, fill, cache, out,
332            )?;
333            Ok(())
334        }
335    }
336}