use std::collections::HashMap;
use super::outline;
use crate::color::Color;
use crate::dim::Dim;
use crate::error::{Error, FontError};
use crate::font::MathFont;
use crate::layout::{layout, BoxContent, MathBox, MathStyle};
use crate::parser::parse;
#[derive(Clone, Debug)]
pub struct SvgOptions {
pub font_size_pt: Dim,
pub color: Color,
pub display: bool,
}
impl Default for SvgOptions {
fn default() -> Self {
Self {
font_size_pt: Dim::from_i64(12),
color: Color::rgb(0, 0, 0),
display: false,
}
}
}
impl SvgOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
pub fn latex_to_svg(latex: &str, font: &MathFont, options: &SvgOptions) -> Result<String, Error> {
let ast = parse(latex)?;
let style = if options.display {
MathStyle::Display
} else {
MathStyle::Text
};
let tree = layout(&ast, font, style)?;
render_svg(&tree, font, options)
}
pub fn render_svg(tree: &MathBox, font: &MathFont, options: &SvgOptions) -> Result<String, Error> {
let em_pt = options.font_size_pt.clone();
let fu_pt = &em_pt / &Dim::from_i64(i64::from(font.units_per_em()));
let svg_w = &tree.width * &em_pt;
let svg_h = (&tree.height + &tree.depth) * &em_pt;
let baseline = &tree.height * &em_pt;
let mut cache: HashMap<u16, String> = HashMap::new();
let mut body = String::new();
emit(
tree,
font,
&em_pt,
&fu_pt,
&Dim::zero(),
&baseline,
options.color,
&mut cache,
&mut body,
)?;
let mut out = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
out.push('\n');
out.push_str(&format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}pt" height="{}pt" viewBox="0 0 {} {}">"#,
svg_w.to_svg_string(),
svg_h.to_svg_string(),
svg_w.to_svg_string(),
svg_h.to_svg_string()
));
out.push('\n');
out.push_str(r#"<g fill=""#);
out.push_str(&options.color.css_hex());
out.push_str("\">\n");
out.push_str(&body);
out.push_str("</g>\n</svg>\n");
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn emit(
bx: &MathBox,
font: &MathFont,
em_pt: &Dim,
fu_pt: &Dim,
origin_x: &Dim,
parent_baseline: &Dim,
fill: Color,
cache: &mut HashMap<u16, String>,
out: &mut String,
) -> Result<(), Error> {
let baseline = parent_baseline - &(bx.shift.clone() * em_pt);
match &bx.content {
BoxContent::Empty | BoxContent::Kern(_) => Ok(()),
BoxContent::Glyph { ch, glyph_id } => {
let d = if let Some(s) = cache.get(glyph_id) {
s.clone()
} else {
let s = outline::glyph_path_d(font, *glyph_id)?;
if s.is_empty() {
return Err(FontError::MissingGlyph { ch: *ch }.into());
}
cache.insert(*glyph_id, s.clone());
s
};
let sx = fu_pt.to_svg_string();
let nsx = (-fu_pt.clone()).to_svg_string();
out.push_str(&format!(
r#"<path d="{d}" transform="translate({} {}) scale({sx} {nsx})"/>"#,
origin_x.to_svg_string(),
baseline.to_svg_string(),
));
out.push('\n');
Ok(())
}
BoxContent::Rule => {
let w = &bx.width * em_pt;
let h_ink = (&bx.height + &bx.depth) * em_pt;
if w.is_zero() || h_ink.is_zero() {
return Ok(());
}
let y = &baseline - &(bx.height.clone() * em_pt);
out.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}"/>"#,
origin_x.to_svg_string(),
y.to_svg_string(),
w.to_svg_string(),
h_ink.to_svg_string()
));
out.push('\n');
Ok(())
}
BoxContent::HList(kids) => {
let mut x = origin_x.clone();
for k in kids {
emit(k, font, em_pt, fu_pt, &x, &baseline, fill, cache, out)?;
x = &x + &(k.width.clone() * em_pt);
}
Ok(())
}
BoxContent::VList(kids) => {
if kids.is_empty() {
return Ok(());
}
emit(
&kids[0], font, em_pt, fu_pt, origin_x, &baseline, fill, cache, out,
)?;
let mut y_below = kids[0].depth.clone() * em_pt;
for k in kids.iter().skip(1) {
let child_base = &baseline + &y_below + &(k.height.clone() * em_pt);
emit(
k,
font,
em_pt,
fu_pt,
origin_x,
&child_base,
fill,
cache,
out,
)?;
y_below = &y_below + &((&k.height + &k.depth) * em_pt);
}
Ok(())
}
BoxContent::Overlap(kids) => {
for k in kids {
emit(k, font, em_pt, fu_pt, origin_x, &baseline, fill, cache, out)?;
}
Ok(())
}
BoxContent::Color(c, inner) => {
out.push_str(&format!(
r#"<g fill="{hex}" stroke="{hex}">"#,
hex = c.css_hex()
));
out.push('\n');
emit(
inner, font, em_pt, fu_pt, origin_x, &baseline, *c, cache, out,
)?;
out.push_str("</g>\n");
Ok(())
}
BoxContent::BackColor(c, inner) => {
let w = &bx.width * em_pt;
let h_ink = (&bx.height + &bx.depth) * em_pt;
let y = &baseline - &(bx.height.clone() * em_pt);
out.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}"/>"#,
origin_x.to_svg_string(),
y.to_svg_string(),
w.to_svg_string(),
h_ink.to_svg_string(),
c.css_hex()
));
out.push('\n');
emit(
inner, font, em_pt, fu_pt, origin_x, &baseline, fill, cache, out,
)?;
Ok(())
}
BoxContent::Line {
x1,
y1,
x2,
y2,
thickness,
} => {
let x1p = origin_x + &(x1.clone() * em_pt);
let y1p = &baseline - &(y1.clone() * em_pt);
let x2p = origin_x + &(x2.clone() * em_pt);
let y2p = &baseline - &(y2.clone() * em_pt);
let sw = thickness.clone() * em_pt;
out.push_str(&format!(
r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="{}"/>"#,
x1p.to_svg_string(),
y1p.to_svg_string(),
x2p.to_svg_string(),
y2p.to_svg_string(),
fill.css_hex(),
sw.to_svg_string(),
));
out.push('\n');
Ok(())
}
BoxContent::Frame {
thickness,
stroke,
inner,
} => {
let w = &bx.width * em_pt;
let h_ink = (&bx.height + &bx.depth) * em_pt;
let t = thickness.clone() * em_pt;
let half = &t / &Dim::from_i64(2);
let x = origin_x + ½
let y = &(&baseline - &(bx.height.clone() * em_pt)) + ½
let rw = (&w - &t).max(&Dim::zero());
let rh = (&h_ink - &t).max(&Dim::zero());
let stroke_css = stroke.unwrap_or(fill).css_hex();
if !rw.is_zero() && !rh.is_zero() {
out.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" fill="none" stroke="{}" stroke-width="{}"/>"#,
x.to_svg_string(),
y.to_svg_string(),
rw.to_svg_string(),
rh.to_svg_string(),
stroke_css,
t.to_svg_string(),
));
out.push('\n');
}
emit(
inner, font, em_pt, fu_pt, origin_x, &baseline, fill, cache, out,
)?;
Ok(())
}
}
}