use resvg::{tiny_skia, usvg};
pub mod d2;
pub mod dot;
pub mod drawio;
pub mod flowchart;
pub mod flowchart_svg;
pub mod kymojson;
pub mod layout;
pub mod mermaid;
pub mod model;
pub mod sequence;
#[cfg(feature = "python")]
mod python;
#[cfg(feature = "wasm")]
mod wasm;
#[cfg(feature = "bpmn")]
pub mod bpmn;
#[derive(Debug)]
pub enum RenderError {
Parse(usvg::Error),
Size { width: u32, height: u32 },
Encode(String),
Pdf(String),
}
impl std::fmt::Display for RenderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RenderError::Parse(e) => write!(f, "invalid SVG: {e}"),
RenderError::Size { width, height } => {
write!(f, "invalid raster size {width}x{height}")
}
RenderError::Encode(e) => write!(f, "PNG encoding failed: {e}"),
RenderError::Pdf(e) => write!(f, "SVG→PDF conversion failed: {e}"),
}
}
}
impl std::error::Error for RenderError {}
impl From<usvg::Error> for RenderError {
fn from(e: usvg::Error) -> Self {
RenderError::Parse(e)
}
}
pub fn svg_to_png(svg: &[u8], scale: f32) -> Result<Vec<u8>, RenderError> {
#[allow(unused_mut)]
let mut opt = usvg::Options::default();
#[cfg(feature = "system-fonts")]
opt.fontdb_mut().load_system_fonts();
let tree = usvg::Tree::from_data(svg, &opt)?;
let size = tree.size();
let width = ((size.width() * scale).round() as i64).clamp(1, u32::MAX as i64) as u32;
let height = ((size.height() * scale).round() as i64).clamp(1, u32::MAX as i64) as u32;
let mut pixmap =
tiny_skia::Pixmap::new(width, height).ok_or(RenderError::Size { width, height })?;
let transform = tiny_skia::Transform::from_scale(scale, scale);
resvg::render(&tree, transform, &mut pixmap.as_mut());
pixmap
.encode_png()
.map_err(|e| RenderError::Encode(e.to_string()))
}
#[cfg(feature = "pdf")]
pub fn svg_to_pdf(svg: &[u8]) -> Result<Vec<u8>, RenderError> {
use svg2pdf::usvg as pdf_usvg;
#[allow(unused_mut)]
let mut opt = pdf_usvg::Options::default();
#[cfg(feature = "system-fonts")]
opt.fontdb_mut().load_system_fonts();
let tree = pdf_usvg::Tree::from_data(svg, &opt).map_err(|e| RenderError::Pdf(e.to_string()))?;
svg2pdf::to_pdf(
&tree,
svg2pdf::ConversionOptions::default(),
svg2pdf::PageOptions::default(),
)
.map_err(|e| RenderError::Pdf(e.to_string()))
}
pub fn mermaid_to_kymojson(src: &str) -> Result<String, mermaid::MermaidError> {
let fc = mermaid::parse(src)?;
let diagram = layout::layout_flowchart(&fc);
Ok(kymojson::export(&diagram))
}
pub fn mermaid_to_d2(src: &str) -> Result<String, mermaid::MermaidError> {
Ok(flowchart::emit::to_d2(&mermaid::parse(src)?))
}
pub fn mermaid_to_dot(src: &str) -> Result<String, mermaid::MermaidError> {
Ok(flowchart::emit::to_dot(&mermaid::parse(src)?))
}
pub fn mermaid_to_mermaid(src: &str) -> Result<String, mermaid::MermaidError> {
Ok(flowchart::emit::to_mermaid(&mermaid::parse(src)?))
}
pub fn mermaid_to_xmi(src: &str) -> Result<String, mermaid::MermaidError> {
Ok(sequence::emit::to_xmi(&mermaid::parse_sequence(src)?))
}
pub fn mermaid_to_mdj(src: &str) -> Result<String, mermaid::MermaidError> {
Ok(sequence::mdj::to_mdj(&mermaid::parse_sequence(src)?))
}
pub fn mermaid_to_gaphor(src: &str) -> Result<String, mermaid::MermaidError> {
Ok(sequence::gaphor::to_gaphor(&mermaid::parse_sequence(src)?))
}
pub fn mermaid_to_drawio(src: &str) -> Result<String, mermaid::MermaidError> {
let fc = mermaid::parse(src)?;
Ok(drawio::to_drawio(&layout::layout_flowchart(&fc)))
}
pub fn mermaid_to_svg(src: &str) -> Result<String, mermaid::MermaidError> {
let fc = mermaid::parse(src)?;
Ok(flowchart_svg::render(&layout::layout_flowchart(&fc)))
}
pub fn d2_to_svg(src: &str) -> Result<String, d2::D2Error> {
let fc = d2::parse(src)?;
Ok(flowchart_svg::render(&layout::layout_flowchart(&fc)))
}
pub fn d2_to_kymojson(src: &str) -> Result<String, d2::D2Error> {
let fc = d2::parse(src)?;
Ok(kymojson::export(&layout::layout_flowchart(&fc)))
}
pub fn dot_to_svg(src: &str) -> Result<String, dot::DotError> {
let fc = dot::parse(src)?;
Ok(flowchart_svg::render(&layout::layout_flowchart(&fc)))
}
pub fn dot_to_kymojson(src: &str) -> Result<String, dot::DotError> {
let fc = dot::parse(src)?;
Ok(kymojson::export(&layout::layout_flowchart(&fc)))
}
#[cfg(feature = "bpmn")]
pub fn drawio_from_kymojson(json: &str) -> Result<String, String> {
drawio::to_drawio_kymojson(json)
}
#[cfg(test)]
mod tests {
const SVG: &[u8] =
br##"<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20"><rect width="40" height="20" fill="#09f"/></svg>"##;
#[test]
fn png_has_magic() {
let png = super::svg_to_png(SVG, 1.0).expect("render png");
assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
}
#[cfg(feature = "pdf")]
#[test]
fn pdf_has_magic() {
let pdf = super::svg_to_pdf(SVG).expect("render pdf");
assert_eq!(&pdf[..5], b"%PDF-");
}
#[test]
fn mermaid_and_d2_to_svg() {
let mmd = super::mermaid_to_svg("flowchart TD\nA[Go] --> B{ok?}").unwrap();
assert!(mmd.starts_with("<?xml") && mmd.contains("fc-shape") && mmd.contains(">ok?<"));
let d2src = "direction: down\nA: Go\nB: \"ok?\" { shape: diamond }\nA -> B";
let d2 = super::d2_to_svg(d2src).unwrap();
assert!(d2.contains("<polygon class=\"fc-shape\"") && d2.contains(">ok?<"));
assert!(super::d2_to_kymojson(d2src).unwrap().contains("\"shape\": \"diamond\""));
let dotsrc = "digraph G {\n A [label=\"Go\"];\n B [label=\"ok?\", shape=diamond];\n A -> B;\n}";
let dot = super::dot_to_svg(dotsrc).unwrap();
assert!(dot.contains("<polygon class=\"fc-shape\"") && dot.contains(">ok?<"));
}
}