1use 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#[derive(Clone, Debug)]
26pub struct SvgOptions {
27 pub font_size_pt: Dim,
29 pub color: Color,
31 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 #[must_use]
48 pub fn new() -> Self {
49 Self::default()
50 }
51}
52
53pub 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
93pub 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 + ½
314 let y = &(&baseline - &(bx.height.clone() * em_pt)) + ½
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}