1 /* Dia -- an diagram creation/manipulation program
2  * Copyright (C) 1998 Alexander Larsson
3  *
4  * render_svg.c - an SVG renderer for dia, based on render_eps.c
5  * Copyright (C) 1999, 2000 James Henstridge
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20  */
21 #include <config.h>
22 
23 #include <stdlib.h>
24 #include <string.h>
25 #include <time.h>
26 #include <math.h>
27 #include <errno.h>
28 #ifdef HAVE_UNISTD_H
29 #include <unistd.h>
30 #endif
31 
32 #include <glib.h>
33 #include <glib/gstdio.h>
34 
35 #include <libxml/entities.h>
36 #include <libxml/tree.h>
37 #include <libxml/xmlmemory.h>
38 
39 #include "geometry.h"
40 #include "diasvgrenderer.h"
41 #include "filter.h"
42 #include "intl.h"
43 #include "message.h"
44 #include "diagramdata.h"
45 #include "dia_xml_libxml.h"
46 #include "object.h"
47 #include "textline.h"
48 
49 G_BEGIN_DECLS
50 
51 #define SVG_TYPE_RENDERER           (svg_renderer_get_type ())
52 #define SVG_RENDERER(obj)           (G_TYPE_CHECK_INSTANCE_CAST ((obj), SVG_TYPE_RENDERER, SvgRenderer))
53 #define SVG_RENDERER_CLASS(klass)   (G_TYPE_CHECK_CLASS_CAST ((klass), SVG_TYPE_RENDERER, SvgRendererClass))
54 #define SVG_IS_RENDERER(obj)        (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SVG_TYPE_RENDERER))
55 #define SVG_RENDERER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SVG_TYPE_RENDERER, SvgRendererClass))
56 
57 GType svg_renderer_get_type (void) G_GNUC_CONST;
58 
59 typedef struct _SvgRenderer SvgRenderer;
60 typedef struct _SvgRendererClass SvgRendererClass;
61 
62 struct _SvgRenderer
63 {
64   DiaSvgRenderer parent_instance;
65 
66   /* track the parents while grouping in draw_object() */
67   GQueue *parents;
68 };
69 
70 struct _SvgRendererClass
71 {
72   DiaSvgRendererClass parent_class;
73 };
74 
75 G_END_DECLS
76 
77 static DiaSvgRenderer *new_svg_renderer(DiagramData *data, const char *filename);
78 
79 static void draw_object       (DiaRenderer *renderer,
80                                DiaObject *object);
81 static void draw_rounded_rect (DiaRenderer *renderer,
82                                Point *ul_corner, Point *lr_corner,
83                                Color *colour, real rounding);
84 static void fill_rounded_rect (DiaRenderer *renderer,
85                                Point *ul_corner, Point *lr_corner,
86                                Color *colour, real rounding);
87 static void draw_string       (DiaRenderer *self,
88 	                       const char *text,
89 			       Point *pos, Alignment alignment,
90 			       Color *colour);
91 static void draw_text_line    (DiaRenderer *self, TextLine *text_line,
92 	                       Point *pos, Alignment alignment, Color *colour);
93 static void draw_text         (DiaRenderer *self, Text *text);
94 
95 static void svg_renderer_class_init (SvgRendererClass *klass);
96 
97 static gpointer parent_class = NULL;
98 
99 /* constructor */
100 static void
svg_renderer_init(GTypeInstance * self,gpointer g_class)101 svg_renderer_init (GTypeInstance *self, gpointer g_class)
102 {
103   SvgRenderer *renderer = SVG_RENDERER (self);
104 
105   renderer->parents = g_queue_new ();
106 }
107 
108 GType
svg_renderer_get_type(void)109 svg_renderer_get_type (void)
110 {
111   static GType object_type = 0;
112 
113   if (!object_type)
114     {
115       static const GTypeInfo object_info =
116       {
117         sizeof (SvgRendererClass),
118         (GBaseInitFunc) NULL,
119         (GBaseFinalizeFunc) NULL,
120         (GClassInitFunc) svg_renderer_class_init,
121         NULL,           /* class_finalize */
122         NULL,           /* class_data */
123         sizeof (SvgRenderer),
124         0,              /* n_preallocs */
125 	svg_renderer_init /* init */
126       };
127 
128       object_type = g_type_register_static (DIA_TYPE_SVG_RENDERER,
129                                             "SvgRenderer",
130                                             &object_info, 0);
131     }
132 
133   return object_type;
134 }
135 
136 static void
begin_render(DiaRenderer * self)137 begin_render (DiaRenderer *self)
138 {
139   SvgRenderer *renderer = SVG_RENDERER (self);
140   g_assert (g_queue_is_empty (renderer->parents));
141   DIA_RENDERER_CLASS (parent_class)->begin_render (DIA_RENDERER (self));
142 }
143 
144 static void
end_render(DiaRenderer * self)145 end_render (DiaRenderer *self)
146 {
147   SvgRenderer *renderer = SVG_RENDERER (self);
148   g_assert (g_queue_is_empty (renderer->parents));
149   DIA_RENDERER_CLASS (parent_class)->end_render (DIA_RENDERER (self));
150 }
151 
152 /* destructor */
153 static void
svg_renderer_finalize(GObject * object)154 svg_renderer_finalize (GObject *object)
155 {
156   SvgRenderer *svg_renderer = SVG_RENDERER (object);
157 
158   g_queue_free (svg_renderer->parents);
159 
160   G_OBJECT_CLASS (parent_class)->finalize (object);
161 }
162 
163 static void
svg_renderer_class_init(SvgRendererClass * klass)164 svg_renderer_class_init (SvgRendererClass *klass)
165 {
166   GObjectClass *object_class = G_OBJECT_CLASS (klass);
167   DiaRendererClass *renderer_class = DIA_RENDERER_CLASS (klass);
168 
169   parent_class = g_type_class_peek_parent (klass);
170 
171   object_class->finalize = svg_renderer_finalize;
172 
173   renderer_class->begin_render = begin_render;
174   renderer_class->end_render = end_render;
175   renderer_class->draw_object = draw_object;
176   renderer_class->draw_rounded_rect = draw_rounded_rect;
177   renderer_class->fill_rounded_rect = fill_rounded_rect;
178   renderer_class->draw_string  = draw_string;
179   renderer_class->draw_text  = draw_text;
180   renderer_class->draw_text_line  = draw_text_line;
181 }
182 
183 
184 static DiaSvgRenderer *
new_svg_renderer(DiagramData * data,const char * filename)185 new_svg_renderer(DiagramData *data, const char *filename)
186 {
187   DiaSvgRenderer *renderer;
188   SvgRenderer *svg_renderer;
189   FILE *file;
190   gchar buf[512];
191   time_t time_now;
192   Rectangle *extent;
193   const char *name;
194   xmlDtdPtr dtd;
195 
196   file = g_fopen(filename, "w");
197 
198   if (file==NULL) {
199     message_error(_("Can't open output file %s: %s\n"),
200 		  dia_message_filename(filename), strerror(errno));
201     return NULL;
202   }
203   fclose(file);
204 
205   /* we need access to our base object */
206   renderer = DIA_SVG_RENDERER (g_object_new(SVG_TYPE_RENDERER, NULL));
207 
208   renderer->filename = g_strdup(filename);
209 
210   renderer->dash_length = 1.0;
211   renderer->dot_length = 0.2;
212   renderer->saved_line_style = LINESTYLE_SOLID;
213   /* apparently most svg readers don't like small values, especially not in the viewBox attribute */
214   renderer->scale = 20.0;
215 
216   /* set up the root node */
217   renderer->doc = xmlNewDoc((const xmlChar *)"1.0");
218   renderer->doc->encoding = xmlStrdup((const xmlChar *)"UTF-8");
219   renderer->doc->standalone = FALSE;
220   dtd = xmlCreateIntSubset(renderer->doc, (const xmlChar *)"svg",
221 		     (const xmlChar *)"-//W3C//DTD SVG 1.0//EN",
222 		     (const xmlChar *)"http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd");
223   xmlAddChild((xmlNodePtr) renderer->doc, (xmlNodePtr) dtd);
224   renderer->root = xmlNewDocNode(renderer->doc, NULL, (const xmlChar *)"svg", NULL);
225   xmlAddSibling(renderer->doc->children, (xmlNodePtr) renderer->root);
226 
227   /* add namespaces to make strict parsers happy, e.g. Firefox */
228   svg_renderer = SVG_RENDERER (renderer);
229 
230   /* set the extents of the SVG document */
231   extent = &data->extents;
232   g_snprintf(buf, sizeof(buf), "%dcm",
233 	     (int)ceil((extent->right - extent->left)));
234   xmlSetProp(renderer->root, (const xmlChar *)"width", (xmlChar *) buf);
235   g_snprintf(buf, sizeof(buf), "%dcm",
236 	     (int)ceil((extent->bottom - extent->top)));
237   xmlSetProp(renderer->root, (const xmlChar *)"height", (xmlChar *) buf);
238   g_snprintf(buf, sizeof(buf), "%d %d %d %d",
239 	     (int)floor(extent->left  * renderer->scale), (int)floor(extent->top * renderer->scale),
240 	     (int)ceil((extent->right - extent->left) * renderer->scale),
241 	     (int)ceil((extent->bottom - extent->top) * renderer->scale));
242   xmlSetProp(renderer->root, (const xmlChar *)"viewBox", (xmlChar *) buf);
243   xmlSetProp(renderer->root,(const xmlChar *)"xmlns", (const xmlChar *)"http://www.w3.org/2000/svg");
244   xmlSetProp(renderer->root,(const xmlChar *)"xmlns", (const xmlChar *)"http://www.w3.org/2000/svg");
245   xmlSetProp(renderer->root,(const xmlChar *)"xmlns:xlink", (const xmlChar *)"http://www.w3.org/1999/xlink");
246 
247   time_now = time(NULL);
248   name = g_get_user_name();
249 
250 #if 0
251   /* some comments at the top of the file ... */
252   xmlAddChild(renderer->root, xmlNewText("\n"));
253   xmlAddChild(renderer->root, xmlNewComment("Dia-Version: "VERSION));
254   xmlAddChild(renderer->root, xmlNewText("\n"));
255   g_snprintf(buf, sizeof(buf), "File: %s", dia->filename);
256   xmlAddChild(renderer->root, xmlNewComment(buf));
257   xmlAddChild(renderer->root, xmlNewText("\n"));
258   g_snprintf(buf, sizeof(buf), "Date: %s", ctime(&time_now));
259   buf[strlen(buf)-1] = '\0'; /* remove the trailing new line */
260   xmlAddChild(renderer->root, xmlNewComment(buf));
261   xmlAddChild(renderer->root, xmlNewText("\n"));
262   g_snprintf(buf, sizeof(buf), "For: %s", name);
263   xmlAddChild(renderer->root, xmlNewComment(buf));
264   xmlAddChild(renderer->root, xmlNewText("\n\n"));
265 
266   xmlNewChild(renderer->root, NULL, "title", dia->filename);
267 #endif
268 
269   return renderer;
270 }
271 
272 static void
draw_object(DiaRenderer * self,DiaObject * object)273 draw_object(DiaRenderer *self,
274             DiaObject *object)
275 {
276   /* wrap in  <g></g>
277    * We could try to be smart and count the objects we using for the object.
278    * If it is only one the grouping is superfluous and should be removed.
279    */
280   DiaSvgRenderer *renderer = DIA_SVG_RENDERER (self);
281   SvgRenderer *svg_renderer = SVG_RENDERER (self);
282   int n_children = 0;
283   xmlNodePtr child, group;
284 
285   g_queue_push_tail (svg_renderer->parents, renderer->root);
286   /* modifying the root pointer so everything below us gets into the new node */
287   renderer->root = group = xmlNewNode (renderer->svg_name_space, (const xmlChar *)"g");
288 
289   object->ops->draw(object, DIA_RENDERER (renderer));
290 
291   /* no easy way to count? */
292   child = renderer->root->children;
293   while (child != NULL) {
294     child = child->next;
295     ++n_children;
296   }
297   renderer->root = g_queue_pop_tail (svg_renderer->parents);
298   /* if there is only one element added to the group node unpack it again  */
299   if (1 == n_children) {
300     xmlAddChild (renderer->root, group->children);
301     xmlUnlinkNode (group); /* dont free the children */
302     xmlFree (group);
303   } else {
304     xmlAddChild (renderer->root, group);
305   }
306 }
307 
308 static void
draw_rounded_rect(DiaRenderer * self,Point * ul_corner,Point * lr_corner,Color * colour,real rounding)309 draw_rounded_rect(DiaRenderer *self,
310                   Point *ul_corner, Point *lr_corner,
311                   Color *colour, real rounding)
312 {
313   DiaSvgRenderer *renderer = DIA_SVG_RENDERER (self);
314   xmlNodePtr node;
315   gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
316 
317   node = xmlNewChild(renderer->root, NULL, (const xmlChar *)"rect", NULL);
318 
319   xmlSetProp(node, (const xmlChar *)"style",
320              (const xmlChar *) DIA_SVG_RENDERER_GET_CLASS(self)->get_draw_style(renderer, colour));
321 
322   g_ascii_formatd(buf, sizeof(buf), "%g", ul_corner->x * renderer->scale);
323   xmlSetProp(node, (const xmlChar *)"x", (xmlChar *) buf);
324   g_ascii_formatd(buf, sizeof(buf), "%g", ul_corner->y * renderer->scale);
325   xmlSetProp(node, (const xmlChar *)"y", (xmlChar *) buf);
326   g_ascii_formatd(buf, sizeof(buf), "%g", (lr_corner->x - ul_corner->x) * renderer->scale);
327   xmlSetProp(node, (const xmlChar *)"width", (xmlChar *) buf);
328   g_ascii_formatd(buf, sizeof(buf), "%g", (lr_corner->y - ul_corner->y) * renderer->scale);
329   xmlSetProp(node, (const xmlChar *)"height", (xmlChar *) buf);
330   g_ascii_formatd(buf, sizeof(buf),"%g", (rounding * renderer->scale));
331   xmlSetProp(node, (const xmlChar *)"rx", (xmlChar *) buf);
332   xmlSetProp(node, (const xmlChar *)"ry", (xmlChar *) buf);
333 }
334 
335 static void
fill_rounded_rect(DiaRenderer * self,Point * ul_corner,Point * lr_corner,Color * colour,real rounding)336 fill_rounded_rect(DiaRenderer *self,
337                   Point *ul_corner, Point *lr_corner,
338                   Color *colour, real rounding)
339 {
340   DiaSvgRenderer *renderer = DIA_SVG_RENDERER (self);
341   xmlNodePtr node;
342   gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
343 
344   node = xmlNewChild(renderer->root, NULL, (const xmlChar *)"rect", NULL);
345 
346   xmlSetProp(node, (const xmlChar *)"style",
347              (const xmlChar *) DIA_SVG_RENDERER_GET_CLASS(self)->get_fill_style(renderer, colour));
348 
349   g_ascii_formatd(buf, sizeof(buf), "%g", (ul_corner->x * renderer->scale));
350   xmlSetProp(node, (const xmlChar *)"x", (xmlChar *) buf);
351   g_ascii_formatd(buf, sizeof(buf), "%g", (ul_corner->y * renderer->scale));
352   xmlSetProp(node, (const xmlChar *)"y", (xmlChar *) buf);
353   g_ascii_formatd(buf, sizeof(buf), "%g", (lr_corner->x - ul_corner->x) * renderer->scale);
354   xmlSetProp(node, (const xmlChar *)"width", (xmlChar *) buf);
355   g_ascii_formatd(buf, sizeof(buf), "%g", (lr_corner->y - ul_corner->y) * renderer->scale);
356   xmlSetProp(node, (const xmlChar *)"height", (xmlChar *) buf);
357   g_ascii_formatd(buf, sizeof(buf),"%g", (rounding * renderer->scale));
358   xmlSetProp(node, (const xmlChar *)"rx", (xmlChar *) buf);
359   xmlSetProp(node, (const xmlChar *)"ry", (xmlChar *) buf);
360 }
361 
362 #define dia_svg_dtostr(buf,d) \
363   g_ascii_formatd(buf,sizeof(buf),"%g",(d)*renderer->scale)
364 
365 static void
node_set_text_style(xmlNodePtr node,DiaSvgRenderer * renderer,const DiaFont * font,real font_height,Alignment alignment,Color * colour)366 node_set_text_style (xmlNodePtr      node,
367                      DiaSvgRenderer *renderer,
368 		     const DiaFont  *font,
369 		     real            font_height,
370                      Alignment       alignment,
371 		     Color          *colour)
372 {
373   char *style, *tmp;
374   real saved_width;
375   gchar d_buf[G_ASCII_DTOSTR_BUF_SIZE];
376   DiaSvgRendererClass *svg_renderer_class = DIA_SVG_RENDERER_GET_CLASS (renderer);
377   /* SVG font-size is the (line-) height, from SVG Spec:
378    * ... property refers to the size of the font from baseline to baseline when multiple lines of text are set ...
379   so we should be able to use font_height directly instead of:
380    */
381   real font_size = dia_font_get_size (font) * (font_height / dia_font_get_height (font));
382   /* ... but at least Inkscape and Firefox would produce the wrong font-size */
383   const gchar *family = dia_font_get_family(font);
384 
385   saved_width = renderer->linewidth;
386   renderer->linewidth = 0.001;
387   style = (char*)svg_renderer_class->get_fill_style(renderer, colour);
388   /* return value must not be freed */
389   renderer->linewidth = saved_width;
390   /* This is going to break for non-LTR texts, as SVG thinks 'start' is
391    * 'right' for those.
392    */
393   switch (alignment) {
394   case ALIGN_LEFT:
395     style = g_strconcat(style, ";text-anchor:start", NULL);
396     break;
397   case ALIGN_CENTER:
398     style = g_strconcat(style, ";text-anchor:middle", NULL);
399     break;
400   case ALIGN_RIGHT:
401     style = g_strconcat(style, ";text-anchor:end", NULL);
402     break;
403   }
404 #if 0 /* would need a unit according to https://bugzilla.mozilla.org/show_bug.cgi?id=707071#c4 */
405   tmp = g_strdup_printf("%s;font-size:%s", style,
406 			dia_svg_dtostr(d_buf, font_size) );
407   g_free (style);
408   style = tmp;
409 #else
410   /* font-size as attribute can work like the other length w/o unit */
411   dia_svg_dtostr(d_buf, font_size);
412   xmlSetProp(node, (const xmlChar *)"font-size", (xmlChar *) d_buf);
413 #endif
414 
415   if (font) {
416      tmp = g_strdup_printf("%s;font-family:%s;font-style:%s;"
417                            "font-weight:%s",style,
418                            strcmp(family, "sans") == 0 ? "sans-serif" : family,
419                            dia_font_get_slant_string(font),
420                            dia_font_get_weight_string(font));
421      g_free(style);
422      style = tmp;
423   }
424 
425   /* have to do something about fonts here ... */
426 
427   xmlSetProp(node, (xmlChar *)"style", (xmlChar *)style);
428   g_free(style);
429 }
430 
431 static void
draw_string(DiaRenderer * self,const char * text,Point * pos,Alignment alignment,Color * colour)432 draw_string(DiaRenderer *self,
433 	    const char *text,
434 	    Point *pos, Alignment alignment,
435 	    Color *colour)
436 {
437   DiaSvgRenderer *renderer = DIA_SVG_RENDERER (self);
438   xmlNodePtr node;
439   gchar d_buf[G_ASCII_DTOSTR_BUF_SIZE];
440 
441   node = xmlNewChild(renderer->root, renderer->svg_name_space, (xmlChar *)"text", (xmlChar *)text);
442 
443   node_set_text_style(node, renderer, self->font, self->font_height, alignment, colour);
444 
445   dia_svg_dtostr(d_buf, pos->x);
446   xmlSetProp(node, (xmlChar *)"x", (xmlChar *)d_buf);
447   dia_svg_dtostr(d_buf, pos->y);
448   xmlSetProp(node, (xmlChar *)"y", (xmlChar *)d_buf);
449 }
450 
451 static void
draw_text_line(DiaRenderer * self,TextLine * text_line,Point * pos,Alignment alignment,Color * colour)452 draw_text_line(DiaRenderer *self, TextLine *text_line,
453 	       Point *pos, Alignment alignment, Color *colour)
454 {
455   DiaSvgRenderer *renderer = DIA_SVG_RENDERER (self);
456   xmlNodePtr node;
457   DiaFont *font = text_line_get_font(text_line); /* no reference? */
458   real font_height = text_line_get_height(text_line);
459   gchar d_buf[G_ASCII_DTOSTR_BUF_SIZE];
460 
461   node = xmlNewChild(renderer->root, renderer->svg_name_space, (const xmlChar *)"text",
462 		     (xmlChar *) text_line_get_string(text_line));
463 
464   /* not using the renderers font but the textlines */
465   node_set_text_style(node, renderer, font, font_height, alignment, colour);
466 
467   dia_svg_dtostr(d_buf, pos->x);
468   xmlSetProp(node, (const xmlChar *)"x", (xmlChar *) d_buf);
469   dia_svg_dtostr(d_buf, pos->y);
470   xmlSetProp(node, (const xmlChar *)"y", (xmlChar *) d_buf);
471   dia_svg_dtostr(d_buf, text_line_get_width(text_line));
472   xmlSetProp(node, (const xmlChar*)"textLength", (xmlChar *) d_buf);
473 }
474 
475 static void
draw_text(DiaRenderer * self,Text * text)476 draw_text (DiaRenderer *self, Text *text)
477 {
478   DiaSvgRenderer *renderer = DIA_SVG_RENDERER (self);
479   Point pos = text->position;
480   int i;
481   xmlNodePtr node_text, node_tspan;
482   gchar d_buf[G_ASCII_DTOSTR_BUF_SIZE];
483 
484   node_text = xmlNewChild(renderer->root, renderer->svg_name_space, (const xmlChar *)"text", NULL);
485   /* text 'global' properties  */
486   node_set_text_style(node_text, renderer, text->font, text->height, text->alignment, &text->color);
487   dia_svg_dtostr(d_buf, pos.x);
488   xmlSetProp(node_text, (const xmlChar *)"x", (xmlChar *) d_buf);
489   dia_svg_dtostr(d_buf, pos.y);
490   xmlSetProp(node_text, (const xmlChar *)"y", (xmlChar *) d_buf);
491 
492   pos = text->position;
493   for (i=0;i<text->numlines;i++) {
494     TextLine *text_line = text->lines[i];
495 
496     node_tspan = xmlNewTextChild(node_text, renderer->svg_name_space, (const xmlChar *)"tspan",
497                                  (const xmlChar *)text_line_get_string(text_line));
498     dia_svg_dtostr(d_buf, pos.x);
499     xmlSetProp(node_tspan, (const xmlChar *)"x", (xmlChar *) d_buf);
500     dia_svg_dtostr(d_buf, pos.y);
501     xmlSetProp(node_tspan, (const xmlChar *)"y", (xmlChar *) d_buf);
502 
503     pos.y += text->height;
504   }
505 }
506 
507 static void
export_svg(DiagramData * data,const gchar * filename,const gchar * diafilename,void * user_data)508 export_svg(DiagramData *data, const gchar *filename,
509            const gchar *diafilename, void* user_data)
510 {
511   DiaSvgRenderer *renderer;
512 
513   if ((renderer = new_svg_renderer(data, filename))) {
514     data_render(data, DIA_RENDERER(renderer), NULL, NULL, NULL);
515     g_object_unref(renderer);
516   }
517 }
518 
519 static const gchar *extensions[] = { "svg", NULL };
520 DiaExportFilter svg_export_filter = {
521   N_("Scalable Vector Graphics"),
522   extensions,
523   export_svg
524 };
525