| 1 | /* $Id$ $Revision$ */ |
| 2 | /* vim:set shiftwidth=4 ts=8: */ |
| 3 | |
| 4 | /************************************************************************* |
| 5 | * Copyright (c) 2011 AT&T Intellectual Property |
| 6 | * All rights reserved. This program and the accompanying materials |
| 7 | * are made available under the terms of the Eclipse Public License v1.0 |
| 8 | * which accompanies this distribution, and is available at |
| 9 | * http://www.eclipse.org/legal/epl-v10.html |
| 10 | * |
| 11 | * Contributors: See CVS logs. Details at http://www.graphviz.org/ |
| 12 | *************************************************************************/ |
| 13 | |
| 14 | /* Comments on the SVG coordinate system (SN 8 Dec 2006): |
| 15 | The initial <svg> element defines the SVG coordinate system so |
| 16 | that the graphviz canvas (in units of points) fits the intended |
| 17 | absolute size in inches. After this, the situation should be |
| 18 | that "px" = "pt" in SVG, so we can dispense with stating units. |
| 19 | Also, the input units (such as fontsize) should be preserved |
| 20 | without scaling in the output SVG (as long as the graph size |
| 21 | was not constrained.) |
| 22 | */ |
| 23 | |
| 24 | #include "config.h" |
| 25 | |
| 26 | #include <stdarg.h> |
| 27 | #include <stdlib.h> |
| 28 | #include <string.h> |
| 29 | #include <ctype.h> |
| 30 | |
| 31 | #include "macros.h" |
| 32 | #include "const.h" |
| 33 | |
| 34 | #include "gvplugin_render.h" |
| 35 | #include "agxbuf.h" |
| 36 | #include "utils.h" |
| 37 | #include "gvplugin_device.h" |
| 38 | #include "gvio.h" |
| 39 | #include "gvcint.h" |
| 40 | |
| 41 | typedef enum { FORMAT_SVG, FORMAT_SVGZ, } format_type; |
| 42 | |
| 43 | /* SVG dash array */ |
| 44 | static char *sdasharray = "5,2" ; |
| 45 | /* SVG dot array */ |
| 46 | static char *sdotarray = "1,5" ; |
| 47 | |
| 48 | #ifndef HAVE_STRCASECMP |
| 49 | extern int strcasecmp(const char *s1, const char *s2); |
| 50 | #endif |
| 51 | |
| 52 | static void svg_bzptarray(GVJ_t * job, pointf * A, int n) |
| 53 | { |
| 54 | int i; |
| 55 | char c; |
| 56 | |
| 57 | c = 'M'; /* first point */ |
| 58 | #if EDGEALIGN |
| 59 | if (A[0].x <= A[n-1].x) { |
| 60 | #endif |
| 61 | for (i = 0; i < n; i++) { |
| 62 | gvprintf(job, "%c" , c); |
| 63 | gvprintdouble(job, A[i].x); |
| 64 | gvputs(job, "," ); |
| 65 | gvprintdouble(job, -A[i].y); |
| 66 | if (i == 0) |
| 67 | c = 'C'; /* second point */ |
| 68 | else |
| 69 | c = ' '; /* remaining points */ |
| 70 | } |
| 71 | #if EDGEALIGN |
| 72 | } else { |
| 73 | for (i = n-1; i >= 0; i--) { |
| 74 | gvprintf(job, "%c" , c); |
| 75 | gvprintdouble(job, A[i].x); |
| 76 | gvputs(job, "," ); |
| 77 | gvprintdouble(job, -A[i].y); |
| 78 | if (i == 0) |
| 79 | c = 'C'; /* second point */ |
| 80 | else |
| 81 | c = ' '; /* remaining points */ |
| 82 | } |
| 83 | } |
| 84 | #endif |
| 85 | } |
| 86 | |
| 87 | static void svg_print_id_class(GVJ_t * job, char* id, char* idx, char* kind, void* obj) |
| 88 | { |
| 89 | char* str; |
| 90 | |
| 91 | gvputs(job, "<g id=\"" ); |
| 92 | gvputs(job, xml_string(id)); |
| 93 | if (idx) |
| 94 | gvprintf (job, "_%s" , xml_string(idx)); |
| 95 | gvprintf(job, "\" class=\"%s" , kind); |
| 96 | if ((str = agget(obj, "class" )) && *str) { |
| 97 | gvputs(job, " " ); |
| 98 | gvputs(job, xml_string(str)); |
| 99 | } |
| 100 | gvputs(job, "\"" ); |
| 101 | } |
| 102 | |
| 103 | static void svg_print_color(GVJ_t * job, gvcolor_t color) |
| 104 | { |
| 105 | switch (color.type) { |
| 106 | case COLOR_STRING: |
| 107 | gvputs(job, color.u.string); |
| 108 | break; |
| 109 | case RGBA_BYTE: |
| 110 | if (color.u.rgba[3] == 0) /* transparent */ |
| 111 | gvputs(job, "transparent" ); |
| 112 | else |
| 113 | gvprintf(job, "#%02x%02x%02x" , |
| 114 | color.u.rgba[0], color.u.rgba[1], color.u.rgba[2]); |
| 115 | break; |
| 116 | default: |
| 117 | assert(0); /* internal error */ |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | static void svg_grstyle(GVJ_t * job, int filled, int gid) |
| 122 | { |
| 123 | obj_state_t *obj = job->obj; |
| 124 | |
| 125 | gvputs(job, " fill=\"" ); |
| 126 | if (filled == GRADIENT) { |
| 127 | gvprintf(job, "url(#l_%d)" , gid); |
| 128 | } else if (filled == RGRADIENT) { |
| 129 | gvprintf(job, "url(#r_%d)" , gid); |
| 130 | } else if (filled) { |
| 131 | svg_print_color(job, obj->fillcolor); |
| 132 | if (obj->fillcolor.type == RGBA_BYTE |
| 133 | && obj->fillcolor.u.rgba[3] > 0 |
| 134 | && obj->fillcolor.u.rgba[3] < 255) |
| 135 | gvprintf(job, "\" fill-opacity=\"%f" , |
| 136 | ((float) obj->fillcolor.u.rgba[3] / 255.0)); |
| 137 | } else { |
| 138 | gvputs(job, "none" ); |
| 139 | } |
| 140 | gvputs(job, "\" stroke=\"" ); |
| 141 | svg_print_color(job, obj->pencolor); |
| 142 | if (obj->penwidth != PENWIDTH_NORMAL) { |
| 143 | gvputs(job, "\" stroke-width=\"" ); |
| 144 | gvprintdouble(job, obj->penwidth); |
| 145 | } |
| 146 | if (obj->pen == PEN_DASHED) { |
| 147 | gvprintf(job, "\" stroke-dasharray=\"%s" , sdasharray); |
| 148 | } else if (obj->pen == PEN_DOTTED) { |
| 149 | gvprintf(job, "\" stroke-dasharray=\"%s" , sdotarray); |
| 150 | } |
| 151 | if (obj->pencolor.type == RGBA_BYTE && obj->pencolor.u.rgba[3] > 0 |
| 152 | && obj->pencolor.u.rgba[3] < 255) |
| 153 | gvprintf(job, "\" stroke-opacity=\"%f" , |
| 154 | ((float) obj->pencolor.u.rgba[3] / 255.0)); |
| 155 | |
| 156 | gvputs(job, "\"" ); |
| 157 | } |
| 158 | |
| 159 | static void (GVJ_t * job, char *str) |
| 160 | { |
| 161 | gvputs(job, "<!-- " ); |
| 162 | gvputs(job, xml_string(str)); |
| 163 | gvputs(job, " -->\n" ); |
| 164 | } |
| 165 | |
| 166 | static void svg_begin_job(GVJ_t * job) |
| 167 | { |
| 168 | char *s; |
| 169 | gvputs(job, |
| 170 | "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" ); |
| 171 | if ((s = agget(job->gvc->g, "stylesheet" )) && s[0]) { |
| 172 | gvputs(job, "<?xml-stylesheet href=\"" ); |
| 173 | gvputs(job, s); |
| 174 | gvputs(job, "\" type=\"text/css\"?>\n" ); |
| 175 | } |
| 176 | #if 0 |
| 177 | gvputs(job, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\n" ); |
| 178 | gvputs(job, |
| 179 | " \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\"" ); |
| 180 | /* This is to work around a bug in the SVG 1.0 DTD */ |
| 181 | gvputs(job, |
| 182 | " [\n <!ATTLIST svg xmlns:xlink CDATA #FIXED \"http://www.w3.org/1999/xlink\">\n]" ); |
| 183 | #else |
| 184 | gvputs(job, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n" ); |
| 185 | gvputs(job, |
| 186 | " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n" ); |
| 187 | #endif |
| 188 | |
| 189 | gvputs(job, "<!-- Generated by " ); |
| 190 | gvputs(job, xml_string(job->common->info[0])); |
| 191 | gvputs(job, " version " ); |
| 192 | gvputs(job, xml_string(job->common->info[1])); |
| 193 | gvputs(job, " (" ); |
| 194 | gvputs(job, xml_string(job->common->info[2])); |
| 195 | gvputs(job, ")\n" ); |
| 196 | gvputs(job, " -->\n" ); |
| 197 | } |
| 198 | |
| 199 | static void svg_begin_graph(GVJ_t * job) |
| 200 | { |
| 201 | obj_state_t *obj = job->obj; |
| 202 | |
| 203 | gvputs(job, "<!--" ); |
| 204 | if (agnameof(obj->u.g)[0]) { |
| 205 | gvputs(job, " Title: " ); |
| 206 | gvputs(job, xml_string(agnameof(obj->u.g))); |
| 207 | } |
| 208 | gvprintf(job, " Pages: %d -->\n" , |
| 209 | job->pagesArraySize.x * job->pagesArraySize.y); |
| 210 | |
| 211 | gvprintf(job, "<svg width=\"%dpt\" height=\"%dpt\"\n" , |
| 212 | job->width, job->height); |
| 213 | gvprintf(job, " viewBox=\"%.2f %.2f %.2f %.2f\"" , |
| 214 | job->canvasBox.LL.x, |
| 215 | job->canvasBox.LL.y, |
| 216 | job->canvasBox.UR.x, |
| 217 | job->canvasBox.UR.y); |
| 218 | /* namespace of svg */ |
| 219 | gvputs(job, " xmlns=\"http://www.w3.org/2000/svg\"" ); |
| 220 | /* namespace of xlink */ |
| 221 | gvputs(job, " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" ); |
| 222 | gvputs(job, ">\n" ); |
| 223 | } |
| 224 | |
| 225 | static void svg_end_graph(GVJ_t * job) |
| 226 | { |
| 227 | gvputs(job, "</svg>\n" ); |
| 228 | } |
| 229 | |
| 230 | static void svg_begin_layer(GVJ_t * job, char *layername, int layerNum, |
| 231 | int numLayers) |
| 232 | { |
| 233 | obj_state_t *obj = job->obj; |
| 234 | |
| 235 | svg_print_id_class(job, layername, NULL, "layer" , obj->u.g); |
| 236 | gvputs(job, ">\n" ); |
| 237 | } |
| 238 | |
| 239 | static void svg_end_layer(GVJ_t * job) |
| 240 | { |
| 241 | gvputs(job, "</g>\n" ); |
| 242 | } |
| 243 | |
| 244 | /* svg_begin_page: |
| 245 | * Currently, svg output does not support pages. |
| 246 | * FIX: If implemented, we must guarantee the id is unique. |
| 247 | */ |
| 248 | static void svg_begin_page(GVJ_t * job) |
| 249 | { |
| 250 | obj_state_t *obj = job->obj; |
| 251 | |
| 252 | /* its really just a page of the graph, but its still a graph, |
| 253 | * and it is the entire graph if we're not currently paging */ |
| 254 | svg_print_id_class(job, obj->id, NULL, "graph" , obj->u.g); |
| 255 | gvputs(job, " transform=\"scale(" ); |
| 256 | gvprintdouble(job, job->scale.x); |
| 257 | gvputs(job, " " ); |
| 258 | gvprintdouble(job, job->scale.y); |
| 259 | gvprintf(job, ") rotate(%d) translate(" , -job->rotation); |
| 260 | gvprintdouble(job, job->translation.x); |
| 261 | gvputs(job, " " ); |
| 262 | gvprintdouble(job, -job->translation.y); |
| 263 | gvputs(job, ")\">\n" ); |
| 264 | /* default style */ |
| 265 | if (agnameof(obj->u.g)[0]) { |
| 266 | gvputs(job, "<title>" ); |
| 267 | gvputs(job, xml_string(agnameof(obj->u.g))); |
| 268 | gvputs(job, "</title>\n" ); |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | static void svg_end_page(GVJ_t * job) |
| 273 | { |
| 274 | gvputs(job, "</g>\n" ); |
| 275 | } |
| 276 | |
| 277 | static void svg_begin_cluster(GVJ_t * job) |
| 278 | { |
| 279 | obj_state_t *obj = job->obj; |
| 280 | |
| 281 | svg_print_id_class(job, obj->id, NULL, "cluster" , obj->u.sg); |
| 282 | gvputs(job, ">\n" ); |
| 283 | gvputs(job, "<title>" ); |
| 284 | gvputs(job, xml_string(agnameof(obj->u.g))); |
| 285 | gvputs(job, "</title>\n" ); |
| 286 | } |
| 287 | |
| 288 | static void svg_end_cluster(GVJ_t * job) |
| 289 | { |
| 290 | gvputs(job, "</g>\n" ); |
| 291 | } |
| 292 | |
| 293 | static void svg_begin_node(GVJ_t * job) |
| 294 | { |
| 295 | obj_state_t *obj = job->obj; |
| 296 | char* idx; |
| 297 | |
| 298 | if (job->layerNum > 1) |
| 299 | idx = job->gvc->layerIDs[job->layerNum]; |
| 300 | else |
| 301 | idx = NULL; |
| 302 | svg_print_id_class(job, obj->id, idx, "node" , obj->u.n); |
| 303 | gvputs(job, ">\n" ); |
| 304 | gvputs(job, "<title>" ); |
| 305 | gvputs(job, xml_string(agnameof(obj->u.n))); |
| 306 | gvputs(job, "</title>\n" ); |
| 307 | } |
| 308 | |
| 309 | static void svg_end_node(GVJ_t * job) |
| 310 | { |
| 311 | gvputs(job, "</g>\n" ); |
| 312 | } |
| 313 | |
| 314 | static void svg_begin_edge(GVJ_t * job) |
| 315 | { |
| 316 | obj_state_t *obj = job->obj; |
| 317 | char *ename; |
| 318 | |
| 319 | svg_print_id_class(job, obj->id, NULL, "edge" , obj->u.e); |
| 320 | gvputs(job, ">\n" ); |
| 321 | |
| 322 | gvputs(job, "<title>" ); |
| 323 | ename = strdup_and_subst_obj("\\E" , (void *) (obj->u.e)); |
| 324 | gvputs(job, xml_string(ename)); |
| 325 | free(ename); |
| 326 | gvputs(job, "</title>\n" ); |
| 327 | } |
| 328 | |
| 329 | static void svg_end_edge(GVJ_t * job) |
| 330 | { |
| 331 | gvputs(job, "</g>\n" ); |
| 332 | } |
| 333 | |
| 334 | static void |
| 335 | svg_begin_anchor(GVJ_t * job, char *href, char *tooltip, char *target, |
| 336 | char *id) |
| 337 | { |
| 338 | gvputs(job, "<g" ); |
| 339 | if (id) { |
| 340 | gvputs(job, " id=\"a_" ); |
| 341 | gvputs(job, xml_string(id)); |
| 342 | gvputs(job, "\"" ); |
| 343 | } |
| 344 | gvputs(job, ">" ); |
| 345 | |
| 346 | gvputs(job, "<a" ); |
| 347 | #if 0 |
| 348 | /* the svg spec implies this can be omitted: http://www.w3.org/TR/SVG/linking.html#Links */ |
| 349 | gvputs(job, " xlink:type=\"simple\"" ); |
| 350 | #endif |
| 351 | if (href && href[0]) { |
| 352 | gvputs(job, " xlink:href=\"" ); |
| 353 | gvputs(job, href); |
| 354 | gvputs(job, "\"" ); |
| 355 | } |
| 356 | #if 0 |
| 357 | /* linking to itself, just so that it can have a xlink:link in the anchor, seems wrong. |
| 358 | * it changes the behavior in browsers, the link apears in the bottom information bar |
| 359 | */ |
| 360 | else { |
| 361 | assert(id && id[0]); /* there should always be an id available */ |
| 362 | gvputs(job, " xlink:href=\"#" ); |
| 363 | gvputs(job, xml_url_string(href)); |
| 364 | gvputs(job, "\"" ); |
| 365 | } |
| 366 | #endif |
| 367 | if (tooltip && tooltip[0]) { |
| 368 | gvputs(job, " xlink:title=\"" ); |
| 369 | gvputs(job, xml_string0(tooltip, 1)); |
| 370 | gvputs(job, "\"" ); |
| 371 | } |
| 372 | if (target && target[0]) { |
| 373 | gvputs(job, " target=\"" ); |
| 374 | gvputs(job, xml_string(target)); |
| 375 | gvputs(job, "\"" ); |
| 376 | } |
| 377 | gvputs(job, ">\n" ); |
| 378 | } |
| 379 | |
| 380 | static void svg_end_anchor(GVJ_t * job) |
| 381 | { |
| 382 | gvputs(job, "</a>\n" ); |
| 383 | gvputs(job, "</g>\n" ); |
| 384 | } |
| 385 | |
| 386 | static void svg_textspan(GVJ_t * job, pointf p, textspan_t * span) |
| 387 | { |
| 388 | obj_state_t *obj = job->obj; |
| 389 | PostscriptAlias *pA; |
| 390 | char *family = NULL, *weight = NULL, *stretch = NULL, *style = NULL; |
| 391 | unsigned int flags; |
| 392 | |
| 393 | gvputs(job, "<text" ); |
| 394 | switch (span->just) { |
| 395 | case 'l': |
| 396 | gvputs(job, " text-anchor=\"start\"" ); |
| 397 | break; |
| 398 | case 'r': |
| 399 | gvputs(job, " text-anchor=\"end\"" ); |
| 400 | break; |
| 401 | default: |
| 402 | case 'n': |
| 403 | gvputs(job, " text-anchor=\"middle\"" ); |
| 404 | break; |
| 405 | } |
| 406 | p.y += span->yoffset_centerline; |
| 407 | if (!obj->labeledgealigned) { |
| 408 | gvputs(job, " x=\"" ); |
| 409 | gvprintdouble(job, p.x); |
| 410 | gvputs(job, "\" y=\"" ); |
| 411 | gvprintdouble(job, -p.y); |
| 412 | gvputs(job, "\"" ); |
| 413 | } |
| 414 | pA = span->font->postscript_alias; |
| 415 | if (pA) { |
| 416 | switch (GD_fontnames(job->gvc->g)) { |
| 417 | case PSFONTS: |
| 418 | family = pA->name; |
| 419 | weight = pA->weight; |
| 420 | style = pA->style; |
| 421 | break; |
| 422 | case SVGFONTS: |
| 423 | family = pA->svg_font_family; |
| 424 | weight = pA->svg_font_weight; |
| 425 | style = pA->svg_font_style; |
| 426 | break; |
| 427 | default: |
| 428 | case NATIVEFONTS: |
| 429 | family = pA->family; |
| 430 | weight = pA->weight; |
| 431 | style = pA->style; |
| 432 | break; |
| 433 | } |
| 434 | stretch = pA->stretch; |
| 435 | |
| 436 | gvprintf(job, " font-family=\"%s" , family); |
| 437 | if (pA->svg_font_family) |
| 438 | gvprintf(job, ",%s" , pA->svg_font_family); |
| 439 | gvputs(job, "\"" ); |
| 440 | if (weight) |
| 441 | gvprintf(job, " font-weight=\"%s\"" , weight); |
| 442 | if (stretch) |
| 443 | gvprintf(job, " font-stretch=\"%s\"" , stretch); |
| 444 | if (style) |
| 445 | gvprintf(job, " font-style=\"%s\"" , style); |
| 446 | } else |
| 447 | gvprintf(job, " font-family=\"%s\"" , span->font->name); |
| 448 | if ((span->font) && (flags = span->font->flags)) { |
| 449 | if ((flags & HTML_BF) && !weight) |
| 450 | gvprintf(job, " font-weight=\"bold\"" ); |
| 451 | if ((flags & HTML_IF) && !style) |
| 452 | gvprintf(job, " font-style=\"italic\"" ); |
| 453 | if ((flags & (HTML_UL|HTML_S|HTML_OL))) { |
| 454 | int comma = 0; |
| 455 | gvprintf(job, " text-decoration=\"" ); |
| 456 | if ((flags & HTML_UL)) { |
| 457 | gvprintf(job, "underline" ); |
| 458 | comma = 1; |
| 459 | } |
| 460 | if ((flags & HTML_OL)) { |
| 461 | gvprintf(job, "%soverline" , (comma?"," :"" )); |
| 462 | comma = 1; |
| 463 | } |
| 464 | if ((flags & HTML_S)) |
| 465 | gvprintf(job, "%sline-through" , (comma?"," :"" )); |
| 466 | gvprintf(job, "\"" ); |
| 467 | } |
| 468 | if ((flags & HTML_SUP)) |
| 469 | gvprintf(job, " baseline-shift=\"super\"" ); |
| 470 | if ((flags & HTML_SUB)) |
| 471 | gvprintf(job, " baseline-shift=\"sub\"" ); |
| 472 | } |
| 473 | |
| 474 | gvprintf(job, " font-size=\"%.2f\"" , span->font->size); |
| 475 | switch (obj->pencolor.type) { |
| 476 | case COLOR_STRING: |
| 477 | if (strcasecmp(obj->pencolor.u.string, "black" )) |
| 478 | gvprintf(job, " fill=\"%s\"" , obj->pencolor.u.string); |
| 479 | break; |
| 480 | case RGBA_BYTE: |
| 481 | gvprintf(job, " fill=\"#%02x%02x%02x\"" , |
| 482 | obj->pencolor.u.rgba[0], obj->pencolor.u.rgba[1], |
| 483 | obj->pencolor.u.rgba[2]); |
| 484 | if (obj->pencolor.u.rgba[3] > 0 && obj->pencolor.u.rgba[3] < 255) |
| 485 | gvprintf(job, " fill-opacity=\"%f\"" , ((float) obj->pencolor.u.rgba[3] / 255.0)); |
| 486 | break; |
| 487 | default: |
| 488 | assert(0); /* internal error */ |
| 489 | } |
| 490 | gvputs(job, ">" ); |
| 491 | if (obj->labeledgealigned) { |
| 492 | gvprintf(job, "<textPath xlink:href=\"#%s_p\" startOffset=\"50%%\">" , xml_string(obj->id)); |
| 493 | gvputs(job, "<tspan x=\"0\" dy=\"" ); |
| 494 | gvprintdouble(job, -p.y); |
| 495 | gvputs(job, "\">" ); |
| 496 | } |
| 497 | gvputs(job, xml_string0(span->str, TRUE)); |
| 498 | if (obj->labeledgealigned) |
| 499 | gvprintf (job, "</tspan></textPath>" ); |
| 500 | gvputs(job, "</text>\n" ); |
| 501 | } |
| 502 | |
| 503 | /* svg_gradstyle |
| 504 | * Outputs the SVG statements that define the gradient pattern |
| 505 | */ |
| 506 | static int svg_gradstyle(GVJ_t * job, pointf * A, int n) |
| 507 | { |
| 508 | pointf G[2]; |
| 509 | float angle; |
| 510 | static int gradId; |
| 511 | int id = gradId++; |
| 512 | |
| 513 | obj_state_t *obj = job->obj; |
| 514 | angle = obj->gradient_angle * M_PI / 180; //angle of gradient line |
| 515 | G[0].x = G[0].y = G[1].x = G[1].y = 0.; |
| 516 | get_gradient_points(A, G, n, angle, 0); //get points on gradient line |
| 517 | |
| 518 | gvprintf(job, |
| 519 | "<defs>\n<linearGradient id=\"l_%d\" gradientUnits=\"userSpaceOnUse\" " , id); |
| 520 | gvputs(job, "x1=\"" ); |
| 521 | gvprintdouble(job, G[0].x); |
| 522 | gvputs(job, "\" y1=\"" ); |
| 523 | gvprintdouble(job, G[0].y); |
| 524 | gvputs(job, "\" x2=\"" ); |
| 525 | gvprintdouble(job, G[1].x); |
| 526 | gvputs(job, "\" y2=\"" ); |
| 527 | gvprintdouble(job, G[1].y); |
| 528 | gvputs(job, "\" >\n" ); |
| 529 | if (obj->gradient_frac > 0) |
| 530 | gvprintf(job, "<stop offset=\"%.03f\" style=\"stop-color:" , obj->gradient_frac - 0.001); |
| 531 | else |
| 532 | gvputs(job, "<stop offset=\"0\" style=\"stop-color:" ); |
| 533 | svg_print_color(job, obj->fillcolor); |
| 534 | gvputs(job, ";stop-opacity:" ); |
| 535 | if (obj->fillcolor.type == RGBA_BYTE && obj->fillcolor.u.rgba[3] > 0 |
| 536 | && obj->fillcolor.u.rgba[3] < 255) |
| 537 | gvprintf(job, "%f" , ((float) obj->fillcolor.u.rgba[3] / 255.0)); |
| 538 | else |
| 539 | gvputs(job, "1." ); |
| 540 | gvputs(job, ";\"/>\n" ); |
| 541 | if (obj->gradient_frac > 0) |
| 542 | gvprintf(job, "<stop offset=\"%.03f\" style=\"stop-color:" , obj->gradient_frac); |
| 543 | else |
| 544 | gvputs(job, "<stop offset=\"1\" style=\"stop-color:" ); |
| 545 | svg_print_color(job, obj->stopcolor); |
| 546 | gvputs(job, ";stop-opacity:" ); |
| 547 | if (obj->stopcolor.type == RGBA_BYTE && obj->stopcolor.u.rgba[3] > 0 |
| 548 | && obj->stopcolor.u.rgba[3] < 255) |
| 549 | gvprintf(job, "%f" , ((float) obj->stopcolor.u.rgba[3] / 255.0)); |
| 550 | else |
| 551 | gvputs(job, "1." ); |
| 552 | gvputs(job, ";\"/>\n</linearGradient>\n</defs>\n" ); |
| 553 | return id; |
| 554 | } |
| 555 | |
| 556 | /* svg_rgradstyle |
| 557 | * Outputs the SVG statements that define the radial gradient pattern |
| 558 | */ |
| 559 | static int svg_rgradstyle(GVJ_t * job, pointf * A, int n) |
| 560 | { |
| 561 | /* pointf G[2]; */ |
| 562 | float angle; |
| 563 | int ifx, ify; |
| 564 | static int rgradId; |
| 565 | int id = rgradId++; |
| 566 | |
| 567 | obj_state_t *obj = job->obj; |
| 568 | angle = obj->gradient_angle * M_PI / 180; //angle of gradient line |
| 569 | /* G[0].x = G[0].y = G[1].x = G[1].y; */ |
| 570 | /* get_gradient_points(A, G, n, 0, 1); */ |
| 571 | if (angle == 0.) { |
| 572 | ifx = ify = 50; |
| 573 | } else { |
| 574 | ifx = 50 * (1 + cos(angle)); |
| 575 | ify = 50 * (1 - sin(angle)); |
| 576 | } |
| 577 | gvprintf(job, |
| 578 | "<defs>\n<radialGradient id=\"r_%d\" cx=\"50%%\" cy=\"50%%\" r=\"75%%\" fx=\"%d%%\" fy=\"%d%%\">\n" , |
| 579 | id, ifx, ify); |
| 580 | gvputs(job, "<stop offset=\"0\" style=\"stop-color:" ); |
| 581 | svg_print_color(job, obj->fillcolor); |
| 582 | gvputs(job, ";stop-opacity:" ); |
| 583 | if (obj->fillcolor.type == RGBA_BYTE && obj->fillcolor.u.rgba[3] > 0 |
| 584 | && obj->fillcolor.u.rgba[3] < 255) |
| 585 | gvprintf(job, "%f" , ((float) obj->fillcolor.u.rgba[3] / 255.0)); |
| 586 | else |
| 587 | gvputs(job, "1." ); |
| 588 | gvputs(job, ";\"/>\n" ); |
| 589 | gvputs(job, "<stop offset=\"1\" style=\"stop-color:" ); |
| 590 | svg_print_color(job, obj->stopcolor); |
| 591 | gvputs(job, ";stop-opacity:" ); |
| 592 | if (obj->stopcolor.type == RGBA_BYTE && obj->stopcolor.u.rgba[3] > 0 |
| 593 | && obj->stopcolor.u.rgba[3] < 255) |
| 594 | gvprintf(job, "%f" , ((float) obj->stopcolor.u.rgba[3] / 255.0)); |
| 595 | else |
| 596 | gvputs(job, "1." ); |
| 597 | gvputs(job, ";\"/>\n</radialGradient>\n</defs>\n" ); |
| 598 | return id; |
| 599 | } |
| 600 | |
| 601 | |
| 602 | static void svg_ellipse(GVJ_t * job, pointf * A, int filled) |
| 603 | { |
| 604 | int gid = 0; |
| 605 | |
| 606 | /* A[] contains 2 points: the center and corner. */ |
| 607 | if (filled == GRADIENT) { |
| 608 | gid = svg_gradstyle(job, A, 2); |
| 609 | } else if (filled == (RGRADIENT)) { |
| 610 | gid = svg_rgradstyle(job, A, 2); |
| 611 | } |
| 612 | gvputs(job, "<ellipse" ); |
| 613 | svg_grstyle(job, filled, gid); |
| 614 | gvputs(job, " cx=\"" ); |
| 615 | gvprintdouble(job, A[0].x); |
| 616 | gvputs(job, "\" cy=\"" ); |
| 617 | gvprintdouble(job, -A[0].y); |
| 618 | gvputs(job, "\" rx=\"" ); |
| 619 | gvprintdouble(job, A[1].x - A[0].x); |
| 620 | gvputs(job, "\" ry=\"" ); |
| 621 | gvprintdouble(job, A[1].y - A[0].y); |
| 622 | gvputs(job, "\"/>\n" ); |
| 623 | } |
| 624 | |
| 625 | static void |
| 626 | svg_bezier(GVJ_t * job, pointf * A, int n, int arrow_at_start, |
| 627 | int arrow_at_end, int filled) |
| 628 | { |
| 629 | int gid = 0; |
| 630 | obj_state_t *obj = job->obj; |
| 631 | |
| 632 | if (filled == GRADIENT) { |
| 633 | gid = svg_gradstyle(job, A, n); |
| 634 | } else if (filled == (RGRADIENT)) { |
| 635 | gid = svg_rgradstyle(job, A, n); |
| 636 | } |
| 637 | gvputs(job, "<path" ); |
| 638 | if (obj->labeledgealigned) { |
| 639 | gvputs(job, " id=\"" ); |
| 640 | gvputs(job, xml_string(obj->id)); |
| 641 | gvputs(job, "_p\" " ); |
| 642 | } |
| 643 | svg_grstyle(job, filled, gid); |
| 644 | gvputs(job, " d=\"" ); |
| 645 | svg_bzptarray(job, A, n); |
| 646 | gvputs(job, "\"/>\n" ); |
| 647 | } |
| 648 | |
| 649 | static void svg_polygon(GVJ_t * job, pointf * A, int n, int filled) |
| 650 | { |
| 651 | int i, gid = 0; |
| 652 | if (filled == GRADIENT) { |
| 653 | gid = svg_gradstyle(job, A, n); |
| 654 | } else if (filled == (RGRADIENT)) { |
| 655 | gid = svg_rgradstyle(job, A, n); |
| 656 | } |
| 657 | gvputs(job, "<polygon" ); |
| 658 | svg_grstyle(job, filled, gid); |
| 659 | gvputs(job, " points=\"" ); |
| 660 | for (i = 0; i < n; i++) { |
| 661 | gvprintdouble(job, A[i].x); |
| 662 | gvputs(job, "," ); |
| 663 | gvprintdouble(job, -A[i].y); |
| 664 | gvputs(job, " " ); |
| 665 | } |
| 666 | /* repeat the first point because Adobe SVG is broken */ |
| 667 | gvprintdouble(job, A[0].x); |
| 668 | gvputs(job, "," ); |
| 669 | gvprintdouble(job, -A[0].y); |
| 670 | gvputs(job, "\"/>\n" ); |
| 671 | } |
| 672 | |
| 673 | static void svg_polyline(GVJ_t * job, pointf * A, int n) |
| 674 | { |
| 675 | int i; |
| 676 | |
| 677 | gvputs(job, "<polyline" ); |
| 678 | svg_grstyle(job, 0, 0); |
| 679 | gvputs(job, " points=\"" ); |
| 680 | for (i = 0; i < n; i++) { |
| 681 | gvprintdouble(job, A[i].x); |
| 682 | gvputs(job, "," ); |
| 683 | gvprintdouble(job, -A[i].y); |
| 684 | gvputs(job, " " ); |
| 685 | } |
| 686 | gvputs(job, "\"/>\n" ); |
| 687 | } |
| 688 | |
| 689 | /* color names from http://www.w3.org/TR/SVG/types.html */ |
| 690 | /* NB. List must be LANG_C sorted */ |
| 691 | static char *svg_knowncolors[] = { |
| 692 | "aliceblue" , "antiquewhite" , "aqua" , "aquamarine" , "azure" , |
| 693 | "beige" , "bisque" , "black" , "blanchedalmond" , "blue" , |
| 694 | "blueviolet" , "brown" , "burlywood" , |
| 695 | "cadetblue" , "chartreuse" , "chocolate" , "coral" , |
| 696 | "cornflowerblue" , "cornsilk" , "crimson" , "cyan" , |
| 697 | "darkblue" , "darkcyan" , "darkgoldenrod" , "darkgray" , |
| 698 | "darkgreen" , "darkgrey" , "darkkhaki" , "darkmagenta" , |
| 699 | "darkolivegreen" , "darkorange" , "darkorchid" , "darkred" , |
| 700 | "darksalmon" , "darkseagreen" , "darkslateblue" , "darkslategray" , |
| 701 | "darkslategrey" , "darkturquoise" , "darkviolet" , "deeppink" , |
| 702 | "deepskyblue" , "dimgray" , "dimgrey" , "dodgerblue" , |
| 703 | "firebrick" , "floralwhite" , "forestgreen" , "fuchsia" , |
| 704 | "gainsboro" , "ghostwhite" , "gold" , "goldenrod" , "gray" , |
| 705 | "green" , "greenyellow" , "grey" , |
| 706 | "honeydew" , "hotpink" , "indianred" , |
| 707 | "indigo" , "ivory" , "khaki" , |
| 708 | "lavender" , "lavenderblush" , "lawngreen" , "lemonchiffon" , |
| 709 | "lightblue" , "lightcoral" , "lightcyan" , "lightgoldenrodyellow" , |
| 710 | "lightgray" , "lightgreen" , "lightgrey" , "lightpink" , |
| 711 | "lightsalmon" , "lightseagreen" , "lightskyblue" , |
| 712 | "lightslategray" , "lightslategrey" , "lightsteelblue" , |
| 713 | "lightyellow" , "lime" , "limegreen" , "linen" , |
| 714 | "magenta" , "maroon" , "mediumaquamarine" , "mediumblue" , |
| 715 | "mediumorchid" , "mediumpurple" , "mediumseagreen" , |
| 716 | "mediumslateblue" , "mediumspringgreen" , "mediumturquoise" , |
| 717 | "mediumvioletred" , "midnightblue" , "mintcream" , |
| 718 | "mistyrose" , "moccasin" , |
| 719 | "navajowhite" , "navy" , "oldlace" , |
| 720 | "olive" , "olivedrab" , "orange" , "orangered" , "orchid" , |
| 721 | "palegoldenrod" , "palegreen" , "paleturquoise" , |
| 722 | "palevioletred" , "papayawhip" , "peachpuff" , "peru" , "pink" , |
| 723 | "plum" , "powderblue" , "purple" , |
| 724 | "red" , "rosybrown" , "royalblue" , |
| 725 | "saddlebrown" , "salmon" , "sandybrown" , "seagreen" , "seashell" , |
| 726 | "sienna" , "silver" , "skyblue" , "slateblue" , "slategray" , |
| 727 | "slategrey" , "snow" , "springgreen" , "steelblue" , |
| 728 | "tan" , "teal" , "thistle" , "tomato" , "turquoise" , |
| 729 | "violet" , |
| 730 | "wheat" , "white" , "whitesmoke" , |
| 731 | "yellow" , "yellowgreen" |
| 732 | }; |
| 733 | |
| 734 | gvrender_engine_t svg_engine = { |
| 735 | svg_begin_job, |
| 736 | 0, /* svg_end_job */ |
| 737 | svg_begin_graph, |
| 738 | svg_end_graph, |
| 739 | svg_begin_layer, |
| 740 | svg_end_layer, |
| 741 | svg_begin_page, |
| 742 | svg_end_page, |
| 743 | svg_begin_cluster, |
| 744 | svg_end_cluster, |
| 745 | 0, /* svg_begin_nodes */ |
| 746 | 0, /* svg_end_nodes */ |
| 747 | 0, /* svg_begin_edges */ |
| 748 | 0, /* svg_end_edges */ |
| 749 | svg_begin_node, |
| 750 | svg_end_node, |
| 751 | svg_begin_edge, |
| 752 | svg_end_edge, |
| 753 | svg_begin_anchor, |
| 754 | svg_end_anchor, |
| 755 | 0, /* svg_begin_anchor */ |
| 756 | 0, /* svg_end_anchor */ |
| 757 | svg_textspan, |
| 758 | 0, /* svg_resolve_color */ |
| 759 | svg_ellipse, |
| 760 | svg_polygon, |
| 761 | svg_bezier, |
| 762 | svg_polyline, |
| 763 | svg_comment, |
| 764 | 0, /* svg_library_shape */ |
| 765 | }; |
| 766 | |
| 767 | gvrender_features_t render_features_svg = { |
| 768 | GVRENDER_Y_GOES_DOWN | GVRENDER_DOES_TRANSFORM | GVRENDER_DOES_LABELS | GVRENDER_DOES_MAPS | GVRENDER_DOES_TARGETS | GVRENDER_DOES_TOOLTIPS, /* flags */ |
| 769 | 4., /* default pad - graph units */ |
| 770 | svg_knowncolors, /* knowncolors */ |
| 771 | sizeof(svg_knowncolors) / sizeof(char *), /* sizeof knowncolors */ |
| 772 | RGBA_BYTE, /* color_type */ |
| 773 | }; |
| 774 | |
| 775 | gvdevice_features_t device_features_svg = { |
| 776 | GVDEVICE_DOES_TRUECOLOR|GVDEVICE_DOES_LAYERS, /* flags */ |
| 777 | {0., 0.}, /* default margin - points */ |
| 778 | {0., 0.}, /* default page width, height - points */ |
| 779 | {72., 72.}, /* default dpi */ |
| 780 | }; |
| 781 | |
| 782 | gvdevice_features_t device_features_svgz = { |
| 783 | GVDEVICE_DOES_TRUECOLOR|GVDEVICE_DOES_LAYERS|GVDEVICE_BINARY_FORMAT|GVDEVICE_COMPRESSED_FORMAT, /* flags */ |
| 784 | {0., 0.}, /* default margin - points */ |
| 785 | {0., 0.}, /* default page width, height - points */ |
| 786 | {72., 72.}, /* default dpi */ |
| 787 | }; |
| 788 | |
| 789 | gvplugin_installed_t gvrender_svg_types[] = { |
| 790 | {FORMAT_SVG, "svg" , 1, &svg_engine, &render_features_svg}, |
| 791 | {0, NULL, 0, NULL, NULL} |
| 792 | }; |
| 793 | |
| 794 | gvplugin_installed_t gvdevice_svg_types[] = { |
| 795 | {FORMAT_SVG, "svg:svg" , 1, NULL, &device_features_svg}, |
| 796 | #if HAVE_LIBZ |
| 797 | {FORMAT_SVGZ, "svgz:svg" , 1, NULL, &device_features_svgz}, |
| 798 | #endif |
| 799 | {0, NULL, 0, NULL, NULL} |
| 800 | }; |
| 801 | |