Skip to main content

graphviz_pure_render/
lib.rs

1//! Output renderers for `graphviz-pure` (`svg`, `json`, `json0`, `dot`, `xdot`, `canon`, `plain`).
2
3#![forbid(unsafe_code)]
4
5mod agwrite;
6pub mod emit;
7pub mod json;
8mod svg_engine;
9mod xdot_engine;
10
11/// Renders `-Tdot` (`xdot == false`) or `-Txdot`.
12pub fn render_dot_positioned(graph: &Graph, xdot: bool) -> String {
13    render_graph(
14        graph,
15        if xdot {
16            OutputFormat::Xdot
17        } else {
18            OutputFormat::Dot
19        },
20    )
21    .unwrap_or_default()
22}
23
24/// Renders `-Tplain`.
25pub fn render_plain(graph: &Graph) -> String {
26    render_graph(graph, OutputFormat::Plain).unwrap_or_default()
27}
28
29/// Renders `-Tjson` (`json0 == false`) or `-Tjson0`.
30pub fn render_json(graph: &Graph, json0: bool) -> Result<String> {
31    render_graph(
32        graph,
33        if json0 {
34            OutputFormat::Json0
35        } else {
36            OutputFormat::Json
37        },
38    )
39}
40pub use emit::reset_render_state;
41
42/// Renders `graph` as SVG (`-Tsvg`).
43pub fn render_svg(graph: &Graph) -> String {
44    let m = emit::model::Model::build(graph);
45    emit::run(&m, svg_engine::SvgEngine::default()).out
46}
47
48use graphviz_pure_core::{Graph, GraphvizError, Result};
49use graphviz_pure_parser::write_canon;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum OutputFormat {
53    Svg,
54    Json,
55    Json0,
56    Dot,
57    Xdot,
58    Canon,
59    Plain,
60}
61
62impl OutputFormat {
63    pub fn parse(name: &str) -> Result<Self> {
64        match name.trim().to_ascii_lowercase().as_str() {
65            "svg" => Ok(Self::Svg),
66            "json" => Ok(Self::Json),
67            "json0" => Ok(Self::Json0),
68            "dot" | "gv" => Ok(Self::Dot),
69            "xdot" | "xdot1.2" | "xdot1.4" => Ok(Self::Xdot),
70            "dot_json" | "xdot_json" => Ok(Self::Json),
71            "canon" => Ok(Self::Canon),
72            "plain" | "plain-ext" => Ok(Self::Plain),
73            other => Err(GraphvizError::UnsupportedFormat(other.to_string())),
74        }
75    }
76}
77
78pub fn render_graph(graph: &Graph, format: OutputFormat) -> Result<String> {
79    render_graph_named(graph, format, "")
80}
81
82/// `agfstnode`/`agfstout` edge order of an unlaid graph.
83fn input_edge_order(g: &Graph) -> Vec<usize> {
84    let mut out = vec![Vec::new(); g.nodes.len()];
85    for (i, e) in g.edges.iter().enumerate() {
86        out[e.tail.0 as usize].push(i);
87    }
88    for l in &mut out {
89        l.sort_by_key(|&e| (g.edges[e].head.0, e));
90    }
91    out.into_iter().flatten().collect()
92}
93
94/// `IS_CLUST_NODE(n)` for each node.
95fn clust_nodes(g: &Graph) -> Vec<bool> {
96    g.nodes
97        .iter()
98        .map(|n| n.attrs.contains_key(graphviz_pure_core::CLUST_NODE_KEY))
99        .collect()
100}
101
102/// Edge iteration order of `agfstnode`/`agfstout` (numbering of `_gvid` in JSON).
103fn edge_order(m: &emit::model::Model<'_>) -> Vec<usize> {
104    m.out_edges.iter().flatten().copied().collect()
105}
106
107/// The graph with layout attributes and, for xdot, draw operations attached.
108fn attached(
109    m: &emit::model::Model<'_>,
110    xdot: bool,
111    fixed_xdot: Option<&str>,
112) -> (Graph, Vec<std::collections::BTreeSet<String>>) {
113    let (mut g, _, _, mut forced) = emit::output::attach_attrs(m);
114    if xdot {
115        let eng = xdot_engine::XdotEngine::new(m.root_attrs().get("xdotversion"), fixed_xdot);
116        let eng = emit::run(m, eng);
117        for (kind, name, value) in eng.sets {
118            let attrs = match kind {
119                emit::job::ObjKind::Root => &mut g.graph_attrs,
120                emit::job::ObjKind::Cluster(c) => &mut g.subgraphs[m.clusters[c].sub].graph_attrs,
121                emit::job::ObjKind::Node(n) => &mut g.nodes[n].attrs,
122                emit::job::ObjKind::Edge(e) => &mut g.edges[e].attrs,
123            };
124            if let emit::job::ObjKind::Cluster(c) = kind {
125                forced[m.clusters[c].sub].insert(name.to_string());
126            }
127            attrs.insert(name.to_string(), value);
128        }
129    }
130    (g, forced)
131}
132
133/// Private graph attribute the no-layout engine sets (`graphviz_pure_layout::nop`).
134const NOP_KEY: &str = "\u{1}nop";
135
136/// Private graph attribute the neato-family engines set (`graphviz_pure_layout::common`):
137/// only dot runs `mark_lowclusters`, so their edges stay where they were declared.
138const NO_LOW_CLUSTERS_KEY: &str = "\u{1}nolowclust";
139
140/// Records the attributes the layout and renderers set in cgraph's dictionaries:
141/// `safe_dcl` for node and edge attributes, and graph values as local symbols (`agxset`).
142fn sync_dicts(g: &mut Graph, orig: &Graph) {
143    use graphviz_pure_core::AttrSym;
144    let set_graph = |d: &mut std::collections::BTreeMap<String, AttrSym>,
145                     now: &graphviz_pure_core::AttrMap,
146                     before: &graphviz_pure_core::AttrMap| {
147        for (k, v) in now {
148            if before.get(k) != Some(v) {
149                d.entry(k.clone()).or_default().defval = v.clone();
150            }
151        }
152    };
153    let (root_now, root_before) = (g.graph_attrs.clone(), &orig.graph_attrs);
154    // safe_dcl declares graph attributes on the root before setting them.
155    for k in root_now.keys() {
156        if !root_before.contains_key(k) {
157            g.dicts[0].graph.entry(k.clone()).or_default();
158        }
159    }
160    set_graph(&mut g.dicts[0].graph, &root_now, root_before);
161    for i in 0..g.subgraphs.len() {
162        let now = g.subgraphs[i].graph_attrs.clone();
163        for k in now.keys() {
164            if !orig.subgraphs[i].graph_attrs.contains_key(k) {
165                g.dicts[0].graph.entry(k.clone()).or_default();
166            }
167        }
168        set_graph(
169            &mut g.dicts[i + 1].graph,
170            &now,
171            &orig.subgraphs[i].graph_attrs,
172        );
173    }
174    let mut node_keys = std::collections::BTreeSet::new();
175    for n in &g.nodes {
176        node_keys.extend(n.attrs.keys().cloned());
177    }
178    for k in node_keys {
179        if !g.dicts.iter().any(|d| d.node.contains_key(&k)) {
180            g.dicts[0].node.insert(k, AttrSym::default());
181        }
182    }
183    let mut edge_keys = std::collections::BTreeSet::new();
184    for e in &g.edges {
185        edge_keys.extend(e.attrs.keys().cloned());
186    }
187    for k in edge_keys {
188        if !g.dicts.iter().any(|d| d.edge.contains_key(&k)) {
189            g.dicts[0].edge.insert(k, AttrSym::default());
190        }
191    }
192}
193
194/// dot's `mark_lowclusters`: an edge whose ends both lie in a cluster belongs to it and its
195/// ancestors.
196fn low_cluster_edges(graph: &Graph) -> Vec<std::collections::BTreeSet<usize>> {
197    let n = graph.subgraphs.len();
198    let mut parent: Vec<Option<usize>> = vec![None; n];
199    for (i, sg) in graph.subgraphs.iter().enumerate() {
200        for c in &sg.subgraphs {
201            parent[c.0 as usize] = Some(i);
202        }
203    }
204    let mut sets = vec![std::collections::BTreeSet::new(); n];
205    for (i, sg) in graph.subgraphs.iter().enumerate() {
206        if !sg.is_cluster {
207            continue;
208        }
209        let members: std::collections::BTreeSet<u32> = sg.nodes.iter().map(|x| x.0).collect();
210        for (ei, e) in graph.edges.iter().enumerate() {
211            if members.contains(&e.tail.0) && members.contains(&e.head.0) {
212                let mut cur = Some(i);
213                while let Some(c) = cur {
214                    sets[c].insert(ei);
215                    cur = parent[c];
216                }
217            }
218        }
219    }
220    sets
221}
222
223/// Renders `graph`; `name` is the requested format name (distinguishes `plain-ext`).
224pub fn render_graph_named(graph: &Graph, format: OutputFormat, name: &str) -> Result<String> {
225    graphviz_pure_core::charset::begin_render(
226        &graph.name,
227        graph.graph_attrs.get("charset").map(String::as_str),
228    );
229    if format == OutputFormat::Canon {
230        if graph.dicts.len() == graph.subgraphs.len() + 1 {
231            return Ok(agwrite::agwrite(graph, &[]));
232        }
233        return Ok(write_canon(graph));
234    }
235    let lname = name.to_ascii_lowercase();
236    if lname == "dot_json" || lname == "xdot_json" {
237        // No layout: the parsed graph, with any `_draw_` attributes expanded for xdot_json.
238        return Ok(json::write_json(
239            graph,
240            lname == "xdot_json",
241            &input_edge_order(graph),
242            &clust_nodes(graph),
243        ));
244    }
245    // Renderers other than dot, xdot, json and canon run before undoClusterEdges.
246    let drawn = match (format, graph.render_view.as_deref()) {
247        (OutputFormat::Svg, Some(v)) => v,
248        _ => graph,
249    };
250    let m = emit::model::Model::build(drawn);
251    Ok(match format {
252        OutputFormat::Svg => emit::run(&m, svg_engine::SvgEngine::default()).out,
253        OutputFormat::Json | OutputFormat::Json0 => {
254            let (g, _) = attached(&m, format == OutputFormat::Json, None);
255            json::write_json(
256                &g,
257                format == OutputFormat::Json,
258                &edge_order(&m),
259                &clust_nodes(graph),
260            )
261        }
262        OutputFormat::Dot | OutputFormat::Xdot => {
263            let (mut g, forced) = attached(
264                &m,
265                format == OutputFormat::Xdot,
266                match lname.as_str() {
267                    "xdot1.2" => Some("1.2"),
268                    "xdot1.4" => Some("1.4"),
269                    _ => None,
270                },
271            );
272            if g.dicts.len() == g.subgraphs.len() + 1 {
273                sync_dicts(&mut g, graph);
274                let extra = if graph.graph_attrs.contains_key(NOP_KEY)
275                    || graph.graph_attrs.contains_key(NO_LOW_CLUSTERS_KEY)
276                {
277                    Vec::new()
278                } else {
279                    low_cluster_edges(graph)
280                };
281                agwrite::agwrite(&g, &extra)
282            } else {
283                emit::output::propagate_inherited(&mut g, graph, &forced);
284                graphviz_pure_parser::write_canon_forced(&g, &forced)
285            }
286        }
287        OutputFormat::Plain => {
288            let zoom = emit::job::Job::new(
289                &m,
290                emit::job::Features {
291                    flags: 0,
292                    default_pad: 0.0,
293                    knowncolors: &[],
294                    rgba: false,
295                },
296                0,
297            )
298            .zoom;
299            emit::output::write_plain(&m, zoom, name.eq_ignore_ascii_case("plain-ext"))
300        }
301        OutputFormat::Canon => unreachable!(),
302    })
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use graphviz_pure_core::{BezierSegment, BoundingBox, Point, Spline};
309    use graphviz_pure_parser::parse_dot;
310
311    // The model accepts only clusters below a graph from its exported `GD_clust` list,
312    // once each below the root: a bad list from a caller that skipped layout cannot
313    // make the cluster tree cyclic or multiply it.
314    #[test]
315    fn cluster_list_key_keeps_only_descendants() {
316        let mut g = parse_dot(
317            "digraph G { subgraph cluster_a { a subgraph cluster_c { c } } subgraph cluster_b { b } }",
318        )
319        .expect("parse");
320        let idx = |name: &str| g.subgraphs.iter().position(|s| s.name == name).unwrap();
321        let (a, c, b) = (idx("cluster_a"), idx("cluster_c"), idx("cluster_b"));
322        g.bb = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
323        for s in &mut g.subgraphs {
324            s.bb = BoundingBox::new(10.0, 10.0, 50.0, 50.0);
325        }
326        g.subgraphs[a].graph_attrs.insert(
327            graphviz_pure_core::CLUSTER_LIST_KEY.to_string(),
328            format!("{a} {c} {c} {b} 99"),
329        );
330        let svg = render_graph(&g, OutputFormat::Svg).expect("render");
331        let titles: Vec<&str> = svg
332            .split("class=\"cluster\">\n<title>")
333            .skip(1)
334            .map(|s| &s[..s.find("</title>").unwrap()])
335            .collect();
336        assert_eq!(titles, ["cluster_a", "cluster_c", "cluster_b"]);
337    }
338
339    fn sample_laid_out_graph() -> Graph {
340        let mut g = parse_dot(
341            r##"
342            digraph G {
343                label="Sample Graph";
344                subgraph cluster_0 {
345                    label="Cluster A";
346                    style=filled;
347                    fillcolor="#eeeeee";
348                    a [label="Node A", shape=box, style="filled,rounded", fillcolor=lightblue];
349                    b [label="{f0|f1|f2}", shape=record];
350                }
351                c [label=<<b>HTML</b><br/>Node>, shape=diamond];
352                a -> b [label="ab"];
353                b -> c [arrowhead=vee, style=dashed];
354            }
355            "##,
356        )
357        .expect("parse sample graph");
358
359        g.bb = BoundingBox::new(0.0, 0.0, 220.0, 180.0);
360
361        if let Some(sub) = g.subgraphs.first_mut() {
362            sub.bb = BoundingBox::new(16.0, 64.0, 204.0, 168.0);
363        }
364
365        if let Some(a) = g.nodes.get_mut(0) {
366            a.pos = Point::new(60.0, 110.0);
367            a.width = 1.0;
368            a.height = 0.5;
369        }
370        if let Some(b) = g.nodes.get_mut(1) {
371            b.pos = Point::new(150.0, 110.0);
372            b.width = 1.2;
373            b.height = 0.5;
374        }
375        if let Some(c) = g.nodes.get_mut(2) {
376            c.pos = Point::new(150.0, 36.0);
377            c.width = 1.1;
378            c.height = 0.6;
379        }
380
381        if let Some(e0) = g.edges.get_mut(0) {
382            e0.spline = Spline {
383                segments: vec![BezierSegment {
384                    points: vec![
385                        Point::new(96.0, 110.0),
386                        Point::new(102.0, 110.0),
387                        Point::new(105.0, 110.0),
388                        Point::new(108.0, 110.0),
389                    ],
390                    start_arrow: None,
391                    end_arrow: Some(Point::new(114.0, 110.0)),
392                }],
393            };
394            e0.label_pos = Some(Point::new(105.0, 122.0));
395        }
396
397        if let Some(e1) = g.edges.get_mut(1) {
398            e1.spline = Spline {
399                segments: vec![BezierSegment {
400                    points: vec![
401                        Point::new(150.0, 92.0),
402                        Point::new(150.0, 80.0),
403                        Point::new(150.0, 70.0),
404                        Point::new(150.0, 62.0),
405                    ],
406                    start_arrow: None,
407                    end_arrow: Some(Point::new(150.0, 58.0)),
408                }],
409            };
410        }
411
412        g
413    }
414
415    #[test]
416    fn svg_renderer_emits_graphviz_structure_shapes_and_edges() {
417        let g = sample_laid_out_graph();
418        let svg = render_graph(&g, OutputFormat::Svg).expect("render svg");
419
420        assert!(svg.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"));
421        assert!(svg.contains("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\""));
422        assert!(svg.contains("<svg width=\"228pt\" height=\"188pt\""));
423        assert!(svg.contains("viewBox=\"0.00 0.00 228.00 188.00\""));
424        assert!(svg.contains(
425            "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 184)\">"
426        ));
427        assert!(svg.contains("<g id=\"clust1\" class=\"cluster\">"));
428        assert!(svg.contains("<g id=\"node1\" class=\"node\">"));
429        assert!(svg.contains("<g id=\"node2\" class=\"node\">"));
430        assert!(svg.contains("<g id=\"node3\" class=\"node\">"));
431        assert!(svg.contains("<g id=\"edge1\" class=\"edge\">"));
432        assert!(svg.contains("<g id=\"edge2\" class=\"edge\">"));
433        assert!(svg.contains("<title>a&#45;&gt;b</title>"));
434        assert!(svg.contains("<path fill=\"none\""));
435        assert!(svg.contains("font-weight=\"bold\""));
436    }
437
438    #[test]
439    fn json_and_json0_render_graphviz_schema() {
440        let g = sample_laid_out_graph();
441        let json0_str = render_graph(&g, OutputFormat::Json0).expect("render json0");
442        let v0: serde_json::Value = serde_json::from_str(&json0_str).expect("valid json0");
443        assert_eq!(v0["name"], "G");
444        assert_eq!(v0["directed"], true);
445        assert_eq!(v0["strict"], false);
446        assert_eq!(v0["bb"], "0,0,220,180");
447        assert_eq!(v0["_subgraph_cnt"], 1);
448        assert!(v0["objects"].is_array());
449        assert_eq!(v0["objects"].as_array().unwrap().len(), 4);
450        assert!(v0["objects"][0].get("_draw_").is_none());
451        assert_eq!(v0["edges"].as_array().unwrap().len(), 2);
452        assert!(
453            v0["edges"][0]["pos"]
454                .as_str()
455                .unwrap()
456                .starts_with("e,114,110 ")
457        );
458
459        let json_str = render_graph(&g, OutputFormat::Json).expect("render json");
460        let v: serde_json::Value = serde_json::from_str(&json_str).expect("valid json");
461        assert!(v["_draw_"].is_array());
462        assert!(v["objects"][0]["_draw_"].is_array());
463        assert!(v["objects"][1]["_draw_"].is_array());
464        assert!(v["edges"][0]["_draw_"].is_array());
465        assert!(v["edges"][0]["_hdraw_"].is_array());
466    }
467
468    #[test]
469    fn dot_xdot_and_plain_outputs_include_layout_positions() {
470        let g = sample_laid_out_graph();
471
472        let dot = render_graph(&g, OutputFormat::Dot).expect("render dot");
473        assert!(dot.contains("bb=\"0,0,220,180\""));
474        assert!(dot.contains("pos=\"60,110\""));
475        assert!(dot.contains("e,114,110 96,110 102,110 105,110 108,110"));
476        assert!(!dot.contains("_draw_"));
477
478        let xdot = render_graph(&g, OutputFormat::Xdot).expect("render xdot");
479        assert!(xdot.contains("xdotversion=1.7"));
480        assert!(xdot.contains("_draw_="));
481        assert!(xdot.contains("_ldraw_="));
482        assert!(xdot.contains("_hdraw_="));
483
484        let plain = render_graph(&g, OutputFormat::Plain).expect("render plain");
485        assert!(plain.starts_with("graph 1 "));
486        assert!(plain.contains("\nnode a "));
487        assert!(plain.contains("\nnode b "));
488        assert!(plain.contains("\nedge a b 4 "));
489        assert!(plain.ends_with("stop\n"));
490    }
491}