/* This software may only be used by you under license from AT&T Corp. ("AT&T"). A copy of AT&T's Source Code Agreement is available at AT&T's Internet website having the URL: If you received this software without first entering into a license with AT&T, you have an infringing copy of this software and cannot use it without violating AT&T's intellectual property rights. */ #pragma prototyped /* this is a rough start at a working SVG driver. you can get image nodes by setting a node's [shape=webimage,shapefile="http://www.your.site.com/path/image.png"] (of course can also be a file: reference if you run locally. which causes a warning (FIX that) but the back end turns this into a custom shape. you can also set [ URL = "http://some.place.com/whatever" ] to get clickable nodes (and edges for that matter). some major areas needing work: 0. fonts including embedded font support. is SVG finished in this area? 1. styles, including dotted/dashed lines, also "style function" passthrough in SVG similar to what we have in postscript. 2. look at what happens in landscape mode, pagination? etc. 3. allow arbitrary user transforms via graph "style" attribute. 4. javascript hooks. particularly, look at this in the context of SVG animation for dynadag output (n.b. dynadag only in alpha release) 5. image node improvement, e.g. set clip path to node boundary, and support scaling with fixed aspect ratio (this feature seems to be broken in the current Adobe SVG plugin for Windows). 6. can we support arbitrary HTML for node contents? 7. encode abstract graph as interleaved XML in some sort of graph XML dialect? 8. accessibility features 9. embed directions about getting plugin, if not present in browser Stephen North north@research.att.com */ #include #include "render.h" #include "utils.h" #ifdef HAVE_LIBZ #include "zlib.h" #ifdef MSWIN32 #include #endif #endif /* SVG font modifiers */ #define REGULAR 0 #define BOLD 1 #define ITALIC 2 /* SVG patterns */ #define P_SOLID 0 #define P_NONE 15 #define P_DOTTED 4 /* i wasn't sure about this */ #define P_DASHED 11 /* or this */ /* SVG bold line constant */ #define WIDTH_NORMAL 1 #define WIDTH_BOLD 3 #ifdef TESTFAILED /* sodipodi doesn't understand style sheets */ #define DEFAULT_STYLE\ "" #define DEFAULT_FILTER \ "\n" \ "\n" \ "\n" \ "\n" \ "\n" \ "\n" \ "\n" \ "\n" \ "\n" \ "\n" \ "\n" \ "\n" \ "" #endif #define SCALE 1 /* SVG dash array */ static char * sdarray = "5,2"; /* SVG dot array */ static char * sdotarray = "1,5"; static int N_pages; static point Pages; static double Scale; static pointf Offset; static int Rot; static box PB; static int onetime = TRUE; static node_t *Curnode; static int GraphURL, ClusterURL, NodeURL, EdgeURL; typedef struct context_t { char *pencolor,*fillcolor,*fontfam,fontopt,font_was_set; char pen,fill,penwidth,style_was_set; double fontsz; } context_t; #define MAXNEST 4 static context_t cstk[MAXNEST]; static int SP; static char *op[] = {"graph", "node", "edge", "graph"}; #ifdef HAVE_LIBZ static gzFile Zfile; #endif static int svg_fputs (char *s) { int len; len = strlen(s); #ifdef HAVE_LIBZ if (Output_lang == SVGZ) return gzwrite(Zfile,s,(unsigned)len); else #endif return fwrite(s,sizeof(char),(unsigned)len,Output_file); } /* svg_printf: * Note that this function is unsafe due to the fixed buffer size. * It should only be used when the caller is sure the input will not * overflow the buffer. In particular, it should be avoided for * input coming from users. Also, if vsnprintf is available, the * code should check for return values to use it safely. */ static int svg_printf(const char *format, ...) { char buf[BUFSIZ]; va_list argp; int len; va_start(argp, format); #ifdef HAVE_VSNPRINTF (void)vsnprintf(buf, sizeof(buf), format, argp); #else (void)vsprintf(buf, format, argp); #endif va_end(argp); len = strlen(buf); /* some *sprintf (e.g C99 std) don't return the number of bytes actually written */ #ifdef HAVE_LIBZ if (Output_lang == SVGZ) return gzwrite(Zfile,buf,(unsigned)len); else #endif return fwrite(buf,sizeof(char),(unsigned)len,Output_file); } static void svg_reset(void) { onetime = TRUE; } static void init_svg(void) { SP = 0; cstk[0].pencolor = DEFAULT_COLOR; /* SVG pencolor */ cstk[0].fillcolor = ""; /* SVG fillcolor */ cstk[0].fontfam = DEFAULT_FONTNAME; /* font family name */ cstk[0].fontsz = DEFAULT_FONTSIZE; /* font size */ cstk[0].fontopt = REGULAR; /* modifier: REGULAR, BOLD or ITALIC */ cstk[0].pen = P_SOLID; /* pen pattern style, default is solid */ cstk[0].fill = P_NONE; cstk[0].penwidth = WIDTH_NORMAL; } /* can't hack a transform directly in SVG preamble, alas */ static point svgpt(point p) { point rv; if (Rot == 0) { rv.x = PB.LL.x / Scale + p.x + Offset.x; rv.y = PB.UR.y / Scale - 1 - p.y - Offset.y; } else { rv.x = PB.UR.x / Scale - 1 - p.y - Offset.x; rv.y = PB.UR.y / Scale - 1 - p.x - Offset.y; } return rv; } static void svgbzptarray(point* A, int n) { int i; point p; char *c; c = "M"; /* first point */ for (i = 0; i < n; i++) { p.x = A[i].x; p.y = A[i].y; p = svgpt(p); svg_printf("%s%d,%d",c,p.x,p.y); if (i==0) c = "C"; /* second point */ else c = " "; /* remaining points */ } } static char* svg_resolve_color(char *name) { /* color names from http://www.w3.org/TR/SVG/types.html */ static char *known_colors[] = { "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow", "grey", "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey", "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray", "slategrey", "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", "whitesmoke", "yellow", "yellowgreen", 0}; static char buf[SMALLBUF]; char *tok, **known; color_t color; tok = canontoken(name); for (known = known_colors; *known; known++) if (streq(tok,*known)) break; if (*known == 0) { if (streq(tok,"transparent")) { tok = "none"; } else { colorxlate(name,&color,RGBA_BYTE); sprintf(buf,"#%02x%02x%02x", color.u.rgba[0], color.u.rgba[1], color.u.rgba[2]); tok = buf; } } return tok; } static void svg_font(context_t* cp) { char *color, buf[BUFSIZ]; int needstyle=0; strcpy(buf," style=\""); if (strcasecmp(cp->fontfam,DEFAULT_FONTNAME)) { sprintf(buf+strlen(buf),"font-family:%s;",cp->fontfam); needstyle++; } if (cp->fontsz != DEFAULT_FONTSIZE) { sprintf(buf+strlen(buf),"font-size:%.2f;",(cp->fontsz)); needstyle++; } color = svg_resolve_color(cp->pencolor); if ((strcasecmp(color,"black"))) { sprintf(buf+strlen(buf),"fill:%s;",color); needstyle++; } if (needstyle) { strcat(buf,"\""); svg_fputs(buf); } } static void svg_grstyle(context_t* cp, int filled) { svg_printf(" style=\""); if (filled) svg_printf("fill:%s;",svg_resolve_color(cp->fillcolor)); else svg_printf("fill:none;"); svg_printf("stroke:%s;",svg_resolve_color(cp->pencolor)); if (cp->penwidth!=WIDTH_NORMAL) svg_printf("stroke-width:%d;",cp->penwidth); if( cp->pen == P_DASHED ) { svg_printf("stroke-dasharray:%s;", sdarray); } else if( cp->pen == P_DOTTED) { svg_printf("stroke-dasharray:%s;", sdotarray); } svg_printf("\""); } static void svg_comment(void* obj, attrsym_t* sym) { char *str = late_string(obj,sym,""); if (str[0]) { svg_fputs (" sequences in str */ svg_fputs (str); svg_fputs (" -->\n"); } } static void svg_begin_job(FILE *ofp, graph_t *g, char **lib, char *user, char *info[], point pages) { char *s; #if HAVE_LIBZ int fd; #endif if (Output_lang == SVGZ) { #if HAVE_LIBZ fd = dup(fileno(Output_file)); /* open dup so can gzclose independent of FILE close */ #ifdef HAVE_SETMODE #ifdef O_BINARY /* * Windows will do \n -> \r\n translations on * stdout unless told otherwise. */ setmode(fd, O_BINARY); #endif #endif Zfile = gzdopen(fd, "wb"); if (!Zfile) { agerr(AGERR, "Error opening compressed output file\n"); exit(1); } #else agerr(AGERR, "No support for compressed output. Not compiled with zlib.\n"); exit(1); #endif } Pages = pages; N_pages = pages.x * pages.y; svg_printf("\n"); if ((s = agget(g, "stylesheet")) && strlen(s)) { svg_printf("\n", s); } svg_printf("proto->n,"URL") || agfindattr(g->proto->e,"URL"))) { svg_printf(" [\n \n]"); } #endif svg_fputs(">\n"); svg_printf("\n", user, g->name, N_pages); } static void svg_end_job(void) { } static void svg_begin_graph(graph_t* g, box bb, point pb) { char *str; double res; PB.LL.x = PB.LL.y = 0; PB.UR.x = (bb.UR.x - bb.LL.x + 2*GD_drawing(g)->margin.x) * SCALE; PB.UR.y = (bb.UR.y - bb.LL.y + 2*GD_drawing(g)->margin.y) * SCALE; Offset.x = GD_drawing(g)->margin.x * SCALE; Offset.y = GD_drawing(g)->margin.y * SCALE; if (onetime) { #if 0 fprintf(stderr,"LL %d %d UR %d %d\n", PB.LL.x,PB.LL.y,PB.UR.x, PB.UR.y); #endif init_svg(); svg_comment(g,agfindattr(g,"comment")); onetime = FALSE; } if ((str = agget(g,"resolution")) && str[0]) res = atof(str); else res = 96; /* terrific guess */ if (res < 1.0) svg_printf("proto->n,"URL") || agfindattr(g->proto->e,"URL"))) { svg_fputs(" xmlns:xlink=\"http://www.w3.org/1999/xlink\""); } svg_fputs(">\n"); } static void svg_end_graph(void) { #ifdef HAVE_LIBZ if (Output_lang==SVGZ) gzclose(Zfile); #endif } static void svg_output_anchor(char *url, char *label, char *tooltip) { svg_printf("\n"); } static void svg_begin_page(graph_t *g, point page, double scale, int rot, point offset) { int page_number; char *s; /* point sz; */ Scale = scale; Rot = rot; page_number = page.x + page.y * Pages.x + 1; /* sz = sub_points(PB.UR,PB.LL); */ /* its really just a page of the graph, but its still a graph, * and it is the entire graph if we're not currently paging */ svg_printf("\n", cstk[0].fontfam,cstk[0].fontsz); svg_printf("%s\n", xml_string(g->name)); if ((s = agget(g, "URL")) && strlen(s)) { GraphURL = 1; s = strdup_and_subst_graph(s,g); svg_printf("\n",xml_string(s)); free(s); } else { GraphURL = 0; } } static void svg_end_page(void) { if (GraphURL) { svg_printf(""); ClusterURL = 0; } svg_fputs("\n"); } static void svg_begin_cluster(graph_t* g) { char *s; svg_printf("",op[Obj],g->meta_node->id); svg_printf("%s\n",xml_string(g->name)); if ((s = agget(g, "URL")) && strlen(s)) { ClusterURL = 1; s = strdup_and_subst_graph(s,g); svg_printf("\n",xml_string(s)); free(s); } else { ClusterURL = 0; } } static void svg_end_cluster (void) { if (ClusterURL) { svg_printf(""); ClusterURL = 0; } svg_printf("\n"); } static void svg_begin_nodes(void) { } static void svg_end_nodes(void) { } static void svg_begin_edges(void) { } static void svg_end_edges(void) { } static void svg_begin_node(node_t* n) { char *url,*label,*tooltip, *m_tooltip=NULL; Curnode = n; #if 0 svg_printf("\n",n->name); svg_comment(n,N_comment); #endif svg_printf("",op[Obj],n->id); svg_printf("%s\n",xml_string(n->name)); if ((url = agget(n, "URL")) && strlen(url)) { NodeURL = 1; url = strdup_and_subst_node(url,n); label = ND_label(n)->text; if ((tooltip = agget(n, "tooltip")) && strlen(tooltip)) { m_tooltip = tooltip = strdup_and_subst_node(tooltip,n); } else { tooltip = label; } svg_output_anchor(url, label, tooltip); if (m_tooltip) free(tooltip); free(url); } else { NodeURL = 0; } } static void svg_end_node (void) { if (NodeURL) { svg_printf(""); NodeURL = 0; } svg_printf("\n"); } static void svg_begin_edge (edge_t* e) { char *url,*label=NULL,*tooltip,*m_tooltip=NULL,*edgeop; textlabel_t *lab=NULL; svg_printf("",op[Obj],e->id); if (e->tail->graph->root->kind & AGFLAG_DIRECTED) edgeop = "->"; else edgeop = "--"; svg_printf("%s%s",xml_string(e->tail->name),edgeop); /* can't do this in single svg_printf because * xml_string's buffer gets reused. */ svg_printf("%s\n",xml_string(e->head->name)); if ((url = agget(e, "URL")) && strlen(url)) { EdgeURL = 1; url = strdup_and_subst_edge(url,e); if ((lab=ED_label(e))) { label = lab->text; } if ((tooltip = agget(e, "tooltip")) && strlen(tooltip)) { m_tooltip = tooltip = strdup_and_subst_edge(tooltip,e); } else { tooltip = label; } svg_output_anchor(url, label, tooltip); if (m_tooltip) free(tooltip); free(url); } else { EdgeURL = 0; } } static void svg_end_edge (void) { if (EdgeURL) { svg_printf(""); EdgeURL = 0; } svg_printf("\n"); } static void svg_begin_context(void) { assert(SP + 1 < MAXNEST); cstk[SP+1] = cstk[SP]; SP++; } static void svg_end_context(void) { int psp = SP - 1; assert(SP > 0); /*free(cstk[psp].fontfam);*/ SP = psp; } static void svg_set_font(char* name, double size) { char *p; #if 0 /* see below */ char *q; #endif context_t *cp; cp = &(cstk[SP]); cp->font_was_set = TRUE; cp->fontsz = size; p = strdup(name); #if 0 /* * this doesn't work as originally intended * fontnames can be things like: "Times-Roman" * where "Roman" is not an ITALIC or BOLD indicator. */ if ((q = strchr(p,'-'))) { *q++ = 0; if (strcasecmp(q,"italic") == 0) cp->fontopt = ITALIC; else if (strcasecmp(q,"bold") == 0) cp->fontopt = BOLD; } cp->fontfam = p; #else cp->fontfam = p; #endif } static void svg_set_pencolor(char* name) { cstk[SP].pencolor = name; } static void svg_set_fillcolor(char* name) { cstk[SP].fillcolor = name; } static void svg_set_style(char** s) { char *line, *p; context_t *cp; cp = &(cstk[SP]); while ((p = line = *s++)) { if (streq(line,"solid")) cp->pen = P_SOLID; else if (streq(line,"dashed")) cp->pen = P_DASHED; else if (streq(line,"dotted")) cp->pen = P_DOTTED; else if (streq(line,"invis")) cp->pen = P_NONE; else if (streq(line,"bold")) cp->penwidth = WIDTH_BOLD; else if (streq(line, "setlinewidth")) { while (*p) p++; p++; cp->penwidth = atol(p); } else if (streq(line,"filled")) cp->fill = P_SOLID; else if (streq(line,"unfilled")) cp->fill = P_NONE; else { agerr(AGERR, "svg_set_style: unsupported style %s - ignoring\n", line); } cp->style_was_set = TRUE; } /* if (cp->style_was_set) svg_style(cp); */ } static void svg_textline(point p, textline_t *line) { char *anchor, *string; point mp; context_t *cp; string = xml_string(line->str); if (strlen(string) == 0) { /* its zero length, don't draw */ return; } cp = &(cstk[SP]); if( cp->pen == P_NONE ) { /* its invisible, don't draw */ return; } switch(line->just) { case 'l': anchor="start"; break; case 'r': anchor="end"; break; default: case 'n': anchor="middle"; break; } mp = svgpt(p); svg_printf(""); svg_fputs(string); svg_fputs("\n"); } static void svg_ellipse(point p, int rx, int ry, int filled) { point mp; if( cstk[SP].pen == P_NONE ) { /* its invisible, don't draw */ return; } mp.x = p.x; mp.y = p.y; mp = svgpt(mp); svg_printf("\n"); } static void svg_bezier(point* A, int n, int arrow_at_start, int arrow_at_end) { if( cstk[SP].pen == P_NONE ) { /* its invisible, don't draw */ return; } svg_fputs("\n"); } static void svg_polygon(point *A, int n, int filled) { int i; point p; if( cstk[SP].pen == P_NONE ) { /* its invisible, don't draw */ return; } svg_printf("\n"); } static void svg_polyline(point* A, int n) { int i; point p; if( cstk[SP].pen == P_NONE ) { /* its invisible, don't draw */ return; } svg_printf("\n"); } static void svg_user_shape(char *name, point *A, int n, int filled) { int i; point p; point sz; char *imagefile; int minx, miny; if( cstk[SP].pen == P_NONE ) { /* its invisible, don't draw */ return; } imagefile = agget(Curnode,"shapefile"); if (imagefile == 0) { svg_polygon(A, n, filled); return; } p = ND_coord_i(Curnode); p.x -= ND_lw_i(Curnode); p.y += ND_ht_i(Curnode)/2; p = svgpt(p); sz.x = ROUND((ND_lw_i(Curnode)+ND_rw_i(Curnode))); sz.y = ROUND(ND_ht_i(Curnode)); svg_printf("\n", name, Curnode->name); svg_printf("\n\n"); svg_printf("\n", imagefile, sz.x, sz.y, minx, miny, name, Curnode->name); } codegen_t SVG_CodeGen = { svg_reset, svg_begin_job, svg_end_job, svg_begin_graph, svg_end_graph, svg_begin_page, svg_end_page, svg_begin_cluster, svg_end_cluster, svg_begin_nodes, svg_end_nodes, svg_begin_edges, svg_end_edges, svg_begin_node, svg_end_node, svg_begin_edge, svg_end_edge, svg_begin_context, svg_end_context, svg_set_font, svg_textline, svg_set_pencolor, svg_set_fillcolor, svg_set_style, svg_ellipse, svg_polygon, svg_bezier, svg_polyline, 0 /* svg_arrowhead */, svg_user_shape, 0 /* svg_comment */, 0 /* svg_textsize */ };