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 math;
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;
static EXTRA_FONTS: std::sync::OnceLock<std::sync::Mutex<Vec<Vec<u8>>>> =
std::sync::OnceLock::new();
pub fn register_font(bytes: Vec<u8>) {
EXTRA_FONTS
.get_or_init(|| std::sync::Mutex::new(Vec::new()))
.lock()
.unwrap()
.push(bytes);
}
macro_rules! load_extra_fonts {
($db:expr) => {
if let Some(fonts) = EXTRA_FONTS.get() {
let fonts = fonts.lock().unwrap();
if !fonts.is_empty() {
let db = $db;
let before = db.faces().count();
for data in fonts.iter() {
db.load_font_data(data.clone());
}
let family = db
.faces()
.skip(before)
.find_map(|f| f.families.first().map(|(name, _)| name.clone()));
if let Some(family) = family {
db.set_sans_serif_family(family.clone());
db.set_serif_family(family.clone());
db.set_cursive_family(family.clone());
db.set_fantasy_family(family.clone());
db.set_monospace_family(family);
}
}
}
};
}
#[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> {
let mut opt = usvg::Options::default();
#[cfg(feature = "system-fonts")]
opt.fontdb_mut().load_system_fonts();
load_extra_fonts!(opt.fontdb_mut());
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;
let mut opt = pdf_usvg::Options::default();
#[cfg(feature = "system-fonts")]
opt.fontdb_mut().load_system_fonts();
load_extra_fonts!(opt.fontdb_mut());
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_sequence_svg(src: &str) -> Result<String, mermaid::MermaidError> {
let mut seq = mermaid::parse_sequence(src)?;
for item in &mut seq.items {
render_sequence_item_math(item);
}
Ok(sequence::svg::render(&seq))
}
fn render_sequence_item_math(item: &mut sequence::Item) {
match item {
sequence::Item::Message(m) => m.text = clean_label(&m.text),
sequence::Item::Note(n) => n.text = clean_label(&n.text),
sequence::Item::Fragment(f) => {
for op in &mut f.operands {
op.guard = clean_label(&op.guard);
for it in &mut op.items {
render_sequence_item_math(it);
}
}
}
sequence::Item::Activate(_)
| sequence::Item::Deactivate(_)
| sequence::Item::Autonumber(_) => {}
}
}
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 mut fc = mermaid::parse(src)?;
render_flowchart_math(&mut fc);
Ok(flowchart_svg::render(&layout::layout_flowchart(&fc)))
}
fn clean_label(s: &str) -> String {
math::strip_br(&math::render(s))
}
fn render_flowchart_math(fc: &mut flowchart::Flowchart) {
for n in &mut fc.nodes {
n.label = clean_label(&n.label);
}
for e in &mut fc.edges {
e.label = clean_label(&e.label);
}
for g in &mut fc.subgraphs {
g.title = clean_label(&g.title);
}
}
pub fn mermaid_state_to_svg(src: &str) -> Result<String, mermaid::MermaidError> {
let mut fc = mermaid::parse_state(src)?;
render_flowchart_math(&mut fc);
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 autonumber_off_keeps_counting() {
let svg = super::mermaid_to_sequence_svg(
"sequenceDiagram\nautonumber 5 5\nA->>B: a\nA->>B: b\nautonumber off\nA->>B: c\nautonumber\nA->>B: d",
)
.unwrap();
assert!(svg.contains(">5 a<") && svg.contains(">10 b<"));
assert!(svg.contains(">c<") && !svg.contains(">15 c<")); assert!(svg.contains(">20 d<")); }
#[test]
fn multiline_node_data_and_continuation() {
let svg = super::mermaid_to_svg(
"flowchart TB\nA@{\n shape: circle\n label: \"Hi\"\n}\nA --> B",
)
.expect("node-data block");
assert!(svg.starts_with("<?xml") && svg.contains(">Hi<"));
let svg = super::mermaid_to_svg("flowchart TB\nA[One]\n--> B[Two]").expect("continuation");
assert!(svg.contains(">One<") && svg.contains(">Two<"));
super::mermaid_to_svg("flowchart LR\na-->b\nb-->").expect("dangling edge");
}
#[test]
fn nested_subgraph_titles_render() {
let svg = super::mermaid_to_svg(
"flowchart TD\nsubgraph Wrapper\n subgraph Inner\n A --> B\n end\nend",
)
.unwrap();
assert!(svg.contains(">Wrapper<") && svg.contains(">Inner<"));
}
#[test]
fn self_loops_and_cycles_terminate() {
for src in [
"flowchart TD\nA --> A",
"flowchart TD\na --> b\nb --> c\nc --> b\nb --> b",
"flowchart\nA --> A\nsubgraph B\nB1 --> B1\nend",
] {
let svg = super::mermaid_to_svg(src).expect("render");
assert!(svg.starts_with("<?xml"), "{src:?}");
}
}
#[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?<"));
}
}