# PyDia SVG+CSS Renderer # Copyright (c) 2011 Max Wahler # # This SVG(Z) renderer is based on the PyDia SVG renderer by Hans Breuer. # # Instead of writing all attributes inline, a separate CSS file is generated # which contain the color/stroke attributes of objects that have an id meta # info field. # # With this, it's possible to style objects differently after the diagram # was exported. This is useful for example when the SVG is embedded in a # HTML page and single elements should be highlighted on actions. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import sys, string, dia class SvgCssRenderer : def __init__ (self) : self.f = None self.line_width = 0.1 self.line_caps = 0 self.line_join = 0 self.line_style = 0 self.dash_length = 0 self.scale = 20.0 self.css = None self.current_id = None self.current_attributes = None def _open(self, filename) : self.f = open(filename, "w") self.css = open(filename + ".css", "w") def begin_render (self, data, filename) : self._open (filename) r = data.extents xofs = - r[0] yofs = - r[1] self.f.write(''' ''' % (r.right - r.left, r.bottom - r.top, r[0] * self.scale, r[1] * self.scale, (r[2] - r[0]) * self.scale, (r[3] - r[1]) * self.scale)) #self.f.write("\n" % (str(data.extents))) #self.f.write("\n" % (data.active_layer.name)) def end_render (self) : self.f.write('') self.f.close() self.css.close() def draw_object (self, object, matrix) : self.f.write('\n') # don't forget to render the object object.draw (self) self.f.write("\n\n"); # write css content if self.current_id is not None : self.css.write('.%s {\n' % self.current_id) for key in self.current_attributes.keys() : self.css.write('\t%s: %s;\n' % (key, self.current_attributes[key])) self.css.write('}\n\n') def set_linewidth (self, width) : if width < 0.001 : # zero line width is invisble ? self.line_width = 0.001 else : self.line_width = width def set_linecaps (self, mode) : self.line_caps = mode def set_linejoin (self, mode) : self.line_join = mode def set_linestyle (self, style) : self.line_style = style def set_dashlength (self, length) : self.dash_length = length def set_fillstyle (self, style) : # currently only 'solid' so not used anywhere else self.fill_style = style def set_font (self, font, size) : self.font = font self.font_size = size def draw_rounded_rect (self, rect, color, rounding) : self.f.write('\n' \ % ( rect.left * self.scale, rect.top * self.scale, (rect.right - rect.left) * self.scale, (rect.bottom - rect.top) * self.scale, self._color_attr('stroke', color), self.line_width * self.scale, self._stroke_style(), rounding * self.scale, rounding * self.scale)) def fill_rounded_rect (self, rect, color, rounding) : self.f.write('\n' \ % ( rect.left * self.scale, rect.top * self.scale, (rect.right - rect.left) * self.scale, (rect.bottom - rect.top) * self.scale, self._color_attr('fill', color), rounding * self.scale, rounding * self.scale)) def draw_line (self, start, end, color) : self.f.write('\n' \ % (start.x * self.scale, start.y * self.scale, end.x * self.scale, end.y * self.scale, self._color_attr('stroke', color), self.line_width * self.scale, self._stroke_style())) def draw_polyline (self, points, color) : self.f.write('\n') def draw_polygon (self, points, color) : self.f.write('\n') def fill_polygon (self, points, color) : self.f.write('\n') def draw_rect (self, rect, color) : self.f.write('\n' \ % ( rect.left * self.scale, rect.top * self.scale, (rect.right - rect.left) * self.scale, (rect.bottom - rect.top) * self.scale, self._color_attr('stroke', color), self.line_width * self.scale, self._stroke_style())) def fill_rect (self, rect, color) : self.f.write('\n' \ % ( rect.left * self.scale, rect.top * self.scale, (rect.right - rect.left) * self.scale, (rect.bottom - rect.top) * self.scale, self._color_attr('fill', color))) def _arc (self, center, width, height, angle1, angle2, color, fill=None) : # not in the renderer interface import math mPi180 = math.pi / 180.0 rx = width / 2.0 ry = height / 2.0 sx = center.x + rx * math.cos(mPi180 * angle1) sy = center.y - ry * math.sin(mPi180 * angle1) ex = center.x + rx * math.cos(mPi180 * angle2) ey = center.y - ry * math.sin(mPi180 * angle2) largearc = (angle2 - angle1 >= 180) sweep = 0 # always draw in negative direction if not fill : self.f.write('\n') def draw_arc (self, center, width, height, angle1, angle2, color) : self._arc(center, width, height, angle1, angle2, color) def fill_arc (self, center, width, height, angle1, angle2, color) : self._arc(center, width, height, angle1, angle2, color, 1) def draw_ellipse (self, center, width, height, color) : self.f.write('\n' \ % (center.x * self.scale, center.y * self.scale, (width / 2) * self.scale, (height / 2) * self.scale, self._color_attr('stroke', color), self.line_width * self.scale, self._stroke_style())) def fill_ellipse (self, center, width, height, color) : self.f.write('\n' \ % (center.x * self.scale, center.y * self.scale, (width / 2) * self.scale, (height / 2) * self.scale, self._color_attr('fill', color))) def draw_bezier (self, bezpoints, color) : self.f.write('\n') def fill_bezier (self, bezpoints, color) : self.f.write('\n') def draw_string (self, text, pos, alignment, color) : if len(text) < 1 : return # shouldn'this be done at the higher level talign = ('start', 'middle', 'end') [alignment] fstyle = ('normal', 'italic', 'oblique') [self.font.style & 0x03] fweight = (400, 200, 300, 500, 600, 700, 800, 900) [(self.font.style >> 4) & 0x7] self.f.write('\n' \ % (pos.x * self.scale, pos.y * self.scale, self._color_attr('fill', color), talign, self.font_size * self.scale, self.font.family, fstyle, fweight)) # avoid writing XML special characters (ampersand must be first to not break the rest) for rep in [('&', '&'), ('<', '<'), ('>', '>'), ('"', '"'), ("'", ''')] : text = string.replace (text, rep[0], rep[1]) self.f.write(text) self.f.write('\n') def draw_image (self, point, width, height, image) : #FIXME : do something better than absolute pathes ? self.f.write('\n' \ % (point.x * self.scale, point.y * self.scale, width * self.scale, height * self.scale, image.uri)) # Helpers, not in the DiaRenderer interface def _color_attr(self, attr_name, color) : if self.current_id is not None : insertion = '' self.current_attributes[attr_name] = self._rgb(color) else : insertion = '%s="%s"' % (attr_name, self._rgb(color)) return insertion def _rgb(self, color) : # given a dia color convert to svg color string rgb = "#%02X%02X%02X" % (int(255 * color.red), int(color.green * 255), int(color.blue * 255)) return rgb def _stroke_style(self) : # return the current line style as svg string dashlen =self.dash_length # dashlen/style interpretation like the DiaGdkRenderer dotlen = dashlen * 0.1 caps = self.line_caps join = self.line_join style = self.line_style st = "" if style == 0 : # LINESTYLE_SOLID pass elif style == 1 : # DASHED st = 'stroke-dasharray="%.2f,%.2f"' % (dashlen * self.scale, dashlen * self.scale) elif style == 2 : # DASH_DOT, gaplen = (dashlen - dotlen) / 2.0 st = 'stroke-dasharray="%.2f,%.2f,%.2f,%.2f"' % (dashlen * self.scale, gaplen * self.scale, dotlen * self.scale, gaplen * self.scale) elif style == 3 : # DASH_DOT_DOT, gaplen = (dashlen - dotlen) / 3.0 st = 'stroke-dasharray="%.2f,%.2f,%.2f,%.2f,%.2f,%.2f"' % (dashlen * self.scale, gaplen * self.scale, dotlen * self.scale, gaplen * self.scale, dotlen * self.scale, gaplen * self.scale) elif style == 4 : # DOTTED st = 'stroke-dasharray="%.2f,%.2f"' % (dotlen * self.scale, dotlen * self.scale) if join == 0 : # MITER pass # st = st + ' stroke-linejoin="bevel"' elif join == 1 : # ROUND st = st + ' stroke-linejoin="round"' elif join == 2 : # BEVEL st = st + ' stroke-linejoin="bevel"' if caps == 0 : # BUTT pass # default stroke-linecap="butt" elif caps == 1 : # ROUND st = st + ' stroke-linecap="round"' elif caps == 2 : # PROJECTING st = st + ' stroke-linecap="square"' # is this the same ? return st class SvgzCssRenderer(SvgCssRenderer) : def _open(self, filename) : # There is some (here) not wanted behaviour in gzip.open/GzipFile : # the filename with path is not only used to adress the file but also # completely stored in the file itself. Correct it here. import os, os.path, gzip path, name = os.path.split(filename) os.chdir(path) self.f = gzip.open (name, "wb") # dia-python keeps a reference to the renderer class and uses it on demand dia.register_export ("SVG+CSS plain", "svg", SvgCssRenderer()) dia.register_export ("SVG+CSS compressed", "svgz", SvgzCssRenderer())