1
2import sys
3import math
4import os.path
5import base64
6import warnings
7import xml.sax.saxutils as xml
8from collections import defaultdict
9
10from . import defs
11
12elementsModule = sys.modules[__name__]
13
14# TODO: Support drawing ellipses without manually using Path
15
16def writeXmlNodeArgs(args, outputFile):
17 for k, v in args.items():
18 if v is None: continue
19 if isinstance(v, DrawingElement):
20 if v.id is None:
21 continue
22 if k == 'xlink:href':
23 v = '#{}'.format(v.id)
24 else:
25 v = 'url(#{})'.format(v.id)
26 outputFile.write(' {}="{}"'.format(k,v))
27
28
29class DrawingElement:
30 ''' Base class for drawing elements
31
32 Subclasses must implement writeSvgElement '''
33 def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
34 forceDup=False):
35 raise NotImplementedError('Abstract base class')
36 def getSvgDefs(self):
37 return ()
38 def getLinkedElems(self):
39 return ()
40 def writeSvgDefs(self, idGen, isDuplicate, outputFile, dryRun):
41 for defn in self.getSvgDefs():
42 if isDuplicate(defn): continue
43 defn.writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
44 if defn.id is None:
45 defn.id = idGen()
46 defn.writeSvgElement(idGen, isDuplicate, outputFile, dryRun,
47 forceDup=True)
48 if not dryRun:
49 outputFile.write('\n')
50 def __eq__(self, other):
51 return self is other
52
53class DrawingBasicElement(DrawingElement):
54 ''' Base class for SVG drawing elements that are a single node with no
55 child nodes '''
56 TAG_NAME = '_'
57 hasContent = False
58 def __init__(self, **args):
59 self.args = {}
60 for k, v in args.items():
61 k = k.replace('__', ':')
62 k = k.replace('_', '-')
63 if k[-1] == '-':
64 k = k[:-1]
65 self.args[k] = v
66 self.children = []
67 self.orderedChildren = defaultdict(list)
68 def checkChildrenAllowed(self):
69 if not self.hasContent:
70 raise RuntimeError(
71 '{} does not support children'.format(type(self)))
72 def allChildren(self):
73 ''' Returns self.children and self.orderedChildren as a single list. '''
74 output = list(self.children)
75 for z in sorted(self.orderedChildren):
76 output.extend(self.orderedChildren[z])
77 return output
78 @property
79 def id(self):
80 return self.args.get('id', None)
81 @id.setter
82 def id(self, newId):
83 self.args['id'] = newId
84 def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
85 forceDup=False):
86 children = self.allChildren()
87 if dryRun:
88 if isDuplicate(self) and self.id is None:
89 self.id = idGen()
90 for elem in self.getLinkedElems():
91 if elem.id is None:
92 elem.id = idGen()
93 if self.hasContent:
94 self.writeContent(idGen, isDuplicate, outputFile, dryRun)
95 if children:
96 self.writeChildrenContent(idGen, isDuplicate, outputFile,
97 dryRun)
98 return
99 if isDuplicate(self) and not forceDup:
100 outputFile.write('<use xlink:href="#{}" />'.format(self.id))
101 return
102 outputFile.write('<')
103 outputFile.write(self.TAG_NAME)
104 writeXmlNodeArgs(self.args, outputFile)
105 if not self.hasContent and not children:
106 outputFile.write(' />')
107 else:
108 outputFile.write('>')
109 if self.hasContent:
110 self.writeContent(idGen, isDuplicate, outputFile, dryRun)
111 if children:
112 self.writeChildrenContent(idGen, isDuplicate, outputFile,
113 dryRun)
114 outputFile.write('</')
115 outputFile.write(self.TAG_NAME)
116 outputFile.write('>')
117 def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
118 ''' Override in a subclass to add data between the start and end
119 tags. This will not be called if hasContent is False. '''
120 raise RuntimeError('This element has no content')
121 def writeChildrenContent(self, idGen, isDuplicate, outputFile, dryRun):
122 ''' Override in a subclass to add data between the start and end
123 tags. This will not be called if hasContent is False. '''
124 children = self.allChildren()
125 if dryRun:
126 for child in children:
127 child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
128 return
129 outputFile.write('\n')
130 for child in children:
131 child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
132 outputFile.write('\n')
133 def getSvgDefs(self):
134 return [v for v in self.args.values()
135 if isinstance(v, DrawingElement)]
136 def writeSvgDefs(self, idGen, isDuplicate, outputFile, dryRun):
137 super().writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
138 for child in self.allChildren():
139 child.writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
140 def __eq__(self, other):
141 if isinstance(other, type(self)):
142 return (self.TAG_NAME == other.TAG_NAME and
143 self.args == other.args and
144 self.children == other.children and
145 self.orderedChildren == other.orderedChildren)
146 return False
147 def appendAnim(self, animateElement):
148 self.children.append(animateElement)
149 def extendAnim(self, animateIterable):
150 self.children.extend(animateIterable)
151 def appendTitle(self, text, **kwargs):
152 self.children.append(Title(text, **kwargs))
153
154class DrawingParentElement(DrawingBasicElement):
155 ''' Base class for SVG elements that can have child nodes '''
156 hasContent = True
157 def __init__(self, children=(), orderedChildren=None, **args):
158 super().__init__(**args)
159 self.children = list(children)
160 if orderedChildren:
161 self.orderedChildren.update(
162 (z, list(v)) for z, v in orderedChildren.items())
163 if self.children or self.orderedChildren:
164 self.checkChildrenAllowed()
165 def draw(self, obj, *, z=None, **kwargs):
166 if obj is None:
167 return
168 if not hasattr(obj, 'writeSvgElement'):
169 elements = obj.toDrawables(elements=elementsModule, **kwargs)
170 else:
171 assert len(kwargs) == 0
172 elements = (obj,)
173 self.extend(elements, z=z)
174 def append(self, element, *, z=None):
175 self.checkChildrenAllowed()
176 if z is not None:
177 self.orderedChildren[z].append(element)
178 else:
179 self.children.append(element)
180 def extend(self, iterable, *, z=None):
181 self.checkChildrenAllowed()
182 if z is not None:
183 self.orderedChildren[z].extend(iterable)
184 else:
185 self.children.extend(iterable)
186 def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
187 pass
188
189class NoElement(DrawingElement):
190 ''' A drawing element that has no effect '''
191 def __init__(self): pass
192 def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
193 forceDup=False):
194 pass
195 def __eq__(self, other):
196 if isinstance(other, type(self)):
197 return True
198 return False
199
200class Group(DrawingParentElement):
201 ''' A group of drawing elements
202
203 Any transform will apply to its children and other attributes will be
204 inherited by its children. '''
205 TAG_NAME = 'g'
206
207class Raw(Group):
208 ''' Any any SVG code to insert into the output. '''
209 def __init__(self, content, defs=(), **kwargs):
210 super().__init__(**kwargs)
211 self.content = content
212 self.defs = defs
213 def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
214 if dryRun:
215 return
216 outputFile.write(self.content)
217 def getSvgDefs(self):
218 return self.defs
219
220class Use(DrawingBasicElement):
221 ''' A copy of another element
222
223 The other element becomes an SVG def shared between all Use elements
224 that reference it. '''
225 TAG_NAME = 'use'
226 def __init__(self, otherElem, x, y, **kwargs):
227 y = -y
228 if isinstance(otherElem, str) and not otherElem.startswith('#'):
229 otherElem = '#' + otherElem
230 super().__init__(xlink__href=otherElem, x=x, y=y, **kwargs)
231
232class Animate(DrawingBasicElement):
233 ''' Animation for a specific property of another element
234
235 This should be added as a child of the element to animate. Otherwise
236 the other element and this element must both be added to the drawing.
237 '''
238 TAG_NAME = 'animate'
239 def __init__(self, attributeName, dur, from_or_values=None, to=None,
240 begin=None, otherElem=None, **kwargs):
241 if to is None:
242 values = from_or_values
243 from_ = None
244 else:
245 values = None
246 from_ = from_or_values
247 if isinstance(otherElem, str) and not otherElem.startswith('#'):
248 otherElem = '#' + otherElem
249 kwargs.update(attributeName=attributeName, to=to, dur=dur, begin=begin)
250 kwargs.setdefault('values', values)
251 kwargs.setdefault('from_', from_)
252 super().__init__(xlink__href=otherElem, **kwargs)
253
254 def getSvgDefs(self):
255 return [v for k, v in self.args.items()
256 if isinstance(v, DrawingElement)
257 if k != 'xlink:href']
258
259 def getLinkedElems(self):
260 return (self.args['xlink:href'],)
261
262class _Mpath(DrawingBasicElement):
263 ''' Used by AnimateMotion '''
264 TAG_NAME = 'mpath'
265 def __init__(self, otherPath, **kwargs):
266 super().__init__(xlink__href=otherPath, **kwargs)
267
268class AnimateMotion(Animate):
269 ''' Animation for the motion another element along a path
270
271 This should be added as a child of the element to animate. Otherwise
272 the other element and this element must both be added to the drawing.
273 '''
274 TAG_NAME = 'animateMotion'
275 def __init__(self, path, dur, from_or_values=None, to=None, begin=None,
276 otherElem=None, **kwargs):
277 useMpath = False
278 if isinstance(path, DrawingElement):
279 useMpath = True
280 pathElem = path
281 path = None
282 kwargs.setdefault('attributeName', None)
283 super().__init__(dur=dur, from_or_values=from_or_values, to=to,
284 begin=begin, path=path, otherElem=otherElem, **kwargs)
285 if useMpath:
286 self.children.append(_Mpath(pathElem))
287
288class AnimateTransform(Animate):
289 ''' Animation for the transform property of another element
290
291 This should be added as a child of the element to animate. Otherwise
292 the other element and this element must both be added to the drawing.
293 '''
294 TAG_NAME = 'animateTransform'
295 def __init__(self, type, dur, from_or_values, to=None, begin=None,
296 attributeName='transform', otherElem=None, **kwargs):
297 super().__init__(attributeName, dur=dur, from_or_values=from_or_values,
298 to=to, begin=begin, type=type, otherElem=otherElem,
299 **kwargs)
300
301class Set(Animate):
302 ''' Animation for a specific property of another element that sets the new
303 value without a transition.
304
305 This should be added as a child of the element to animate. Otherwise
306 the other element and this element must both be added to the drawing.
307 '''
308 TAG_NAME = 'set'
309 def __init__(self, attributeName, dur, to=None, begin=None,
310 otherElem=None, **kwargs):
311 super().__init__(attributeName, dur=dur, from_or_values=None,
312 to=to, begin=begin, otherElem=otherElem, **kwargs)
313
314class Discard(Animate):
315 ''' Animation configuration specifying when it is safe to discard another
316 element. E.g. when it will no longer be visible after an animation.
317
318 This should be added as a child of the element to animate. Otherwise
319 the other element and this element must both be added to the drawing.
320 '''
321 TAG_NAME = 'discard'
322 def __init__(self, attributeName, begin=None, **kwargs):
323 kwargs.setdefault('attributeName', None)
324 kwargs.setdefault('to', None)
325 kwargs.setdefault('dur', None)
326 super().__init__(from_or_values=None, begin=begin, otherElem=None,
327 **kwargs)
328
329class Image(DrawingBasicElement):
330 ''' A linked or embedded raster image '''
331 TAG_NAME = 'image'
332 MIME_MAP = {
333 '.bm': 'image/bmp',
334 '.bmp': 'image/bmp',
335 '.gif': 'image/gif',
336 '.jpeg':'image/jpeg',
337 '.jpg': 'image/jpeg',
338 '.png': 'image/png',
339 '.svg': 'image/svg+xml',
340 '.tif': 'image/tiff',
341 '.tiff':'image/tiff',
342 '.pdf': 'application/pdf',
343 '.txt': 'text/plain',
344 }
345 MIME_DEFAULT = 'image/png'
346 def __init__(self, x, y, width, height, path=None, data=None, embed=False,
347 mimeType=None, **kwargs):
348 ''' Specify either the path or data argument. If path is used and
349 embed is True, the image file is embedded in a data URI. '''
350 if path is None and data is None:
351 raise ValueError('Either path or data arguments must be given')
352 if embed:
353 if mimeType is None and path is not None:
354 ext = os.path.splitext(path)[1].lower()
355 if ext in self.MIME_MAP:
356 mimeType = self.MIME_MAP[ext]
357 else:
358 mimeType = self.MIME_DEFAULT
359 warnings.warn('Unknown image file type "{}"'.format(ext),
360 Warning)
361 if mimeType is None:
362 mimeType = self.MIME_DEFAULT
363 warnings.warn('Unspecified image type; assuming png', Warning)
364 if data is not None:
365 embed = True
366 if embed and data is None:
367 with open(path, 'rb') as f:
368 data = f.read()
369 if not embed:
370 uri = path
371 else:
372 encData = base64.b64encode(data).decode(encoding='ascii')
373 uri = 'data:{};base64,{}'.format(mimeType, encData)
374 super().__init__(x=x, y=-y-height, width=width, height=height,
375 xlink__href=uri, **kwargs)
376
377class Text(DrawingParentElement):
378 ''' Text
379
380 Additional keyword arguments are output as additional arguments to the
381 SVG node e.g. fill='red', font_size=20, text_anchor='middle',
382 letter_spacing=1.5.
383
384 CairoSVG bug with letter spacing text on a path: The first two letters
385 are always spaced as if letter_spacing=1. '''
386 TAG_NAME = 'text'
387 hasContent = True
388 def __new__(cls, text, *args, path=None, id=None, _skipCheck=False,
389 **kwargs):
390 # Check for the special case of multi-line text on a path
391 # This is inconsistently implemented by renderers so we return a group
392 # of single-line text on paths instead.
393 if path is not None and not _skipCheck:
394 text, _ = cls._handleTextArgument(text, True)
395 if len(text) > 1:
396 # Special case
397 g = Group(id=id)
398 for i, line in enumerate(text):
399 subtext = [None] * len(text)
400 subtext[i] = line
401 g.append(Text(subtext, *args, path=path, _skipCheck=True,
402 **kwargs))
403 return g
404 return super().__new__(cls)
405 def __init__(self, text, fontSize, x=None, y=None, *, center=False,
406 valign=None, lineHeight=1, lineOffset=0, path=None,
407 startOffset=None, pathArgs=None, tspanArgs=None,
408 cairoFix=True, _skipCheck=False, **kwargs):
409 # Check argument requirements
410 if path is None:
411 if x is None or y is None:
412 raise TypeError(
413 "__init__() missing required arguments: 'x' and 'y' "
414 "are required unless 'path' is specified")
415 try:
416 y = -y
417 except TypeError:
418 pass
419 else:
420 if x is not None or y is not None:
421 raise TypeError(
422 "__init__() conflicting arguments: 'x' and 'y' "
423 "should not be used when 'path' is specified")
424 if pathArgs is None:
425 pathArgs = {}
426 if startOffset is not None:
427 pathArgs.setdefault('startOffset', startOffset)
428 if tspanArgs is None:
429 tspanArgs = {}
430 onPath = path is not None
431
432 text, singleLine = self._handleTextArgument(text, forceMulti=onPath)
433 numLines = len(text)
434
435 # Text alignment
436 centerCompat = False
437 if center and valign is None:
438 valign = 'middle'
439 centerCompat = singleLine and not onPath
440 if center and kwargs.get('text_anchor') is None:
441 kwargs['text_anchor'] = 'middle'
442 if valign == 'middle':
443 if centerCompat: # Backwards compatible centering
444 lineOffset += 0.5 * center
445 else:
446 lineOffset += 0.4 - lineHeight * (numLines - 1) / 2
447 elif valign == 'top':
448 lineOffset += 1
449 elif valign == 'bottom':
450 lineOffset += -lineHeight * (numLines - 1)
451 if singleLine:
452 dy = '{}em'.format(lineOffset)
453 kwargs.setdefault('dy', dy)
454 # Text alignment on a path
455 if onPath:
456 if kwargs.get('text_anchor') == 'start':
457 pathArgs.setdefault('startOffset', '0')
458 elif kwargs.get('text_anchor') == 'middle':
459 pathArgs.setdefault('startOffset', '50%')
460 elif kwargs.get('text_anchor') == 'end':
461 if cairoFix and 'startOffset' not in pathArgs:
462 # Fix CairoSVG not drawing the last character with aligned
463 # right
464 tspanArgs.setdefault('dx', -1)
465 pathArgs.setdefault('startOffset', '100%')
466
467 super().__init__(x=x, y=y, font_size=fontSize, **kwargs)
468 self._textPath = None
469 if singleLine:
470 self.escapedText = xml.escape(text[0])
471 else:
472 # Add elements for each line of text
473 self.escapedText = ''
474 if path is None:
475 # Text is an iterable
476 for i, line in enumerate(text):
477 dy = '{}em'.format(lineOffset if i == 0 else lineHeight)
478 self.appendLine(line, x=x, dy=dy, **tspanArgs)
479 else:
480 self._textPath = _TextPath(path, **pathArgs)
481 assert sum(bool(line) for line in text) <= 1, (
482 'Logic error, __new__ should handle multi-line paths')
483 for i, line in enumerate(text):
484 if not line: continue
485 dy = '{}em'.format(lineOffset + i*lineHeight)
486 tspan = TSpan(line, dy=dy, **tspanArgs)
487 self._textPath.append(tspan)
488 self.append(self._textPath)
489 @staticmethod
490 def _handleTextArgument(text, forceMulti=False):
491 # Handle multi-line text (contains '\n' or is a list of strings)
492 singleLine = isinstance(text, str)
493 if isinstance(text, str):
494 singleLine = '\n' not in text and not forceMulti
495 if singleLine:
496 text = (text,)
497 else:
498 text = tuple(text.splitlines())
499 singleLine = False
500 else:
501 singleLine = False
502 text = tuple(text)
503 return text, singleLine
504 def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
505 if dryRun:
506 return
507 outputFile.write(self.escapedText)
508 def writeChildrenContent(self, idGen, isDuplicate, outputFile, dryRun):
509 ''' Override in a subclass to add data between the start and end
510 tags. This will not be called if hasContent is False. '''
511 children = self.allChildren()
512 if dryRun:
513 for child in children:
514 child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
515 return
516 for child in children:
517 child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
518 def appendLine(self, line, **kwargs):
519 if self._textPath is not None:
520 raise ValueError('appendLine is not supported for text on a path')
521 self.append(TSpan(line, **kwargs))
522
523class _TextPath(DrawingParentElement):
524 TAG_NAME = 'textPath'
525 hasContent = True
526 def __init__(self, path, **kwargs):
527 super().__init__(xlink__href=path, **kwargs)
528
529class _TextContainingElement(DrawingBasicElement):
530 ''' A private parent class used for elements that only have plain text
531 content. '''
532 hasContent = True
533 def __init__(self, text, **kwargs):
534 super().__init__(**kwargs)
535 self.escapedText = xml.escape(text)
536 def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
537 if dryRun:
538 return
539 outputFile.write(self.escapedText)
540
541class TSpan(_TextContainingElement):
542 ''' A line of text within the Text element. '''
543 TAG_NAME = 'tspan'
544
545class Title(_TextContainingElement):
546 ''' A title element.
547
548 This element can be appended with shape.appendTitle("Your title!"),
549 which can be useful for adding a tooltip or on-hover text display
550 to an element.
551 '''
552 TAG_NAME = 'title'
553
554class Rectangle(DrawingBasicElement):
555 ''' A rectangle
556
557 Additional keyword arguments are output as additional arguments to the
558 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
559 TAG_NAME = 'rect'
560 def __init__(self, x, y, width, height, **kwargs):
561 try:
562 y = -y-height
563 except TypeError:
564 pass
565 super().__init__(x=x, y=y, width=width, height=height,
566 **kwargs)
567
568class Circle(DrawingBasicElement):
569 ''' A circle
570
571 Additional keyword arguments are output as additional properties to the
572 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
573 TAG_NAME = 'circle'
574 def __init__(self, cx, cy, r, **kwargs):
575 try:
576 cy = -cy
577 except TypeError:
578 pass
579 super().__init__(cx=cx, cy=cy, r=r, **kwargs)
580
581class Ellipse(DrawingBasicElement):
582 ''' An ellipse
583
584 Additional keyword arguments are output as additional properties to the
585 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
586 TAG_NAME = 'ellipse'
587 def __init__(self, cx, cy, rx, ry, **kwargs):
588 try:
589 cy = -cy
590 except TypeError:
591 pass
592 super().__init__(cx=cx, cy=cy, rx=rx, ry=ry, **kwargs)
593
594class ArcLine(Circle):
595 ''' An arc
596
597 In most cases, use Arc instead of ArcLine. ArcLine uses the
598 stroke-dasharray SVG property to make the edge of a circle look like
599 an arc.
600
601 Additional keyword arguments are output as additional arguments to the
602 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
603 def __init__(self, cx, cy, r, startDeg, endDeg, **kwargs):
604 if endDeg - startDeg == 360:
605 super().__init__(cx, cy, r, **kwargs)
606 return
607 startDeg, endDeg = (-endDeg) % 360, (-startDeg) % 360
608 arcDeg = (endDeg - startDeg) % 360
609 def arcLen(deg): return math.radians(deg) * r
610 wholeLen = 2 * math.pi * r
611 if endDeg == startDeg:
612 offset = 1
613 dashes = "0 {}".format(wholeLen+2)
614 #elif endDeg >= startDeg:
615 elif True:
616 startLen = arcLen(startDeg)
617 arcLen = arcLen(arcDeg)
618 offLen = wholeLen - arcLen
619 offset = -startLen
620 dashes = "{} {}".format(arcLen, offLen)
621 #else:
622 # firstLen = arcLen(endDeg)
623 # secondLen = arcLen(360-startDeg)
624 # gapLen = wholeLen - firstLen - secondLen
625 # offset = 0
626 # dashes = "{} {} {}".format(firstLen, gapLen, secondLen)
627 super().__init__(cx, cy, r, stroke_dasharray=dashes,
628 stroke_dashoffset=offset, **kwargs)
629
630class Path(DrawingBasicElement):
631 ''' An arbitrary path
632
633 Path Supports building an SVG path by calling instance methods
634 corresponding to path commands.
635
636 Additional keyword arguments are output as additional properties to the
637 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
638 TAG_NAME = 'path'
639 def __init__(self, d='', **kwargs):
640 super().__init__(d=d, **kwargs)
641 def append(self, commandStr, *args):
642 if len(self.args['d']) > 0:
643 commandStr = ' ' + commandStr
644 if len(args) > 0:
645 commandStr = commandStr + ','.join(map(str, args))
646 self.args['d'] += commandStr
647 return self
648 def M(self, x, y): return self.append('M', x, -y)
649 def m(self, dx, dy): return self.append('m', dx, -dy)
650 def L(self, x, y): return self.append('L', x, -y)
651 def l(self, dx, dy): return self.append('l', dx, -dy)
652 def H(self, x): return self.append('H', x)
653 def h(self, dx): return self.append('h', dx)
654 def V(self, y): return self.append('V', -y)
655 def v(self, dy): return self.append('v', -dy)
656 def Z(self): return self.append('Z')
657 def C(self, cx1, cy1, cx2, cy2, ex, ey):
658 return self.append('C', cx1, -cy1, cx2, -cy2, ex, -ey)
659 def c(self, cx1, cy1, cx2, cy2, ex, ey):
660 return self.append('c', cx1, -cy1, cx2, -cy2, ex, -ey)
661 def S(self, cx2, cy2, ex, ey): return self.append('S', cx2, -cy2, ex, -ey)
662 def s(self, cx2, cy2, ex, ey): return self.append('s', cx2, -cy2, ex, -ey)
663 def Q(self, cx, cy, ex, ey): return self.append('Q', cx, -cy, ex, -ey)
664 def q(self, cx, cy, ex, ey): return self.append('q', cx, -cy, ex, -ey)
665 def T(self, ex, ey): return self.append('T', ex, -ey)
666 def t(self, ex, ey): return self.append('t', ex, -ey)
667 def A(self, rx, ry, rot, largeArc, sweep, ex, ey):
668 return self.append('A', rx, ry, rot, int(bool(largeArc)),
669 int(bool(sweep)), ex, -ey)
670 def a(self, rx, ry, rot, largeArc, sweep, ex, ey):
671 return self.append('a', rx, ry, rot, int(bool(largeArc)),
672 int(bool(sweep)), ex, -ey)
673 def arc(self, cx, cy, r, startDeg, endDeg, cw=False, includeM=True,
674 includeL=False):
675 ''' Uses A() to draw a circular arc '''
676 largeArc = (endDeg - startDeg) % 360 > 180
677 startRad, endRad = startDeg*math.pi/180, endDeg*math.pi/180
678 sx, sy = r*math.cos(startRad), r*math.sin(startRad)
679 ex, ey = r*math.cos(endRad), r*math.sin(endRad)
680 if includeL:
681 self.L(cx+sx, cy+sy)
682 elif includeM:
683 self.M(cx+sx, cy+sy)
684 return self.A(r, r, 0, largeArc ^ cw, cw, cx+ex, cy+ey)
685
686class Lines(Path):
687 ''' A sequence of connected lines (or a polygon)
688
689 Additional keyword arguments are output as additional properties to the
690 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
691 def __init__(self, sx, sy, *points, close=False, **kwargs):
692 super().__init__(d='', **kwargs)
693 self.M(sx, sy)
694 assert len(points) % 2 == 0
695 for i in range(len(points) // 2):
696 self.L(points[2*i], points[2*i+1])
697 if close:
698 self.Z()
699
700class Line(Lines):
701 ''' A line
702
703 Additional keyword arguments are output as additional properties to the
704 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
705 def __init__(self, sx, sy, ex, ey, **kwargs):
706 super().__init__(sx, sy, ex, ey, close=False, **kwargs)
707
708class Arc(Path):
709 ''' An arc
710
711 Additional keyword arguments are output as additional properties to the
712 SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
713 def __init__(self, cx, cy, r, startDeg, endDeg, cw=False, **kwargs):
714 super().__init__(d='', **kwargs)
715 self.arc(cx, cy, r, startDeg, endDeg, cw=cw, includeM=True)
716