use ratex_layout::{LayoutOptions, layout, to_display_list};
use ratex_parser::parse;
use ratex_svg::{SvgColorSyntax, SvgOptions, render_to_svg_with_color_syntax};
use ratex_types::color::Color;
use ratex_types::math_style::MathStyle;
use std::fmt;
#[derive(Clone, Debug, PartialEq)]
pub struct MathPicture {
pub svg: String,
pub width: f64,
pub height: f64,
pub depth: f64,
}
impl MathPicture {
pub fn px_width(&self, size: f64) -> f64 {
self.width * size
}
pub fn px_height(&self, size: f64) -> f64 {
(self.height + self.depth) * size
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MathError {
pub message: String,
pub position: Option<usize>,
}
impl fmt::Display for MathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.position {
Some(at) => write!(f, "{} (at byte {at})", self.message),
None => f.write_str(&self.message),
}
}
}
impl std::error::Error for MathError {}
pub fn typeset(
tex: &str,
display: bool,
size: f64,
color: [u8; 4],
) -> Result<MathPicture, MathError> {
let ast = parse(tex.trim()).map_err(|e| MathError {
message: e.message,
position: e.loc.map(|l| l.start),
})?;
let style = if display {
MathStyle::Display
} else {
MathStyle::Text
};
let [r, g, b, a] = color.map(|c| f32::from(c) / 255.0);
let opts = LayoutOptions::default()
.with_style(style)
.with_color(Color::new(r, g, b, a));
let list = to_display_list(&layout(&ast, &opts));
let svg = render_to_svg_with_color_syntax(
&list,
&SvgOptions {
font_size: size,
padding: 0.0,
stroke_width: (size / 16.0).max(0.5),
embed_glyphs: true,
font_dir: String::new(),
},
SvgColorSyntax::Rgb,
);
Ok(MathPicture {
svg: in_pixels(svg),
width: list.width,
height: list.height,
depth: list.depth,
})
}
fn in_pixels(svg: String) -> String {
let Some(head_end) = svg.find('>') else {
return svg;
};
let (head, rest) = svg.split_at(head_end);
let mut head = head.to_string();
for attr in ["width=\"", "height=\""] {
if let Some(i) = head.find(attr) {
let v = i + attr.len();
if let Some(q) = head[v..].find('"') {
let value = head[v..v + q].trim_end_matches("pt").to_string();
head.replace_range(v..v + q, &value);
}
}
}
head.push_str(rest);
head
}
#[cfg(test)]
mod tests {
use super::*;
const BLACK: [u8; 4] = [0, 0, 0, 255];
#[test]
fn an_inline_formula_is_a_standalone_svg_with_metrics() {
let p = typeset("E = mc^2", false, 16.0, BLACK).unwrap();
assert!(
p.svg
.starts_with("<svg xmlns=\"http://www.w3.org/2000/svg\"")
);
assert!(p.svg.ends_with("</svg>"));
assert!(p.svg.contains("<path"), "{}", p.svg);
assert!(!p.svg.contains("<text"), "{}", p.svg);
assert!(!p.svg.contains("font-family"), "{}", p.svg);
assert!(p.width > 3.0 && p.width < 5.0, "{}", p.width);
assert!(p.height > 0.6 && p.height < 1.0, "{}", p.height);
assert_eq!(p.depth, 0.0);
}
#[test]
fn a_display_formula_has_a_depth_and_a_display_style() {
let d = typeset(r"\sum_{i=0}^n i", true, 16.0, BLACK).unwrap();
let t = typeset(r"\sum_{i=0}^n i", false, 16.0, BLACK).unwrap();
assert!(d.height + d.depth > t.height + t.depth, "{d:?} vs {t:?}");
assert!(d.width < t.width, "{d:?} vs {t:?}");
assert!(d.depth > 0.0);
}
#[test]
fn the_root_is_sized_in_pixels_at_the_requested_size() {
let p = typeset("x", false, 20.0, BLACK).unwrap();
let head = &p.svg[..p.svg.find('>').unwrap()];
assert!(!head.contains("pt"), "{head}");
let attr = |name: &str| -> f64 {
head.split(&format!("{name}=\""))
.nth(1)
.unwrap()
.split('"')
.next()
.unwrap()
.parse()
.unwrap()
};
assert!((attr("width") - p.px_width(20.0)).abs() < 1e-3);
assert!((attr("height") - p.px_height(20.0)).abs() < 1e-3);
}
#[test]
fn colour_is_the_ink() {
let p = typeset("x", false, 16.0, [255, 0, 0, 255]).unwrap();
assert!(p.svg.contains("fill=\"rgb(255,0,0)\""), "{}", p.svg);
assert!(!p.svg.contains("rgba("), "{}", p.svg);
let p = typeset("x", false, 16.0, [0, 0, 255, 128]).unwrap();
assert!(
p.svg.contains("fill=\"rgb(0,0,255)\" fill-opacity=\"0.50"),
"{}",
p.svg
);
}
#[test]
fn surrounding_whitespace_is_insignificant() {
let a = typeset("\n x + y \n", false, 16.0, BLACK).unwrap();
let b = typeset("x + y", false, 16.0, BLACK).unwrap();
assert_eq!(a, b);
}
#[test]
fn unreadable_tex_is_an_error_that_names_a_byte() {
let e = typeset(r"\frac{a", false, 16.0, BLACK).unwrap_err();
assert!(!e.message.is_empty());
assert!(e.position.is_some(), "{e}");
assert!(e.to_string().contains("at byte"), "{e}");
}
#[test]
fn an_empty_formula_is_an_empty_picture_not_an_error() {
let p = typeset("", false, 16.0, BLACK).unwrap();
assert_eq!(p.width, 0.0);
assert!(p.svg.starts_with("<svg"));
}
}