3 # A Diagram abstraction based on svgwrite
 
   5 # Copyright (C) 2018  Antonio Ospite <ao2@ao2.it>
 
   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 3 of the License, or
 
  10 # (at your option) any later version.
 
  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.
 
  17 # You should have received a copy of the GNU General Public License
 
  18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
  21 from math import degrees
 
  23 from svgwrite.data.types import SVGAttribute
 
  24 from .diagram import Diagram
 
  27 class InkscapeDrawing(svgwrite.Drawing):
 
  28     """An svgwrite.Drawing subclass which supports Inkscape layers"""
 
  29     def __init__(self, *args, **kwargs):
 
  30         super(InkscapeDrawing, self).__init__(*args, **kwargs)
 
  32         inkscape_attributes = {
 
  33             'xmlns:inkscape': SVGAttribute('xmlns:inkscape',
 
  36                                            const=frozenset(['http://www.inkscape.org/namespaces/inkscape'])),
 
  37             'inkscape:groupmode': SVGAttribute('inkscape:groupmode',
 
  40                                                const=frozenset(['layer'])),
 
  41             'inkscape:label': SVGAttribute('inkscape:label',
 
  43                                            types=frozenset(['string']),
 
  47         self.validator.attributes.update(inkscape_attributes)
 
  49         elements = self.validator.elements
 
  51         svg_attributes = set(elements['svg'].valid_attributes)
 
  52         svg_attributes.add('xmlns:inkscape')
 
  53         elements['svg'].valid_attributes = frozenset(svg_attributes)
 
  55         g_attributes = set(elements['g'].valid_attributes)
 
  56         g_attributes.add('inkscape:groupmode')
 
  57         g_attributes.add('inkscape:label')
 
  58         elements['g'].valid_attributes = frozenset(g_attributes)
 
  60         self['xmlns:inkscape'] = 'http://www.inkscape.org/namespaces/inkscape'
 
  62     def layer(self, **kwargs):
 
  63         """Create an inkscape layer.
 
  65         An optional 'label' keyword argument can be passed to set a user
 
  66         friendly name for the layer."""
 
  67         label = kwargs.pop('label', None)
 
  69         new_layer = self.g(**kwargs)
 
  70         new_layer['inkscape:groupmode'] = 'layer'
 
  73             new_layer['inkscape:label'] = label
 
  78 class SvgwriteDiagram(Diagram):
 
  79     def __init__(self, width, height, **kwargs):
 
  80         super(SvgwriteDiagram, self).__init__(width, height, **kwargs)
 
  82         self.svg = InkscapeDrawing(None, profile='full', size=(str(width) + "px", str(height) + "px"))
 
  83         self.active_group = self.svg
 
  86         # Reset the SVG object
 
  87         self.svg.elements = []
 
  88         self.svg.add(self.svg.defs)
 
  90         rect = self.svg.rect((0, 0), ('100%', '100%'))
 
  91         self._fill(rect, self.background)
 
  94     def save_svg(self, filename):
 
  95         self.svg.saveas(filename)
 
  97     def add(self, element):
 
  98         self.active_group.add(element)
 
 100     def color_to_rgba(self, color):
 
 101         color = super(SvgwriteDiagram, self).color_to_rgba(color)
 
 103         return color[0] * 255, color[1] * 255, color[2] * 255, color[3]
 
 105     def _fill(self, element, fill_color):
 
 107             r, g, b, a = self.color_to_rgba(fill_color)
 
 108             fill_color = svgwrite.utils.rgb(r, g, b, mode='RGB')
 
 109             element['fill'] = fill_color
 
 110             element['fill-opacity'] = a
 
 112             element['fill'] = 'none'
 
 114     def _stroke(self, element, stroke_color):
 
 116             r, g, b, a = self.color_to_rgba(stroke_color)
 
 117             stroke_color = svgwrite.utils.rgb(r, g, b, mode='RGB')
 
 118             element['stroke'] = stroke_color
 
 119             element['stroke-opacity'] = a
 
 120             element['stroke-linejoin'] = 'round'
 
 121             element['stroke-width'] = self.stroke_width
 
 123             element['stroke'] = 'none'
 
 125     def draw_polygon_by_verts(self, verts,
 
 126                               stroke_color=(0, 0, 0),
 
 128         polygon = self.svg.polygon(verts)
 
 130         self._fill(polygon, fill_color)
 
 131         self._stroke(polygon, stroke_color)
 
 135     def draw_star_by_verts(self, cx, cy, verts, stroke_color=(0, 0, 0)):
 
 137             line = self.svg.line((cx, cy), v)
 
 138             self._stroke(line, stroke_color)
 
 141     def draw_circle(self, cx, cy, radius=10.0,
 
 143                     fill_color=(0, 0, 0, 0.5)):
 
 144         circle = self.svg.circle((cx, cy), radius)
 
 146         self._fill(circle, fill_color)
 
 147         self._stroke(circle, stroke_color)
 
 151     def draw_line(self, x1, y1, x2, y2, stroke_color=(0, 0, 0, 1)):
 
 152         line = self.svg.line((x1, y1), (x1, y2))
 
 153         self._stroke(line, stroke_color)
 
 157     def draw_rect(self, x, y, width, height, theta=0,
 
 159                   fill_color=(1, 1, 1, 0.8)):
 
 160         rect = self.svg.rect((x, y), (width, height))
 
 162         rect['transform'] = 'rotate(%f, %f, %f)' % (degrees(theta), x, y)
 
 164         self._fill(rect, fill_color)
 
 165         self._stroke(rect, stroke_color)
 
 169     def draw_centered_text(self, cx, cy, text, theta=0.0,
 
 171                            align_baseline=False,
 
 172                            bb_stroke_color=None,
 
 175         # Using font_size to calculate dy is not optimal as the font _height_ may
 
 176         # be different from the font_size, but it's better than nothing.
 
 177         text_element = self.svg.text(text, x=[cx], y=[cy], dy=[self.font_size / 2.])
 
 178         self._fill(text_element, color)
 
 179         text_element['font-size'] = self.font_size
 
 180         text_element['text-anchor'] = 'middle'
 
 181         text_element['transform'] = 'rotate(%f, %f, %f)' % (degrees(theta), cx, cy)
 
 182         self.add(text_element)
 
 185             warnings.warn("The align_baseline option has not been implemented yet.")
 
 187         if bb_stroke_color or bb_fill_color:
 
 188             warnings.warn("Drawing the bounding box has not been implemented yet.")