use resvg::{tiny_skia, usvg};
pub use kymo_graph::{
d2, dagre_svg, dot, drawio, flowchart, flowchart_svg, kymojson, math, metrics, model, style,
};
pub use kymo_layout::{self as layout, dagre as layout_dagre};
pub use kymo_mermaid::{classdiagram, mermaid, sequence};
pub use kymo_mermaid::{
mermaid_to_d2, mermaid_to_drawio, mermaid_to_dot, mermaid_to_gaphor, mermaid_to_kymojson,
mermaid_to_mdj, mermaid_to_mermaid, mermaid_to_svg, mermaid_to_svg_auto, mermaid_to_svg_dagre,
mermaid_to_svg_styled,
mermaid_to_xmi, mermaid_block_to_svg, mermaid_class_to_svg, mermaid_er_to_svg,
mermaid_kanban_to_svg, mermaid_mindmap_to_svg, mermaid_requirement_to_svg,
mermaid_to_sequence_svg, mermaid_state_to_svg, render_flowchart_math,
};
#[cfg(feature = "kymo")]
pub mod kymo;
#[cfg(feature = "kymo")]
pub mod editor;
#[cfg(feature = "mobile")]
uniffi::setup_scaffolding!();
#[cfg(feature = "mobile")]
mod ffi;
#[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 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<") && svg.contains(">a<")); assert!(svg.contains(">10<") && svg.contains(">b<")); assert!(svg.contains(">c<") && !svg.contains(">15<")); assert!(svg.contains(">20<") && svg.contains(">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?<"));
}
}