README.md: mention also the tetraflexagon example
[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 try:
25     from .diagram import Diagram
26 except ValueError:
27     from diagram import Diagram
28
29
30 class InkscapeDrawing(svgwrite.Drawing):
31     """An svgwrite.Drawing subclass which supports Inkscape layers"""
32     def __init__(self, *args, **kwargs):
33         super(InkscapeDrawing, self).__init__(*args, **kwargs)
34
35         inkscape_attributes = {
36             'xmlns:inkscape': SVGAttribute('xmlns:inkscape',
37                                            anim=False,
38                                            types=[],
39                                            const=frozenset(['http://www.inkscape.org/namespaces/inkscape'])),
40             'inkscape:groupmode': SVGAttribute('inkscape:groupmode',
41                                                anim=False,
42                                                types=[],
43                                                const=frozenset(['layer'])),
44             'inkscape:label': SVGAttribute('inkscape:label',
45                                            anim=False,
46                                            types=frozenset(['string']),
47                                            const=[])
48         }
49
50         self.validator.attributes.update(inkscape_attributes)
51
52         elements = self.validator.elements
53
54         svg_attributes = set(elements['svg'].valid_attributes)
55         svg_attributes.add('xmlns:inkscape')
56         elements['svg'].valid_attributes = frozenset(svg_attributes)
57
58         g_attributes = set(elements['g'].valid_attributes)
59         g_attributes.add('inkscape:groupmode')
60         g_attributes.add('inkscape:label')
61         elements['g'].valid_attributes = frozenset(g_attributes)
62
63         self['xmlns:inkscape'] = 'http://www.inkscape.org/namespaces/inkscape'
64
65     def layer(self, **kwargs):
66         """Create an inkscape layer.
67
68         An optional 'label' keyword argument can be passed to set a user
69         friendly name for the layer."""
70         label = kwargs.pop('label', None)
71
72         new_layer = self.g(**kwargs)
73         new_layer['inkscape:groupmode'] = 'layer'
74
75         if label:
76             new_layer['inkscape:label'] = label
77
78         return new_layer
79
80
81 class SvgwriteDiagram(Diagram):
82     def __init__(self, width, height, **kwargs):
83         super(SvgwriteDiagram, self).__init__(width, height, **kwargs)
84
85         self.svg = InkscapeDrawing(None, profile='full', size=(str(width) + "px", str(height) + "px"))
86         self.active_group = self.svg
87
88     def clear(self):
89         # Reset the SVG object
90         self.svg.elements = []
91         self.svg.add(self.svg.defs)
92
93         rect = self.svg.rect((0, 0), ('100%', '100%'))
94         self._fill(rect, self.background)
95         self.svg.add(rect)
96
97     def save_svg(self, filename):
98         self.svg.saveas(filename)
99
100     def add(self, element):
101         self.active_group.add(element)
102
103     def color_to_rgba(self, color):
104         color = super(SvgwriteDiagram, self).color_to_rgba(color)
105
106         return color[0] * 255, color[1] * 255, color[2] * 255, color[3]
107
108     def _fill(self, element, fill_color):
109         if fill_color:
110             r, g, b, a = self.color_to_rgba(fill_color)
111             fill_color = svgwrite.utils.rgb(r, g, b, mode='RGB')
112             element['fill'] = fill_color
113             element['fill-opacity'] = a
114         else:
115             element['fill'] = 'none'
116
117     def _stroke(self, element, stroke_color):
118         if stroke_color:
119             r, g, b, a = self.color_to_rgba(stroke_color)
120             stroke_color = svgwrite.utils.rgb(r, g, b, mode='RGB')
121             element['stroke'] = stroke_color
122             element['stroke-opacity'] = a
123             element['stroke-linejoin'] = 'round'
124             element['stroke-width'] = self.stroke_width
125         else:
126             element['stroke'] = 'none'
127
128     def draw_polygon_by_verts(self, verts,
129                               stroke_color=(0, 0, 0),
130                               fill_color=None):
131         polygon = self.svg.polygon(verts)
132
133         self._fill(polygon, fill_color)
134         self._stroke(polygon, stroke_color)
135
136         self.add(polygon)
137
138     def draw_star_by_verts(self, cx, cy, verts, stroke_color=(0, 0, 0)):
139         for v in verts:
140             line = self.svg.line((cx, cy), v)
141             self._stroke(line, stroke_color)
142             self.add(line)
143
144     def draw_circle(self, cx, cy, radius=10.0,
145                     stroke_color=None,
146                     fill_color=(0, 0, 0, 0.5)):
147         circle = self.svg.circle((cx, cy), radius)
148
149         self._fill(circle, fill_color)
150         self._stroke(circle, stroke_color)
151
152         self.add(circle)
153
154     def draw_line(self, x1, y1, x2, y2, stroke_color=(0, 0, 0, 1)):
155         line = self.svg.line((x1, y1), (x1, y2))
156         self._stroke(line, stroke_color)
157
158         self.add(line)
159
160     def draw_rect(self, x, y, width, height, theta=0,
161                   stroke_color=None,
162                   fill_color=(1, 1, 1, 0.8)):
163         rect = self.svg.rect((x, y), (width, height))
164
165         rect['transform'] = 'rotate(%f, %f, %f)' % (degrees(theta), x, y)
166
167         self._fill(rect, fill_color)
168         self._stroke(rect, stroke_color)
169
170         self.add(rect)
171
172     def draw_centered_text(self, cx, cy, text, theta=0.0,
173                            color=(0, 0, 0),
174                            align_baseline=False,
175                            bb_stroke_color=None,
176                            bb_fill_color=None):
177
178         # Using font_size to calculate dy is not optimal as the font _height_ may
179         # be different from the font_size, but it's better than nothing.
180         text_element = self.svg.text(text, x=[cx], y=[cy], dy=[self.font_size / 2.])
181         self._fill(text_element, color)
182         text_element['font-size'] = self.font_size
183         text_element['text-anchor'] = 'middle'
184         text_element['transform'] = 'rotate(%f, %f, %f)' % (degrees(theta), cx, cy)
185         self.add(text_element)
186
187         if align_baseline:
188             warnings.warn("The align_baseline option has not been implemented yet.")
189
190         if bb_stroke_color or bb_fill_color:
191             warnings.warn("Drawing the bounding box has not been implemented yet.")
192
193         # XXX: this is just a very quick and dirty estimate to advance the text
194         return self.font_size * len(text) / 2
195
196
197 def test():
198     diagram = SvgwriteDiagram(400, 400)
199
200     Diagram.test(diagram)
201
202     diagram.save_svg('svgwrite_diagram_test.svg')
203
204
205 if __name__ == "__main__":
206     test()