[rrd-developers] [patch] support for SVG image file format

Peter Speck speck at ruc.dk
Tue Mar 26 03:55:34 MET 2002


Hi,

This patch is for current cvs (aka the 2002-03-24 snapshot). It adds 
support for SVG image file format by using "--imgformat SVG".

SVG is a image format defined by W3C (like PNG). It is based on xml and 
is resolution independent. Support in viewiers for gzip'ed files is 
recommended by W3C (suffix .svgz).

The patch does not use any 3rd party libraries and is mostly just 
addition of a gfx_render_svg function to rrd_gfx.c. Thanks to the new 
rrd_gfx functionality this addition to rrd is very straightforward.
The patch supports vertical text, so enabling it in rrd_graph.c helps 
display of it (seatch for 'yaxis description').

The open source SVG viewer Batik from the Apache Foundation is a rather 
good tool as it allows you to see the internals of the SVG file. You can 
download it from:
http://xml.apache.org/batik/
The browser plugin from Adobe can be downloaded from:
http://www.adobe.com/svg/viewer/install/main.html
More viewers (and editors) are listed at W3C:
http://www.w3.org/Graphics/SVG/SVG-Implementations

The patch is somewhat verbose as I have spent some time implementing 
optimizations for reducing the svg filesize. This includes reducing the 
number of characters for writing coordinates and numbers. I have reduced 
initial filesize by approx. 50% (this is of the raw non-gzip file).

I have made a sample .svg file and screendump which you can view at:
http://vitality.dk/rrdtool/samle.html  (44Kb, for viewing the source)
http://vitality.dk/rrdtool/samle.svg   (37Kb, for SVG viewer)
http://vitality.dk/rrdtool/samle.svgz  (11Kb, ditto, compressed)
http://vitality.dk/rrdtool/samle.png   (46Kb, screendump)
My web provider hasn't added svg/svgz yet to the list of mime types, so 
you might have best luck by downloading the .svg file before viewing it.

It does not yet handle stringwidth calculations for e.g. calculation of 
legend spacing. I will look into that.

Support for tabwidth is missing too. As reading info from an svg file is 
much much more complex than writing it, there is no support for the 
--lazy flag.

  - Peter Speck

---------
--- ../cvs-rr/program/src/rrd_gfx.h	Sun Mar 17 23:40:18 2002
+++ src/rrd_gfx.h	Sun Mar 24 22:24:39 2002
@@ -25,6 +25,7 @@
    char *filename;             /* font or image filename */
    char *text;
    double        x,y;          /* position */
+  double	angle;
    enum gfx_h_align_en halign; /* text alignement */
    enum gfx_v_align_en valign; /* text alignement */
    double        tabwidth;
@@ -79,6 +80,11 @@
                                double zoom,
                                gfx_color_t background, FILE *fo);

+/* turn graph into an svg image */
+int       gfx_render_svg (gfx_canvas_t *canvas,
+                              art_u32 width, art_u32 height,
+                              double zoom,
+                              gfx_color_t background, FILE *fo);

  /* free memory used by nodes this will also remove memory required for
     node chain and associated material */
--- ../cvs-rr/program/src/rrd_graph.h	Sat Mar 23 20:59:39 2002
+++ src/rrd_graph.h	Sun Mar 24 20:11:23 2002
@@ -25,7 +25,7 @@
  	    GF_DEF, GF_CDEF, GF_VDEF,
  	    GF_PART};

-enum if_en {IF_PNG=0};
+enum if_en {IF_PNG=0,IF_SVG};

  enum vdef_op_en {
  		 VDEF_MAXIMUM	/* like the MAX in (G)PRINT */
--- ../cvs-rr/program/src/rrd_gfx.c	Sun Mar 17 23:40:18 2002
+++ src/rrd_gfx.c	Tue Mar 26 03:48:13 2002
@@ -173,6 +173,7 @@
     node->filename = strdup(font);
     node->x = x;
     node->y = y;
+   node->angle = angle;
     node->color = color;
     node->tabwidth = tabwidth;
     node->halign = h_align;
@@ -466,4 +467,403 @@
    png_write_end(png_ptr, info_ptr);
    png_destroy_write_struct(&png_ptr, &info_ptr);
    return 1;
+}
+
+static int svg_indent = 0;
+static int svg_single_line = 0;
+static void svg_print_indent(FILE *fp)
+{
+  int i;
+  for (i = svg_indent - svg_single_line; i > 0; i--) {
+    putc(' ', fp);
+    putc(' ', fp);
+  }
+}
+
+static void svg_start_tag(FILE *fp, const char *name)
+{
+  svg_print_indent(fp);
+  putc('<', fp);
+  fputs(name, fp);
+  svg_indent++;
+}
+
+static void svg_close_tag_single_line(FILE *fp)
+{
+  svg_single_line++;
+  putc('>', fp);
+}
+
+static void svg_close_tag(FILE *fp)
+{
+  putc('>', fp);
+  if (!svg_single_line)
+    putc('\n', fp);
+}
+
+static void svg_end_tag(FILE *fp, const char *name)
+{
+  /* name is NULL if closing empty-node tag */
+  svg_indent--;
+  if (svg_single_line)
+    svg_single_line--;
+  else if (name)
+    svg_print_indent(fp);
+  if (name != NULL) {
+    fputs("</", fp);
+    fputs(name, fp);
+  } else {
+    putc('/', fp);
+  }
+  svg_close_tag(fp);
+}
+
+static void svg_close_tag_empty_node(FILE *fp)
+{
+  svg_end_tag(fp, NULL);
+}
+
+static void svg_write_text(FILE *fp, const char *p)
+{
+  char ch;
+  const char *start, *last;
+  if (!p)
+    return;
+  /* trim leading spaces */
+  while (*p == ' ')
+    p++;
+  start = p;
+  /* trim trailing spaces */
+  last = p - 1;
+  while ((ch = *p) != 0) {
+    if (ch != ' ')
+      last = p;
+    p++;
+  }
+  /* encode trimmed text */
+  p = start;
+  while (p <= last) {
+    ch = *p++;
+    switch (ch) {
+      case '&': fputs("&amp;", fp); break;
+      case '<': fputs("&lt;", fp); break;
+      case '>': fputs("&gt;", fp); break;
+      case '"': fputs("&quot;", fp); break;
+      default: putc(ch, fp);
+    }
+  }
+}
+
+static void svg_write_number(FILE *fp, double d)
+{
+  /* omit decimals if integer to reduce filesize */
+  char buf[60], *p;
+  snprintf(buf, sizeof(buf), "%.2f", d);
+  p = buf; /* doesn't trust snprintf return value */
+  while (*p)
+    p++;
+  while (--p > buf) {
+    char ch = *p;
+    if (ch == '0') {
+      *p = '\0'; /* zap trailing zeros */
+      continue;
+    }
+    if (ch == '.')
+      *p = '\0'; /* zap trailing dot */
+    break;
+  }
+  fputs(buf, fp);
+}
+
+static int svg_color_is_black(int c)
+{
+  /* gfx_color_t is RRGGBBAA, svg can use #RRGGBB like html */
+  c = (int)((c >> 8) & 0xFFFFFF);
+  return !c;
+}
+
+static void svg_write_color(FILE *fp, int c)
+{
+  /* gfx_color_t is RRGGBBAA, svg can use #RRGGBB like html */
+  c = (int)((c >> 8) & 0xFFFFFF);
+  if ((c & 0x0F0F0F) == ((c >> 4) & 0x0F0F0F)) {
+    /* css2 short form, #rgb is #rrggbb, not #r0g0b0 */
+    fprintf(fp, "#%03X",
+          ( ((c >> 8) & 0xF00)
+          | ((c >> 4) & 0x0F0)
+          | ( c       & 0x00F)));
+  } else {
+    fprintf(fp, "#%06X", c);
+  }
+}
+
+static int svg_is_int_step(double a, double b)
+{
+  double diff = fabs(a - b);
+  return floor(diff) == diff;
+}
+
+static int svg_path_straight_segment(FILE *fp,
+    double lastA, double currentA, double currentB,
+    gfx_node_t *node,
+    int segment_idx, int isx, char absChar, char relChar)
+{
+  if (!svg_is_int_step(lastA, currentA)) {
+    putc(absChar, fp);
+    svg_write_number(fp, currentA);
+    return 0;
+  }
+  if (segment_idx < node->points - 1) {
+    ArtVpath *vec = node->path + segment_idx + 1;
+    if (vec->code == ART_LINETO) {
+      double nextA = (isx ? vec->x : vec->y) - LINEOFFSET;
+      double nextB = (isx ? vec->y : vec->x) - LINEOFFSET;
+      if (nextB == currentB
+          && ((currentA >= lastA) == (nextA >= currentA))
+          && svg_is_int_step(currentA, nextA)) {
+        return 1; /* skip to next as it is a straight line  */
+      }
+    }
+  }
+  putc(relChar, fp);
+  svg_write_number(fp, currentA - lastA);
+  return 0;
+}
+
+static void svg_path(FILE *fp, gfx_node_t *node, int multi)
+{
+  int i;
+  double lastX = 0, lastY = 0;
+  /* for straight lines <path..> tags take less space than
+     <line..> tags because of the efficient packing
+     in the 'd' attribute */
+  svg_start_tag(fp, "path");
+  if (!multi) {
+    fputs(" stroke-width=\"", fp);
+    svg_write_number(fp, node->size);
+    fputs("\" stroke=\"", fp);
+    svg_write_color(fp, node->color);
+    fputs("\" fill=\"none\"", fp);
+  }
+  fputs(" d=\"", fp);
+  /* specification of the 'd' attribute: */
+  /* http://www.w3.org/TR/SVG/paths.html#PathDataGeneralInformation */
+  for (i = 0; i < node->points; i++) {
+    ArtVpath *vec = node->path + i;
+    double x = vec->x - LINEOFFSET;
+    double y = vec->y - LINEOFFSET;
+    switch (vec->code) {
+    case ART_MOVETO_OPEN: /* fall-through */
+    case ART_MOVETO:
+      putc('M', fp);
+      svg_write_number(fp, x);
+      putc(',', fp);
+      svg_write_number(fp, y);
+      break;
+    case ART_LINETO:
+      /* try optimize filesize by using minimal lineto commands */
+      /* without introducing rounding errors. */
+      if (x == lastX) {
+        if (svg_path_straight_segment(fp, lastY, y, x, node, i, 0, 'V', 
'v'))
+          continue;
+      } else if (y == lastY) {
+        if (svg_path_straight_segment(fp, lastX, x, y, node, i, 1, 'H', 
'h'))
+          continue;
+      } else {
+        putc('L', fp);
+        svg_write_number(fp, x);
+        putc(',', fp);
+        svg_write_number(fp, y);
+      }
+      break;
+    case ART_CURVETO: break; /* unsupported */
+    case ART_END: break; /* nop */
+    }
+    lastX = x;
+    lastY = y;
+  }
+  fputs("\"", fp);
+  svg_close_tag_empty_node(fp);
+}
+
+static void svg_multi_path(FILE *fp, gfx_node_t **nodeR)
+{
+  /* optimize for multiple paths with the same color, penwidth, etc. */
+  int num = 1;
+  gfx_node_t *node = *nodeR;
+  gfx_node_t *next = node->next;
+  while (next) {
+    if (next->type != node->type
+        || next->size != node->size
+        || next->color != node->color)
+      break;
+    next = next->next;
+    num++;
+  }
+  if (num == 1) {
+    svg_path(fp, node, 0);
+    return;
+  }
+  svg_start_tag(fp, "g");
+  fputs(" stroke-width=\"", fp);
+  svg_write_number(fp, node->size);
+  fputs("\" stroke=\"", fp);
+  svg_write_color(fp, node->color);
+  fputs("\" fill=\"none\"", fp);
+  svg_close_tag(fp);
+  while (num && node) {
+    svg_path(fp, node, 1);
+    if (!--num)
+      break;
+    node = node->next;
+    *nodeR = node;
+  }
+  svg_end_tag(fp, "g");
+}
+
+static void svg_area(FILE *fp, gfx_node_t *node)
+{
+  int i;
+  double startX = 0, startY = 0;
+  svg_start_tag(fp, "polygon");
+  fputs(" fill=\"", fp);
+  svg_write_color(fp, node->color);
+  fputs("\" points=\"", fp);
+  for (i = 0; i < node->points; i++) {
+    ArtVpath *vec = node->path + i;
+    double x = vec->x - LINEOFFSET;
+    double y = vec->y - LINEOFFSET;
+    switch (vec->code) {
+      case ART_MOVETO_OPEN: /* fall-through */
+      case ART_MOVETO:
+        svg_write_number(fp, x);
+        putc(',', fp);
+        svg_write_number(fp, y);
+        startX = x;
+        startY = y;
+        break;
+      case ART_LINETO:
+        if (i == node->points - 2
+			&& node->path[i + 1].code == ART_END
+            && fabs(x - startX) < 0.001 && fabs(y - startY) < 0.001) {
+          break; /* poly area always closed, no need for last point */
+        }
+        putc(' ', fp);
+        svg_write_number(fp, x);
+        putc(',', fp);
+        svg_write_number(fp, y);
+        break;
+      case ART_CURVETO: break; /* unsupported */
+      case ART_END: break; /* nop */
+    }
+  }
+  fputs("\"", fp);
+  svg_close_tag_empty_node(fp);
+}
+
+static void svg_text(FILE *fp, gfx_node_t *node)
+{
+  double x = node->x - LINEOFFSET;
+  double y = node->y - LINEOFFSET;
+  if (node->angle != 0) {
+    svg_start_tag(fp, "g");
+    fputs(" transform=\"translate(", fp);
+    svg_write_number(fp, x);
+    fputs(",", fp);
+    svg_write_number(fp, y);
+    fputs(") rotate(", fp);
+    svg_write_number(fp, node->angle);
+    fputs(")\"", fp);
+    x = y = 0;
+    svg_close_tag(fp);
+  }
+  switch (node->valign) {
+  case GFX_V_TOP:  y += node->size; break;
+  case GFX_V_CENTER: y += node->size / 2; break;
+  case GFX_V_BOTTOM: break;
+  case GFX_V_NULL: break;
+  }
+  svg_start_tag(fp, "text");
+  fputs(" x=\"", fp);
+  svg_write_number(fp, x);
+  fputs("\" y=\"", fp);
+  svg_write_number(fp, y);
+  fputs("\" font-size=\"", fp);
+  svg_write_number(fp, node->size);
+  fputs("\"", fp);
+  if (!svg_color_is_black(node->color)) {
+    fputs(" fill=\"", fp);
+    svg_write_color(fp, node->color);
+    fputs("\"", fp);
+  }
+  switch (node->halign) {
+  case GFX_H_RIGHT:  fputs(" text-anchor=\"end\"", fp); break;
+  case GFX_H_CENTER: fputs(" text-anchor=\"middle\"", fp); break;
+  case GFX_H_LEFT: break;
+  case GFX_H_NULL: break;
+  }
+  svg_close_tag_single_line(fp);
+  /* support for node->tabwidth missing */
+  svg_write_text(fp, node->text);
+  svg_end_tag(fp, "text");
+  if (node->angle != 0)
+    svg_end_tag(fp, "g");
+}
+
+int       gfx_render_svg (gfx_canvas_t *canvas,
+                art_u32 width, art_u32 height,
+                double zoom,
+                gfx_color_t background, FILE *fp){
+  gfx_node_t *node = canvas->firstnode;
+  fputs(
+"<?xml version=\"1.0\" standalone=\"yes\"?>\n"
+"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\n"
+"   \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n"
+"<!--\n"
+"   SVG file created by RRDtool,\n"
+"   Tobias Oetiker <tobi at oetike.ch>, http://tobi.oetiker.ch\n"
+"\n"
+"   The width/height attributes in the outhermost svg node\n"
+"   are just default sizes for the browser which is used\n"
+"   if the svg file is openened directly without being\n"
+"   embedded in an html file.\n"
+"   The viewBox is the local coord system for rrdtool.\n"
+"-->\n", fp);
+  svg_start_tag(fp, "svg");
+  fputs(" width=\"", fp);
+  svg_write_number(fp, width * zoom);
+  fputs("\" height=\"", fp);
+  svg_write_number(fp, height * zoom);
+  fputs("\" x=\"0\" y=\"0\" viewBox=\"", fp);
+  svg_write_number(fp, -LINEOFFSET);
+  fputs(" ", fp);
+  svg_write_number(fp, -LINEOFFSET);
+  fputs(" ", fp);
+  svg_write_number(fp, width - LINEOFFSET);
+  fputs(" ", fp);
+  svg_write_number(fp, height - LINEOFFSET);
+  fputs("\" preserveAspectRatio=\"xMidYMid\"", fp);
+  fputs(" font-family=\"Helvetica\"", fp); /* default font */
+  svg_close_tag(fp);
+  svg_start_tag(fp, "rect");
+  fprintf(fp, " x=\"0\" y=\"0\" width=\"%d\" height=\"%d\"", width, 
height);
+  fputs(" style=\"fill:", fp);
+  svg_write_color(fp, background);
+  fputs("\"", fp);
+  svg_close_tag_empty_node(fp);
+  while (node) {
+    switch (node->type) {
+    case GFX_LINE:
+      svg_multi_path(fp, &node);
+      break;
+    case GFX_AREA:
+      svg_area(fp, node);
+      break;
+    case GFX_TEXT:
+      svg_text(fp, node);
+    }
+    node = node->next;
+  }
+  svg_end_tag(fp, "svg");
+  return 0;
  }
--- ../cvs-rr/program/src/rrd_graph.c	Sat Mar 23 21:41:48 2002
+++ src/rrd_graph.c	Sun Mar 24 22:43:11 2002
@@ -182,6 +182,7 @@
  enum if_en if_conv(char *string){

      conv_if(PNG,IF_PNG)
+    conv_if(SVG,IF_SVG)

      return (-1);
  }
@@ -1868,6 +1869,9 @@
      case IF_PNG:
  	   size = PngSize(fd,&(im->xgif),&(im->ygif));
  	   break;
+    case IF_SVG:
+	   size = 0;
+	   break;
      }
      fclose(fd);
      return size;
@@ -2352,6 +2356,9 @@
    switch (im->imgformat) {
    case IF_PNG:
      gfx_render_png (canvas,im->xgif,im->ygif,im->zoom,0x0,fo);
+    break;
+  case IF_SVG:
+    gfx_render_svg (canvas,im->xgif,im->ygif,im->zoom,0x0,fo);
      break;
    }
    if (strcmp(im->graphfile,"-") != 0)


--
Unsubscribe mailto:rrd-developers-request at list.ee.ethz.ch?subject=unsubscribe
Help        mailto:rrd-developers-request at list.ee.ethz.ch?subject=help
Archive     http://www.ee.ethz.ch/~slist/rrd-developers
WebAdmin    http://www.ee.ethz.ch/~slist/lsg2.cgi



More information about the rrd-developers mailing list