Coverage Report

Created: 2026-03-15 06:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/igraph/src/io/dot.c
Line
Count
Source
1
/*
2
   igraph library.
3
   Copyright (C) 2005-2020  The igraph development team
4
5
   This program is free software; you can redistribute it and/or modify
6
   it under the terms of the GNU General Public License as published by
7
   the Free Software Foundation; either version 2 of the License, or
8
   (at your option) any later version.
9
10
   This program is distributed in the hope that it will be useful,
11
   but WITHOUT ANY WARRANTY; without even the implied warranty of
12
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
   GNU General Public License for more details.
14
15
   You should have received a copy of the GNU General Public License
16
   along with this program; if not, write to the Free Software
17
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18
   02110-1301 USA
19
20
*/
21
22
#include "igraph_foreign.h"
23
24
#include "igraph_attributes.h"
25
#include "igraph_error.h"
26
#include "igraph_interface.h"
27
#include "igraph_memory.h"
28
#include "igraph_version.h"
29
30
#include "graph/attributes.h"
31
#include "internal/hacks.h" /* strcasecmp & strdup */
32
#include "math/safe_intop.h" /* IGRAPH_MAX_EXACT_REAL */
33
34
#include <ctype.h>
35
#include <string.h>
36
37
1.16M
#define CHECK(cmd) do { int ret=cmd; if (ret<0) IGRAPH_ERROR("Writing DOT format failed.", IGRAPH_EFILE); } while (0)
38
39
837k
static igraph_error_t dot_escape(const char *orig, igraph_vector_char_t* result) {
40
    /* do we have to escape the string at all? */
41
837k
    igraph_int_t i, j, len = strlen(orig), newlen = 0;
42
837k
    igraph_bool_t need_quote = false, is_number = true;
43
44
    /* first, check whether the string is equal to some reserved word, or empty */
45
837k
    if (!strcasecmp(orig, "graph") || !strcasecmp(orig, "digraph") ||
46
837k
        !strcasecmp(orig, "node") || !strcasecmp(orig, "edge") ||
47
837k
        !strcasecmp(orig, "strict") || !strcasecmp(orig, "subgraph") || len == 0) {
48
108k
        need_quote = true;
49
108k
        is_number = false;
50
108k
    }
51
52
    /* next, check whether we need to escape the string for any other reason.
53
     * Also update is_number and newlen */
54
258M
    for (i = 0; i < len; i++) {
55
257M
        if (isdigit(orig[i])) {
56
8.33M
            newlen++;
57
249M
        } else if (orig[i] == '-' && i == 0) {
58
3.55k
            newlen++;
59
249M
        } else if (orig[i] == '.') {
60
1.08M
            if (is_number) {
61
3.76k
                newlen++;
62
1.07M
            } else {
63
1.07M
                need_quote = true;
64
1.07M
                newlen++;
65
1.07M
            }
66
248M
        } else if (orig[i] == '_') {
67
3.12M
            is_number = false; newlen++;
68
245M
        } else if (orig[i] == '\\' || orig[i] == '"' || orig[i] == '\n') {
69
26.3M
            need_quote = true; is_number = false; newlen += 2; /* will be escaped */
70
218M
        } else if (isalpha(orig[i])) {
71
38.9M
            is_number = false; newlen++;
72
179M
        } else {
73
179M
            is_number = false; need_quote = true; newlen++;
74
179M
        }
75
257M
    }
76
837k
    if (is_number && len > 0 && orig[len - 1] == '.') {
77
485
        is_number = false;
78
485
    }
79
837k
    if (!is_number && isdigit(orig[0])) {
80
3.72k
        need_quote = true;
81
3.72k
    }
82
83
837k
    if (is_number || !need_quote) {
84
683k
        IGRAPH_CHECK(igraph_vector_char_resize(result, newlen + 1));
85
683k
        memcpy(VECTOR(*result), orig, newlen);
86
683k
    } else {
87
154k
        newlen += 2;
88
154k
        IGRAPH_CHECK(igraph_vector_char_resize(result, newlen + 1));
89
154k
        VECTOR(*result)[0] = '"';
90
154k
        VECTOR(*result)[newlen - 1] = '"';
91
92
        /* Escape quotes, backslashes and newlines.
93
         * Even though the format spec at https://graphviz.org/doc/info/lang.html
94
         * claims that only quotes need escaping, escaping backslashes appears to
95
         * be necessary as well for GraphViz to render labels correctly.
96
         * Tested with GraphViz 2.50. */
97
245M
        for (i = 0, j = 1; i < len; i++) {
98
245M
            if (orig[i] == '\n') {
99
10.9M
                VECTOR(*result)[j++] = '\\';
100
10.9M
                VECTOR(*result)[j++] = 'n';
101
10.9M
                continue;
102
10.9M
            }
103
234M
            if (orig[i] == '\\' || orig[i] == '"') {
104
15.4M
                VECTOR(*result)[j++] = '\\';
105
15.4M
            }
106
234M
            VECTOR(*result)[j++] = orig[i];
107
234M
        }
108
154k
    }
109
837k
    VECTOR(*result)[newlen] = 0;
110
111
837k
    return IGRAPH_SUCCESS;
112
837k
}
113
114
/* Writes exactly representable integral values in standard integer notation, without decimal points or e-notation.
115
 * Floating point values that are written with e-notation are quoted, otherwise the Graphviz parser cannot handle them.
116
 */
117
76.2k
static igraph_error_t fprint_integral_or_precise(FILE *file, igraph_real_t x, igraph_vector_char_t *buf) {
118
76.2k
    if (fabs(x) <= IGRAPH_MAX_EXACT_REAL && floor(x) == x) {
119
        /* write exactly representable integral values in standard integer notation;
120
         * the above conditional skips +-Inf and NaN */
121
4.47k
        CHECK(fprintf(file, "%.f", x));
122
71.8k
    } else {
123
        /* write as precise float and quote if necessary */
124
71.8k
        char str[50]; /* large enough to hold any precisely printed real */
125
71.8k
        CHECK(igraph_real_snprintf_precise(str, sizeof(str) / sizeof(str[0]), x));
126
71.8k
        IGRAPH_CHECK(dot_escape(str, buf));
127
71.8k
        CHECK(fputs(VECTOR(*buf), file));
128
71.8k
    }
129
76.2k
    return IGRAPH_SUCCESS;
130
76.2k
}
131
132
/**
133
 * \function igraph_write_graph_dot
134
 * \brief Write the graph to a stream in DOT format.
135
 *
136
 * </para><para>
137
 * DOT is the format used by the widely known GraphViz software, see
138
 * http://www.graphviz.org for details. The grammar of the DOT format
139
 * can be found here: http://www.graphviz.org/doc/info/lang.html
140
 *
141
 * </para><para>
142
 * This is only a preliminary implementation, no visualization
143
 * information is written.
144
 *
145
 * </para><para>
146
 * This format is meant solely for interoperability with Graphviz.
147
 * It is not recommended for data exchange or archival.
148
 *
149
 * \param graph The graph to write to the stream.
150
 * \param outstream The stream to write the file to.
151
 * \return Error code.
152
 *
153
 * Time complexity: should be proportional to the number of characters written
154
 * to the file.
155
 *
156
 * \sa \ref igraph_write_graph_graphml() for a more modern format.
157
 *
158
 * \example examples/simple/dot.c
159
 */
160
3.51k
igraph_error_t igraph_write_graph_dot(const igraph_t *graph, FILE* outstream) {
161
3.51k
    const igraph_int_t no_of_nodes = igraph_vcount(graph);
162
3.51k
    const igraph_int_t no_of_edges = igraph_ecount(graph);
163
3.51k
    const igraph_bool_t directed = igraph_is_directed(graph);
164
3.51k
    const char *edgeop = directed ? "->" : "--";
165
3.51k
    igraph_strvector_t gnames, vnames, enames;
166
3.51k
    igraph_vector_int_t gtypes, vtypes, etypes;
167
3.51k
    igraph_vector_t numv;
168
3.51k
    igraph_strvector_t strv;
169
3.51k
    igraph_vector_bool_t boolv;
170
3.51k
    igraph_vector_char_t buf, buf2;
171
172
3.51k
    IGRAPH_STRVECTOR_INIT_FINALLY(&gnames, 0);
173
3.51k
    IGRAPH_STRVECTOR_INIT_FINALLY(&vnames, 0);
174
3.51k
    IGRAPH_STRVECTOR_INIT_FINALLY(&enames, 0);
175
3.51k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&gtypes, 0);
176
3.51k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&vtypes, 0);
177
3.51k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&etypes, 0);
178
3.51k
    IGRAPH_CHECK(igraph_i_attribute_get_info(graph,
179
3.51k
                 &gnames, &gtypes,
180
3.51k
                 &vnames, &vtypes,
181
3.51k
                 &enames, &etypes));
182
183
3.51k
    IGRAPH_VECTOR_INIT_FINALLY(&numv, 0);
184
3.51k
    IGRAPH_STRVECTOR_INIT_FINALLY(&strv, 0);
185
3.51k
    IGRAPH_VECTOR_BOOL_INIT_FINALLY(&boolv, 0);
186
187
3.51k
    IGRAPH_VECTOR_CHAR_INIT_FINALLY(&buf, 0);
188
3.51k
    IGRAPH_VECTOR_CHAR_INIT_FINALLY(&buf2, 0);
189
190
3.51k
    CHECK(fprintf(outstream, "/* Created by igraph %s */\n", IGRAPH_VERSION));
191
192
3.51k
    if (directed) {
193
138
        CHECK(fprintf(outstream, "digraph {\n"));
194
3.37k
    } else {
195
3.37k
        CHECK(fprintf(outstream, "graph {\n"));
196
3.37k
    }
197
198
    /* Write the graph attributes */
199
3.51k
    if (igraph_vector_int_size(&gtypes) > 0) {
200
520
        CHECK(fprintf(outstream, "  graph [\n"));
201
1.36k
        for (igraph_int_t i = 0; i < igraph_vector_int_size(&gtypes); i++) {
202
846
            const char *name;
203
846
            name = igraph_strvector_get(&gnames, i);
204
846
            IGRAPH_CHECK(dot_escape(name, &buf));
205
846
            if (VECTOR(gtypes)[i] == IGRAPH_ATTRIBUTE_NUMERIC) {
206
85
                IGRAPH_CHECK(igraph_i_attribute_get_numeric_graph_attr(graph, name, &numv));
207
85
                CHECK(fprintf(outstream, "    %s=", VECTOR(buf)));
208
85
                IGRAPH_CHECK(fprint_integral_or_precise(outstream, VECTOR(numv)[0], &buf));
209
85
                CHECK(fputc('\n', outstream));
210
761
            } else if (VECTOR(gtypes)[i] == IGRAPH_ATTRIBUTE_STRING) {
211
611
                const char *s;
212
611
                IGRAPH_CHECK(igraph_i_attribute_get_string_graph_attr(graph, name, &strv));
213
611
                s = igraph_strvector_get(&strv, 0);
214
611
                IGRAPH_CHECK(dot_escape(s, &buf2));
215
611
                CHECK(fprintf(outstream, "    %s=%s\n", VECTOR(buf), VECTOR(buf2)));
216
611
            } else if (VECTOR(gtypes)[i] == IGRAPH_ATTRIBUTE_BOOLEAN) {
217
150
                IGRAPH_CHECK(igraph_i_attribute_get_bool_graph_attr(graph, name, &boolv));
218
150
                CHECK(fprintf(outstream, "    %s=%d\n", VECTOR(buf), VECTOR(boolv)[0] ? 1 : 0));
219
150
                IGRAPH_WARNING("Boolean graph attribute was converted to numeric.");
220
150
            } else {
221
0
                IGRAPH_WARNING("A non-numeric, non-string, non-boolean graph attribute was ignored.");
222
0
            }
223
846
        }
224
520
        CHECK(fprintf(outstream, "  ];\n"));
225
520
    }
226
227
    /* Write the vertices */
228
3.51k
    if (igraph_vector_int_size(&vtypes) > 0) {
229
36.2k
        for (igraph_int_t i = 0; i < no_of_nodes; i++) {
230
32.7k
            CHECK(fprintf(outstream, "  %" IGRAPH_PRId " [\n", i));
231
84.2k
            for (igraph_int_t j = 0; j < igraph_vector_int_size(&vtypes); j++) {
232
51.5k
                const char *name;
233
51.5k
                name = igraph_strvector_get(&vnames, j);
234
51.5k
                IGRAPH_CHECK(dot_escape(name, &buf));
235
51.5k
                if (VECTOR(vtypes)[j] == IGRAPH_ATTRIBUTE_NUMERIC) {
236
4.90k
                    IGRAPH_CHECK(igraph_i_attribute_get_numeric_vertex_attr(graph, name, igraph_vss_1(i), &numv));
237
4.90k
                    CHECK(fprintf(outstream, "    %s=", VECTOR(buf)));
238
4.90k
                    IGRAPH_CHECK(fprint_integral_or_precise(outstream, VECTOR(numv)[0], &buf));
239
4.90k
                    CHECK(fputc('\n', outstream));
240
46.6k
                } else if (VECTOR(vtypes)[j] == IGRAPH_ATTRIBUTE_STRING) {
241
42.1k
                    const char *s;
242
42.1k
                    IGRAPH_CHECK(igraph_i_attribute_get_string_vertex_attr(graph, name, igraph_vss_1(i), &strv));
243
42.1k
                    s = igraph_strvector_get(&strv, 0);
244
42.1k
                    IGRAPH_CHECK(dot_escape(s, &buf2));
245
42.1k
                    CHECK(fprintf(outstream, "    %s=%s\n", VECTOR(buf), VECTOR(buf2)));
246
42.1k
                } else if (VECTOR(vtypes)[j] == IGRAPH_ATTRIBUTE_BOOLEAN) {
247
4.52k
                    IGRAPH_CHECK(igraph_i_attribute_get_bool_vertex_attr(graph, name, igraph_vss_1(i), &boolv));
248
4.52k
                    CHECK(fprintf(outstream, "    %s=%d\n", VECTOR(buf), VECTOR(boolv)[0] ? 1 : 0));
249
4.52k
                    IGRAPH_WARNING("A boolean vertex attribute was converted to numeric.");
250
4.52k
                } else {
251
0
                    IGRAPH_WARNING("A non-numeric, non-string, non-boolean vertex attribute was ignored.");
252
0
                }
253
51.5k
            }
254
32.7k
            CHECK(fprintf(outstream, "  ];\n"));
255
32.7k
        }
256
3.50k
    } else {
257
243
        for (igraph_int_t i = 0; i < no_of_nodes; i++) {
258
230
            CHECK(fprintf(outstream, "  %" IGRAPH_PRId ";\n", i));
259
230
        }
260
13
    }
261
3.51k
    CHECK(fprintf(outstream, "\n"));
262
263
    /* Write the edges */
264
3.51k
    if (igraph_vector_int_size(&etypes) > 0) {
265
161k
        for (igraph_int_t i = 0; i < no_of_edges; i++) {
266
159k
            igraph_int_t from = IGRAPH_FROM(graph, i);
267
159k
            igraph_int_t to = IGRAPH_TO(graph, i);
268
159k
            CHECK(fprintf(outstream, "  %" IGRAPH_PRId " %s %" IGRAPH_PRId " [\n", from, edgeop, to));
269
533k
            for (igraph_int_t j = 0; j < igraph_vector_int_size(&etypes); j++) {
270
373k
                const char *name;
271
373k
                name = igraph_strvector_get(&enames, j);
272
373k
                IGRAPH_CHECK(dot_escape(name, &buf));
273
373k
                if (VECTOR(etypes)[j] == IGRAPH_ATTRIBUTE_NUMERIC) {
274
71.2k
                    IGRAPH_CHECK(igraph_i_attribute_get_numeric_edge_attr(graph,
275
71.2k
                                 name, igraph_ess_1(i), &numv));
276
71.2k
                    CHECK(fprintf(outstream, "    %s=", VECTOR(buf)));
277
71.2k
                    IGRAPH_CHECK(fprint_integral_or_precise(outstream, VECTOR(numv)[0], &buf));
278
71.2k
                    CHECK(fputc('\n', outstream));
279
302k
                } else if (VECTOR(etypes)[j] == IGRAPH_ATTRIBUTE_STRING) {
280
296k
                    const char *s;
281
296k
                    IGRAPH_CHECK(igraph_i_attribute_get_string_edge_attr(graph,
282
296k
                                 name, igraph_ess_1(i), &strv));
283
296k
                    s = igraph_strvector_get(&strv, 0);
284
296k
                    IGRAPH_CHECK(dot_escape(s, &buf2));
285
296k
                    CHECK(fprintf(outstream, "    %s=%s\n", VECTOR(buf), VECTOR(buf2)));
286
296k
                } else if (VECTOR(etypes)[j] == IGRAPH_ATTRIBUTE_BOOLEAN) {
287
5.77k
                    IGRAPH_CHECK(igraph_i_attribute_get_bool_edge_attr(graph,
288
5.77k
                                 name, igraph_ess_1(i), &boolv));
289
5.77k
                    CHECK(fprintf(outstream, "    %s=%d\n", VECTOR(buf), VECTOR(boolv)[0] ? 1 : 0));
290
5.77k
                    IGRAPH_WARNING("A boolean edge attribute was converted to numeric.");
291
5.77k
                } else {
292
0
                    IGRAPH_WARNING("A non-numeric, non-string graph attribute ignored.");
293
0
                }
294
373k
            }
295
159k
            CHECK(fprintf(outstream, "  ];\n"));
296
159k
        }
297
1.83k
    } else {
298
111k
        for (igraph_int_t i = 0; i < no_of_edges; i++) {
299
109k
            igraph_int_t from = IGRAPH_FROM(graph, i);
300
109k
            igraph_int_t to = IGRAPH_TO(graph, i);
301
109k
            CHECK(fprintf(outstream, "  %" IGRAPH_PRId " %s %" IGRAPH_PRId ";\n", from, edgeop, to));
302
109k
        }
303
1.83k
    }
304
3.51k
    CHECK(fprintf(outstream, "}\n"));
305
306
3.51k
    igraph_vector_char_destroy(&buf2);
307
3.51k
    igraph_vector_char_destroy(&buf);
308
3.51k
    igraph_vector_bool_destroy(&boolv);
309
3.51k
    igraph_strvector_destroy(&strv);
310
3.51k
    igraph_vector_destroy(&numv);
311
3.51k
    igraph_vector_int_destroy(&etypes);
312
3.51k
    igraph_vector_int_destroy(&vtypes);
313
3.51k
    igraph_vector_int_destroy(&gtypes);
314
3.51k
    igraph_strvector_destroy(&enames);
315
3.51k
    igraph_strvector_destroy(&vnames);
316
3.51k
    igraph_strvector_destroy(&gnames);
317
3.51k
    IGRAPH_FINALLY_CLEAN(11);
318
319
3.51k
    return IGRAPH_SUCCESS;
320
3.51k
}
321
322
#undef CHECK