1
2import math
3import os.path
4import base64
5import warnings
6
7# TODO: Support drawing ellipses without manually using Path
8
9
10class DrawingElement:
11 ''' Base class for drawing elements
12
13 Subclasses must implement writeSvgElement '''
14 def writeSvgElement(self, outputFile):
15 raise NotImplementedError('Abstract base class')
16 def __eq__(self, other):
17 return self is other
18
19class DrawingBasicElement(DrawingElement):
20 ''' Base class for SVG drawing elements that are a single node with no
21 child nodes '''
22 TAG_NAME = '_'
23 def __init__(self, **args):
24 self.args = args
25 def writeSvgElement(self, outputFile):
26 outputFile.write('<')
27 outputFile.write(self.TAG_NAME)
28 outputFile.write(' ')
29 for k, v in self.args.items():
30 k = k.replace('__', ':')
31 k = k.replace('_', '-')
32 outputFile.write('{}="{}" '.format(k,v))
33 outputFile.write('/>')
34 def __eq__(self, other):
35 if isinstance(other, type(self)):
36 return (self.tagName == other.tagName and
37 self.args == other.args)
38 return False
39
40class NoElement(DrawingElement):
41 ''' A drawing element that has no effect '''
42 def __init__(self): pass
43 def writeSvgElement(self, outputFile):
44 pass
45 def __eq__(self, other):
46 if isinstance(other, type(self)):
47 return True
48 return False
49
50class Image(DrawingBasicElement):
51 ''' A linked or embedded raster image '''
52 TAG_NAME = 'image'
53 MIME_MAP = {
54 '.bm': 'image/bmp',
55 '.bmp': 'image/bmp',
56 '.gif': 'image/gif',
57 '.jpeg':'image/jpeg',
58 '.jpg': 'image/jpeg',
59 '.png': 'image/png',
60 '.tif': 'image/tiff',
61 '.tiff':'image/tiff',
62 '.pdf': 'application/pdf',
63 '.txt': 'text/plain',
64 }
65 MIME_DEFAULT = 'image/png'
66 def __init__(self, x, y, width, height, path=None, data=None, embed=False,
67 mimeType=None, **kwargs):
68 ''' Specify either the path or data argument. If path is used and
69 embed is True, the image file is embedded in a data URI. '''
70 if path is None and data is None:
71 raise ValueError('Either path or data arguments must be given')
72 if mimeType is None and path is not None:
73 ext = os.path.splitext(path)[1].lower()
74 if ext in self.MIME_MAP:
75 mimeType = self.MIME_MAP[ext]
76 else:
77 mimeType = self.MIME_DEFAULT
78 warnings.warn('Unknown image file type "{}"'.format(ext), Warning)
79 if mimeType is None:
80 mimeType = self.MIME_DEFAULT
81 warnings.warn('Unspecified image type; assuming png'.format(ext), Warning)
82 if data is not None:
83 embed = True
84 if embed and data is None:
85 with open(path, 'rb') as f:
86 data = f.read()
87 if not embed:
88 uri = path
89 else:
90 encData = base64.b64encode(data).decode()
91 uri = 'data:{};base64,{}'.format(mimeType, encData)
92 super().__init__(x=x, y=-y-height, width=width, height=height,
93 href=uri, **kwargs)
94
95class Rectangle(DrawingBasicElement):
96 ''' A rectangle
97
98 Additional keyword arguments are output as additional arguments to the
99 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
100 TAG_NAME = 'rect'
101 def __init__(self, x, y, width, height, **kwargs):
102 super().__init__(x=x, y=-y-height, width=width, height=height,
103 **kwargs)
104
105class Circle(DrawingBasicElement):
106 ''' A circle
107
108 Additional keyword arguments are output as additional properties to the
109 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
110 TAG_NAME = 'circle'
111 def __init__(self, cx, cy, r, **kwargs):
112 super().__init__(cx=cx, cy=-cy, r=r, **kwargs)
113
114class ArcLine(Circle):
115 ''' An arc
116
117 In most cases, use Arc instead of ArcLine. ArcLine uses the
118 stroke-dasharray SVG property to make the edge of a circle look like
119 an arc.
120
121 Additional keyword arguments are output as additional arguments to the
122 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
123 def __init__(self, cx, cy, r, startDeg, endDeg, **kwargs):
124 if endDeg - startDeg == 360:
125 super().__init__(cx, cy, r, **kwargs)
126 return
127 startDeg, endDeg = (-endDeg) % 360, (-startDeg) % 360
128 arcDeg = (endDeg - startDeg) % 360
129 def arcLen(deg): return math.radians(deg) * r
130 wholeLen = 2 * math.pi * r
131 if endDeg == startDeg:
132 offset = 1
133 dashes = "0 {}".format(wholeLen+2)
134 #elif endDeg >= startDeg:
135 elif True:
136 startLen = arcLen(startDeg)
137 arcLen = arcLen(arcDeg)
138 offLen = wholeLen - arcLen
139 offset = -startLen
140 dashes = "{} {}".format(arcLen, offLen)
141 #else:
142 # firstLen = arcLen(endDeg)
143 # secondLen = arcLen(360-startDeg)
144 # gapLen = wholeLen - firstLen - secondLen
145 # offset = 0
146 # dashes = "{} {} {}".format(firstLen, gapLen, secondLen)
147 super().__init__(cx, cy, r, stroke_dasharray=dashes,
148 stroke_dashoffset=offset, **kwargs)
149
150class Path(DrawingBasicElement):
151 ''' An arbitrary path
152
153 Path Supports building an SVG path by calling instance methods
154 corresponding to path commands.
155
156 Additional keyword arguments are output as additional properties to the
157 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
158 TAG_NAME = 'path'
159 def __init__(self, d='', **kwargs):
160 super().__init__(d=d, **kwargs)
161 def append(self, commandStr, *args):
162 if len(self.args['d']) > 0:
163 commandStr = ' ' + commandStr
164 if len(args) > 0:
165 commandStr = commandStr + ','.join(map(str, args))
166 self.args['d'] += commandStr
167 def M(self, x, y): self.append('M', x, -y)
168 def m(self, dx, dy): self.append('m', dx, -dy)
169 def L(self, x, y): self.append('L', x, -y)
170 def l(self, dx, dy): self.append('l', dx, -dy)
171 def H(self, x, y): self.append('H', x)
172 def h(self, dx): self.append('h', dx)
173 def V(self, y): self.append('V', -y)
174 def v(self, dy): self.append('v', -dy)
175 def Z(self): self.append('Z')
176 def C(self, cx1, cy1, cx2, cy2, ex, ey):
177 self.append('C', cx1, -cy1, cx2, -cy2, ex, -ey)
178 def c(self, cx1, cy1, cx2, cy2, ex, ey):
179 self.append('c', cx1, -cy1, cx2, -cy2, ex, -ey)
180 def S(self, cx2, cy2, ex, ey): self.append('S', cx2, -cy2, ex, -ey)
181 def s(self, cx2, cy2, ex, ey): self.append('s', cx2, -cy2, ex, -ey)
182 def Q(self, cx, cy, ex, ey): self.append('Q', cx, -cy, ex, -ey)
183 def q(self, cx, cy, ex, ey): self.append('q', cx, -cy, ex, -ey)
184 def T(self, ex, ey): self.append('T', ex, -ey)
185 def t(self, ex, ey): self.append('t', ex, -ey)
186 def A(self, rx, ry, rot, largeArc, sweep, ex, ey):
187 self.append('A', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex, -ey)
188 def a(self, rx, ry, rot, largeArc, sweep, ex, ey):
189 self.append('a', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex, -ey)
190 def arc(self, cx, cy, r, startDeg, endDeg, cw=False, includeM=True, includeL=False):
191 ''' Uses A() to draw a circular arc '''
192 largeArc = (endDeg - startDeg) % 360 > 180
193 startRad, endRad = startDeg*math.pi/180, endDeg*math.pi/180
194 sx, sy = r*math.cos(startRad), r*math.sin(startRad)
195 ex, ey = r*math.cos(endRad), r*math.sin(endRad)
196 if includeL:
197 self.L(cx+sx, cy+sy)
198 elif includeM:
199 self.M(cx+sx, cy+sy)
200 self.A(r, r, 0, largeArc ^ cw, cw, cx+ex, cy+ey)
201
202class Lines(Path):
203 ''' A sequence of connected lines (or a polygon)
204
205 Additional keyword arguments are output as additional properties to the
206 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
207 def __init__(self, sx, sy, *points, close=False, **kwargs):
208 super().__init__(d='', **kwargs)
209 self.M(sx, sy)
210 assert len(points) % 2 == 0
211 for i in range(len(points) // 2):
212 self.L(points[2*i], points[2*i+1])
213 if close:
214 self.Z()
215
216class Line(Lines):
217 ''' A line
218
219 Additional keyword arguments are output as additional properties to the
220 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
221 def __init__(self, sx, sy, ex, ey, **kwargs):
222 super().__init__(sx, sy, ex, ey, close=False, **kwargs)
223
224class Arc(Path):
225 ''' An arc
226
227 Additional keyword arguments are output as additional properties to the
228 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
229 def __init__(self, cx, cy, r, startDeg, endDeg, cw=False, **kwargs):
230 super().__init__(d='', **kwargs)
231 self.arc(cx, cy, r, startDeg, endDeg, cw=cw, includeM=True)
232