bf945413459abb52b428e61c18b6ea97437f8f44
[flexagon-toolkit.git] / src / diagram / svgwrite_diagram.py
1 #!/usr/bin/env python
2 #
3 # A Diagram abstraction based on svgwrite
4 #
5 # Copyright (C) 2018  Antonio Ospite <ao2@ao2.it>
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 3 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, see <http://www.gnu.org/licenses/>.
19
20 import warnings
21 from math import degrees
22 import svgwrite
23 from svgwrite.data.types import SVGAttribute
24 from .diagram import Diagram
25
26
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)
31
32         inkscape_attributes = {
33             'xmlns:inkscape': SVGAttribute('xmlns:inkscape',
34                                            anim=False,
35                                            types=[],
36                                            const=frozenset(['http://www.inkscape.org/namespaces/inkscape'])),
37             'inkscape:groupmode': SVGAttribute('inkscape:groupmode',
38                                                anim=False,
39                                                types=[],
40                                                const=frozenset(['layer'])),
41             'inkscape:label': SVGAttribute('inkscape:label',
42                                            anim=False,
43                                            types=frozenset(['string']),
44                                            const=[])
45         }
46
47         self.validator.attributes.update(inkscape_attributes)
48
49         elements = self.validator.elements
50
51         svg_attributes = set(elements['svg'].valid_attributes)
52         svg_attributes.add('xmlns:inkscape')
53         elements['svg'].valid_attributes = frozenset(svg_attributes)
54
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)
59
60         self['xmlns:inkscape'] = 'http://www.inkscape.org/namespaces/inkscape'
61
62     def layer(self, **kwargs):
63         """Create an inkscape layer.
64
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)
68
69         new_layer = self.g(**kwargs)
70         new_layer['inkscape:groupmode'] = 'layer'
71
72         if label:
73             new_layer['inkscape:label'] = label
74
75         return new_layer
76
77
78 class SvgwriteDiagram(Diagram):
79     def __init__(self, width, height, **kwargs):
80         super(SvgwriteDiagram, self).__init__(width, height, **kwargs)
81
82         self.svg = InkscapeDrawing(None, profile='full', size=(str(width) + "px", str(height) + "px"))
83         self.active_group = self.svg
84
85     def save_svg(self, filename):
86         self.svg.saveas(filename)
87
88     def add(self, element):
89         self.active_group.add(element)
90
91     def color_to_rgba(self, color):
92         color = super(SvgwriteDiagram, self).color_to_rgba(color)
93
94         return color[0] * 255, color[1] * 255, color[2] * 255, color[3]
95
96     def _fill(self, element, fill_color):
97         if fill_color:
98             r, g, b, a = self.color_to_rgba(fill_color)
99             fill_color = svgwrite.utils.rgb(r, g, b, mode='RGB')
100             element['fill'] = fill_color
101             element['fill-opacity'] = a
102         else:
103             element['fill'] = 'none'
104
105     def _stroke(self, element, stroke_color):
106         if stroke_color:
107             r, g, b, a = self.color_to_rgba(stroke_color)
108             stroke_color = svgwrite.utils.rgb(r, g, b, mode='RGB')
109             element['stroke'] = stroke_color
110             element['stroke-opacity'] = a
111             element['stroke-linejoin'] = 'round'
112         else:
113             element['stroke'] = 'none'
114
115     def draw_polygon_by_verts(self, verts,
116                               stroke_color=(0, 0, 0),
117                               fill_color=None):
118         polygon = self.svg.polygon(verts, stroke_width=self.stroke_width)
119
120         self._fill(polygon, fill_color)
121         self._stroke(polygon, stroke_color)
122
123         self.add(polygon)
124
125     def draw_star_by_verts(self, cx, cy, verts, stroke_color=(0, 0, 0)):
126         for v in verts:
127             line = self.svg.line((cx, cy), v, stroke_width=self.stroke_width)
128             self._stroke(line, stroke_color)
129             self.add(line)
130
131     def draw_centered_text(self, cx, cy, text, theta=0.0,
132                            color=(0, 0, 0),
133                            align_baseline=False,
134                            bb_stroke_color=None,
135                            bb_fill_color=None):
136
137         # Using font_size to calculate dy is not optimal as the font _height_ may
138         # be different from the font_size, but it's better than nothing.
139         text_element = self.svg.text(text, x=[cx], y=[cy], dy=[self.font_size / 2.])
140         self._fill(text_element, color)
141         text_element['font-size'] = self.font_size
142         text_element['text-anchor'] = 'middle'
143         text_element['transform'] = 'rotate(%f, %f, %f)' % (degrees(theta), cx, cy)
144         self.add(text_element)
145
146         if align_baseline:
147             warnings.warn("The align_baseline option has not been implemented yet.")
148
149         if bb_stroke_color or bb_fill_color:
150             warnings.warn("Drawing the bounding box has not been implemented yet.")