1use resvg::{tiny_skia, usvg};
11
12pub mod d2;
16pub mod dot;
17pub mod drawio;
18pub mod flowchart;
19pub mod flowchart_svg;
20pub mod kymojson;
21pub mod layout;
22pub mod math;
23pub mod mermaid;
24pub mod model;
25pub mod sequence;
26
27#[cfg(feature = "python")]
29mod python;
30#[cfg(feature = "wasm")]
31mod wasm;
32
33#[cfg(feature = "bpmn")]
37pub mod bpmn;
38
39static EXTRA_FONTS: std::sync::OnceLock<std::sync::Mutex<Vec<Vec<u8>>>> =
46 std::sync::OnceLock::new();
47
48pub fn register_font(bytes: Vec<u8>) {
53 EXTRA_FONTS
54 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
55 .lock()
56 .unwrap()
57 .push(bytes);
58}
59
60macro_rules! load_extra_fonts {
65 ($db:expr) => {
66 if let Some(fonts) = EXTRA_FONTS.get() {
67 let fonts = fonts.lock().unwrap();
68 if !fonts.is_empty() {
69 let db = $db;
70 let before = db.faces().count();
71 for data in fonts.iter() {
72 db.load_font_data(data.clone());
73 }
74 let family = db
75 .faces()
76 .skip(before)
77 .find_map(|f| f.families.first().map(|(name, _)| name.clone()));
78 if let Some(family) = family {
79 db.set_sans_serif_family(family.clone());
80 db.set_serif_family(family.clone());
81 db.set_cursive_family(family.clone());
82 db.set_fantasy_family(family.clone());
83 db.set_monospace_family(family);
84 }
85 }
86 }
87 };
88}
89
90#[derive(Debug)]
92pub enum RenderError {
93 Parse(usvg::Error),
95 Size { width: u32, height: u32 },
97 Encode(String),
99 Pdf(String),
101}
102
103impl std::fmt::Display for RenderError {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 RenderError::Parse(e) => write!(f, "invalid SVG: {e}"),
107 RenderError::Size { width, height } => {
108 write!(f, "invalid raster size {width}x{height}")
109 }
110 RenderError::Encode(e) => write!(f, "PNG encoding failed: {e}"),
111 RenderError::Pdf(e) => write!(f, "SVG→PDF conversion failed: {e}"),
112 }
113 }
114}
115
116impl std::error::Error for RenderError {}
117
118impl From<usvg::Error> for RenderError {
119 fn from(e: usvg::Error) -> Self {
120 RenderError::Parse(e)
121 }
122}
123
124pub fn svg_to_png(svg: &[u8], scale: f32) -> Result<Vec<u8>, RenderError> {
130 let mut opt = usvg::Options::default();
131 #[cfg(feature = "system-fonts")]
132 opt.fontdb_mut().load_system_fonts();
133 load_extra_fonts!(opt.fontdb_mut());
134
135 let tree = usvg::Tree::from_data(svg, &opt)?;
136 let size = tree.size();
137
138 let width = ((size.width() * scale).round() as i64).clamp(1, u32::MAX as i64) as u32;
139 let height = ((size.height() * scale).round() as i64).clamp(1, u32::MAX as i64) as u32;
140
141 let mut pixmap =
142 tiny_skia::Pixmap::new(width, height).ok_or(RenderError::Size { width, height })?;
143
144 let transform = tiny_skia::Transform::from_scale(scale, scale);
145 resvg::render(&tree, transform, &mut pixmap.as_mut());
146
147 pixmap
148 .encode_png()
149 .map_err(|e| RenderError::Encode(e.to_string()))
150}
151
152#[cfg(feature = "pdf")]
161pub fn svg_to_pdf(svg: &[u8]) -> Result<Vec<u8>, RenderError> {
162 use svg2pdf::usvg as pdf_usvg;
163
164 let mut opt = pdf_usvg::Options::default();
165 #[cfg(feature = "system-fonts")]
166 opt.fontdb_mut().load_system_fonts();
167 load_extra_fonts!(opt.fontdb_mut());
168
169 let tree = pdf_usvg::Tree::from_data(svg, &opt).map_err(|e| RenderError::Pdf(e.to_string()))?;
170
171 svg2pdf::to_pdf(
172 &tree,
173 svg2pdf::ConversionOptions::default(),
174 svg2pdf::PageOptions::default(),
175 )
176 .map_err(|e| RenderError::Pdf(e.to_string()))
177}
178
179pub fn mermaid_to_kymojson(src: &str) -> Result<String, mermaid::MermaidError> {
185 let fc = mermaid::parse(src)?;
186 let diagram = layout::layout_flowchart(&fc);
187 Ok(kymojson::export(&diagram))
188}
189
190pub fn mermaid_to_d2(src: &str) -> Result<String, mermaid::MermaidError> {
196 Ok(flowchart::emit::to_d2(&mermaid::parse(src)?))
197}
198
199pub fn mermaid_to_dot(src: &str) -> Result<String, mermaid::MermaidError> {
201 Ok(flowchart::emit::to_dot(&mermaid::parse(src)?))
202}
203
204pub fn mermaid_to_mermaid(src: &str) -> Result<String, mermaid::MermaidError> {
206 Ok(flowchart::emit::to_mermaid(&mermaid::parse(src)?))
207}
208
209pub fn mermaid_to_xmi(src: &str) -> Result<String, mermaid::MermaidError> {
215 Ok(sequence::emit::to_xmi(&mermaid::parse_sequence(src)?))
216}
217
218pub fn mermaid_to_sequence_svg(src: &str) -> Result<String, mermaid::MermaidError> {
221 let mut seq = mermaid::parse_sequence(src)?;
222 for item in &mut seq.items {
223 render_sequence_item_math(item);
224 }
225 Ok(sequence::svg::render(&seq))
226}
227
228fn render_sequence_item_math(item: &mut sequence::Item) {
230 match item {
231 sequence::Item::Message(m) => m.text = clean_label(&m.text),
232 sequence::Item::Note(n) => n.text = clean_label(&n.text),
233 sequence::Item::Fragment(f) => {
234 for op in &mut f.operands {
235 op.guard = clean_label(&op.guard);
236 for it in &mut op.items {
237 render_sequence_item_math(it);
238 }
239 }
240 }
241 sequence::Item::Activate(_)
242 | sequence::Item::Deactivate(_)
243 | sequence::Item::Autonumber(_) => {}
244 }
245}
246
247pub fn mermaid_to_mdj(src: &str) -> Result<String, mermaid::MermaidError> {
254 Ok(sequence::mdj::to_mdj(&mermaid::parse_sequence(src)?))
255}
256
257pub fn mermaid_to_gaphor(src: &str) -> Result<String, mermaid::MermaidError> {
264 Ok(sequence::gaphor::to_gaphor(&mermaid::parse_sequence(src)?))
265}
266
267pub fn mermaid_to_drawio(src: &str) -> Result<String, mermaid::MermaidError> {
274 let fc = mermaid::parse(src)?;
275 Ok(drawio::to_drawio(&layout::layout_flowchart(&fc)))
276}
277
278pub fn mermaid_to_svg(src: &str) -> Result<String, mermaid::MermaidError> {
282 let mut fc = mermaid::parse(src)?;
283 render_flowchart_math(&mut fc);
284 Ok(flowchart_svg::render(&layout::layout_flowchart(&fc)))
285}
286
287fn clean_label(s: &str) -> String {
290 math::strip_br(&math::render(s))
294}
295
296fn render_flowchart_math(fc: &mut flowchart::Flowchart) {
298 for n in &mut fc.nodes {
299 n.label = clean_label(&n.label);
300 }
301 for e in &mut fc.edges {
302 e.label = clean_label(&e.label);
303 }
304 for g in &mut fc.subgraphs {
305 g.title = clean_label(&g.title);
306 }
307}
308
309pub fn mermaid_state_to_svg(src: &str) -> Result<String, mermaid::MermaidError> {
311 let mut fc = mermaid::parse_state(src)?;
312 render_flowchart_math(&mut fc);
313 Ok(flowchart_svg::render(&layout::layout_flowchart(&fc)))
314}
315
316pub fn d2_to_svg(src: &str) -> Result<String, d2::D2Error> {
319 let fc = d2::parse(src)?;
320 Ok(flowchart_svg::render(&layout::layout_flowchart(&fc)))
321}
322
323pub fn d2_to_kymojson(src: &str) -> Result<String, d2::D2Error> {
326 let fc = d2::parse(src)?;
327 Ok(kymojson::export(&layout::layout_flowchart(&fc)))
328}
329
330pub fn dot_to_svg(src: &str) -> Result<String, dot::DotError> {
334 let fc = dot::parse(src)?;
335 Ok(flowchart_svg::render(&layout::layout_flowchart(&fc)))
336}
337
338pub fn dot_to_kymojson(src: &str) -> Result<String, dot::DotError> {
340 let fc = dot::parse(src)?;
341 Ok(kymojson::export(&layout::layout_flowchart(&fc)))
342}
343
344#[cfg(feature = "bpmn")]
349pub fn drawio_from_kymojson(json: &str) -> Result<String, String> {
350 drawio::to_drawio_kymojson(json)
351}
352
353#[cfg(test)]
354mod tests {
355 const SVG: &[u8] =
356 br##"<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20"><rect width="40" height="20" fill="#09f"/></svg>"##;
357
358 #[test]
359 fn png_has_magic() {
360 let png = super::svg_to_png(SVG, 1.0).expect("render png");
361 assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
362 }
363
364 #[cfg(feature = "pdf")]
365 #[test]
366 fn pdf_has_magic() {
367 let pdf = super::svg_to_pdf(SVG).expect("render pdf");
368 assert_eq!(&pdf[..5], b"%PDF-");
369 }
370
371 #[test]
372 fn autonumber_off_keeps_counting() {
373 let svg = super::mermaid_to_sequence_svg(
376 "sequenceDiagram\nautonumber 5 5\nA->>B: a\nA->>B: b\nautonumber off\nA->>B: c\nautonumber\nA->>B: d",
377 )
378 .unwrap();
379 assert!(svg.contains(">5 a<") && svg.contains(">10 b<"));
380 assert!(svg.contains(">c<") && !svg.contains(">15 c<")); assert!(svg.contains(">20 d<")); }
383
384 #[test]
385 fn multiline_node_data_and_continuation() {
386 let svg = super::mermaid_to_svg(
388 "flowchart TB\nA@{\n shape: circle\n label: \"Hi\"\n}\nA --> B",
389 )
390 .expect("node-data block");
391 assert!(svg.starts_with("<?xml") && svg.contains(">Hi<"));
392
393 let svg = super::mermaid_to_svg("flowchart TB\nA[One]\n--> B[Two]").expect("continuation");
395 assert!(svg.contains(">One<") && svg.contains(">Two<"));
396
397 super::mermaid_to_svg("flowchart LR\na-->b\nb-->").expect("dangling edge");
399 }
400
401 #[test]
402 fn nested_subgraph_titles_render() {
403 let svg = super::mermaid_to_svg(
406 "flowchart TD\nsubgraph Wrapper\n subgraph Inner\n A --> B\n end\nend",
407 )
408 .unwrap();
409 assert!(svg.contains(">Wrapper<") && svg.contains(">Inner<"));
410 }
411
412 #[test]
413 fn self_loops_and_cycles_terminate() {
414 for src in [
417 "flowchart TD\nA --> A",
418 "flowchart TD\na --> b\nb --> c\nc --> b\nb --> b",
419 "flowchart\nA --> A\nsubgraph B\nB1 --> B1\nend",
420 ] {
421 let svg = super::mermaid_to_svg(src).expect("render");
422 assert!(svg.starts_with("<?xml"), "{src:?}");
423 }
424 }
425
426 #[test]
427 fn mermaid_and_d2_to_svg() {
428 let mmd = super::mermaid_to_svg("flowchart TD\nA[Go] --> B{ok?}").unwrap();
430 assert!(mmd.starts_with("<?xml") && mmd.contains("fc-shape") && mmd.contains(">ok?<"));
431 let d2src = "direction: down\nA: Go\nB: \"ok?\" { shape: diamond }\nA -> B";
432 let d2 = super::d2_to_svg(d2src).unwrap();
433 assert!(d2.contains("<polygon class=\"fc-shape\"") && d2.contains(">ok?<"));
434 assert!(super::d2_to_kymojson(d2src)
436 .unwrap()
437 .contains("\"shape\": \"diamond\""));
438 let dotsrc =
440 "digraph G {\n A [label=\"Go\"];\n B [label=\"ok?\", shape=diamond];\n A -> B;\n}";
441 let dot = super::dot_to_svg(dotsrc).unwrap();
442 assert!(dot.contains("<polygon class=\"fc-shape\"") && dot.contains(">ok?<"));
443 }
444}