use ratex_layout::{layout, to_display_list, LayoutOptions};
use ratex_parser::parse;
use ratex_svg::{render_to_svg, SvgOptions};
use ratex_types::math_style::MathStyle;
pub fn latex_to_svg(latex: &str, display: bool, color: &str) -> Option<String> {
let latex = latex.trim();
if latex.is_empty() {
return None;
}
let nodes = parse(latex).ok()?;
if nodes.is_empty() {
return None;
}
let opts = LayoutOptions {
style: if display {
MathStyle::Display
} else {
MathStyle::Text
},
..Default::default()
};
let root = layout(&nodes, &opts);
let list = to_display_list(&root);
let svg = render_to_svg(
&list,
&SvgOptions {
embed_glyphs: true,
..Default::default()
},
);
let svg = recolor(svg, color);
svg.contains("<svg").then_some(svg)
}
fn recolor(svg: String, color: &str) -> String {
svg.replace("rgba(0,0,0,1)", color)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn renders_simple_math_to_rasterizable_svg() {
for (latex, display) in [("x^2 + y^2 = z^2", true), ("\\frac{a}{b}", false)] {
let svg = latex_to_svg(latex, display, "#d0d0d0").expect("RaTeX renders basic math");
assert!(svg.contains("<svg"), "has an <svg> root: {latex}");
assert!(
svg.contains("<path") || svg.contains("<image"),
"standalone glyphs (path/image), not webfont <text>: {latex}"
);
let img =
crate::preview::svg::rasterize_bytes(svg.as_bytes(), Path::new("m.svg"), 1600)
.expect("resvg rasterizes the standalone SVG");
assert!(
img.width() > 0 && img.height() > 0,
"non-empty raster: {latex}"
);
use image::GenericImageView;
let ink = img.pixels().filter(|(_, _, p)| p.0[3] > 16).count();
assert!(
ink > 50,
"equation raster has visible ink: {latex} (ink={ink})"
);
}
}
#[test]
fn empty_or_garbage_returns_none_not_panic() {
assert!(latex_to_svg("", true, "#d0d0d0").is_none());
assert!(latex_to_svg(" ", false, "#d0d0d0").is_none());
let _ = latex_to_svg("\\frac{", true, "#d0d0d0");
let _ = latex_to_svg("\\undefinedcmd{x}", false, "#d0d0d0");
}
#[test]
fn display_and_inline_styles_both_render() {
let d = latex_to_svg("\\sum_{i=0}^{n} i", true, "#d0d0d0").expect("display");
let i = latex_to_svg("\\sum_{i=0}^{n} i", false, "#d0d0d0").expect("inline");
assert!(d.contains("<svg") && i.contains("<svg"));
}
#[test]
fn recolors_glyphs_away_from_invisible_black() {
use image::GenericImageView;
let svg = latex_to_svg("E = mc^2", false, "#d0d0d0").expect("renders");
assert!(
!svg.contains("rgba(0,0,0,1)"),
"不可視の純黒フィルが残っていない"
);
assert!(svg.contains("#d0d0d0"), "指定色でグリフを塗る");
let img = crate::preview::svg::rasterize_bytes(svg.as_bytes(), Path::new("m.svg"), 1024)
.expect("rasterizes");
let opaque_non_black = img
.pixels()
.filter(|(_, _, p)| p.0[3] > 200 && (p.0[0] > 40 || p.0[1] > 40 || p.0[2] > 40))
.count();
assert!(
opaque_non_black > 50,
"端末背景に映える明色インクが実在する (non_black={opaque_non_black})"
);
}
}