···
···
import xml.sax.saxutils as xml
from collections import defaultdict
8
+
from . import defs, url_encode
12
-
elementsModule = sys.modules[__name__]
14
-
# TODO: Support drawing ellipses without manually using Path
16
-
def writeXmlNodeArgs(args, outputFile):
11
+
def write_xml_node_args(args, output_file, id_map=None):
for k, v in args.items():
if isinstance(v, DrawingElement):
16
+
if id_map and id(v) in id_map:
17
+
mapped_id = id_map[id(v)]
18
+
if mapped_id is None:
23
-
v = '#{}'.format(v.id)
21
+
v = '#{}'.format(mapped_id)
25
-
v = 'url(#{})'.format(v.id)
26
-
outputFile.write(' {}="{}"'.format(k,v))
23
+
v = 'url(#{})'.format(mapped_id)
24
+
output_file.write(' {}="{}"'.format(k,v))
30
-
''' Base class for drawing elements
28
+
'''Base class for drawing elements.
32
-
Subclasses must implement writeSvgElement '''
33
-
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
30
+
Subclasses must implement write_svg_element.
32
+
def write_svg_element(self, id_map, is_duplicate, output_file, dry_run,
raise NotImplementedError('Abstract base class')
36
-
def getSvgDefs(self):
35
+
def get_svg_defs(self):
38
-
def getLinkedElems(self):
37
+
def get_linked_elems(self):
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)
39
+
def write_svg_defs(self, id_map, is_duplicate, output_file, dry_run):
40
+
for defn in self.get_svg_defs():
41
+
if is_duplicate(defn):
43
+
defn.write_svg_defs(id_map, is_duplicate, output_file, dry_run)
46
-
defn.writeSvgElement(idGen, isDuplicate, outputFile, dryRun,
49
-
outputFile.write('\n')
46
+
defn.write_svg_element(
47
+
id_map, is_duplicate, output_file, dry_run, force_dup=True)
49
+
output_file.write('\n')
class DrawingBasicElement(DrawingElement):
54
-
''' Base class for SVG drawing elements that are a single node with no
54
+
'''Base class for SVG drawing elements that are a single node with no child
def __init__(self, **args):
for k, v in args.items():
···
67
-
self.orderedChildren = defaultdict(list)
68
-
def checkChildrenAllowed(self):
69
-
if not self.hasContent:
68
+
self.ordered_children = defaultdict(list)
69
+
def check_children_allowed(self):
70
+
if not self.has_content:
71
-
'{} does not support children'.format(type(self)))
72
-
def allChildren(self):
73
-
''' Returns self.children and self.orderedChildren as a single list. '''
72
+
'{} does not support children'.format(type(self)))
73
+
def all_children(self):
74
+
'''Return self.children and self.ordered_children as a single list.'''
output = list(self.children)
75
-
for z in sorted(self.orderedChildren):
76
-
output.extend(self.orderedChildren[z])
76
+
for z in sorted(self.ordered_children):
77
+
output.extend(self.ordered_children[z])
return self.args.get('id', None)
82
-
def id(self, newId):
83
-
self.args['id'] = newId
84
-
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
86
-
children = self.allChildren()
88
-
if isDuplicate(self) and self.id is None:
90
-
for elem in self.getLinkedElems():
82
+
def write_svg_element(self, id_map, is_duplicate, output_file, dry_run,
84
+
children = self.all_children()
86
+
if is_duplicate(self) and self.id is None:
88
+
for elem in self.get_linked_elems():
94
-
self.writeContent(idGen, isDuplicate, outputFile, dryRun)
91
+
if self.has_content:
92
+
self.write_content(id_map, is_duplicate, output_file, dry_run)
96
-
self.writeChildrenContent(idGen, isDuplicate, outputFile,
94
+
self.write_children_content(
95
+
id_map, is_duplicate, output_file, dry_run)
99
-
if isDuplicate(self) and not forceDup:
100
-
outputFile.write('<use xlink:href="#{}" />'.format(self.id))
97
+
if is_duplicate(self) and not force_dup:
99
+
if id_map and id(self) in id_map:
100
+
mapped_id = id_map[id(self)]
101
+
output_file.write('<use xlink:href="#{}" />'.format(mapped_id))
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(' />')
103
+
output_file.write('<')
104
+
output_file.write(self.TAG_NAME)
105
+
override_args = self.args
106
+
if id(self) in id_map:
107
+
override_args = dict(override_args)
108
+
override_args['id'] = id_map[id(self)]
109
+
write_xml_node_args(override_args, output_file, id_map)
110
+
if not self.has_content and not children:
111
+
output_file.write(' />')
108
-
outputFile.write('>')
109
-
if self.hasContent:
110
-
self.writeContent(idGen, isDuplicate, outputFile, dryRun)
113
+
output_file.write('>')
114
+
if self.has_content:
115
+
self.write_content(id_map, is_duplicate, output_file, dry_run)
112
-
self.writeChildrenContent(idGen, isDuplicate, outputFile,
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. '''
117
+
self.write_children_content(
118
+
id_map, is_duplicate, output_file, dry_run)
119
+
output_file.write('</')
120
+
output_file.write(self.TAG_NAME)
121
+
output_file.write('>')
122
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
123
+
'''Override in a subclass to add data between the start and end tags.
125
+
This will not be called if has_content is False.
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()
128
+
def write_children_content(self, id_map, is_duplicate, output_file,
130
+
'''Override in a subclass to add data between the start and end tags.
132
+
This will not be called if has_content is False.
134
+
children = self.all_children()
127
-
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
137
+
child.write_svg_element(
138
+
id_map, is_duplicate, output_file, dry_run)
129
-
outputFile.write('\n')
140
+
output_file.write('\n')
131
-
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
132
-
outputFile.write('\n')
133
-
def getSvgDefs(self):
142
+
child.write_svg_element(id_map, is_duplicate, output_file, dry_run)
143
+
output_file.write('\n')
144
+
def get_svg_defs(self):
return [v for v in self.args.values()
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)
147
+
def write_svg_defs(self, id_map, is_duplicate, output_file, dry_run):
148
+
super().write_svg_defs(id_map, is_duplicate, output_file, dry_run)
149
+
for child in self.all_children():
150
+
child.write_svg_defs(id_map, is_duplicate, output_file, dry_run)
if isinstance(other, type(self)):
return (self.TAG_NAME == other.TAG_NAME and
self.args == other.args and
self.children == other.children and
145
-
self.orderedChildren == other.orderedChildren)
156
+
self.ordered_children == other.ordered_children)
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):
158
+
def append_anim(self, animate_element):
159
+
self.children.append(animate_element)
160
+
def extend_anim(self, animate_iterable):
161
+
self.children.extend(animate_iterable)
162
+
def append_title(self, text, **kwargs):
self.children.append(Title(text, **kwargs))
class DrawingParentElement(DrawingBasicElement):
155
-
''' Base class for SVG elements that can have child nodes '''
157
-
def __init__(self, children=(), orderedChildren=None, **args):
166
+
'''Base class for SVG elements that can have child nodes.'''
168
+
def __init__(self, children=(), ordered_children=None, **args):
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()
171
+
if ordered_children:
172
+
self.ordered_children.update(
173
+
(z, list(v)) for z, v in ordered_children.items())
174
+
if self.children or self.ordered_children:
175
+
self.check_children_allowed()
def draw(self, obj, *, z=None, **kwargs):
168
-
if not hasattr(obj, 'writeSvgElement'):
169
-
elements = obj.toDrawables(elements=elementsModule, **kwargs)
179
+
if not hasattr(obj, 'write_svg_element'):
180
+
elements = obj.to_drawables(**kwargs)
173
-
self.extend(elements, z=z)
184
+
if hasattr(elements, 'write_svg_element'):
185
+
self.append(elements, z=z)
187
+
self.extend(elements, z=z)
def append(self, element, *, z=None):
175
-
self.checkChildrenAllowed()
189
+
self.check_children_allowed()
177
-
self.orderedChildren[z].append(element)
191
+
self.ordered_children[z].append(element)
self.children.append(element)
def extend(self, iterable, *, z=None):
181
-
self.checkChildrenAllowed()
195
+
self.check_children_allowed()
183
-
self.orderedChildren[z].extend(iterable)
197
+
self.ordered_children[z].extend(iterable)
self.children.extend(iterable)
186
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
200
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
class NoElement(DrawingElement):
''' A drawing element that has no effect '''
191
-
def __init__(self): pass
192
-
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
205
+
def __init__(self):
207
+
def write_svg_element(self, id_map, is_duplicate, output_file, dry_run,
if isinstance(other, type(self)):
···
class Group(DrawingParentElement):
201
-
''' A group of drawing elements
216
+
'''A group of drawing elements.
203
-
Any transform will apply to its children and other attributes will be
204
-
inherited by its children. '''
218
+
Any transform will apply to its children and other attributes will be
219
+
inherited by its children.
208
-
''' Any any SVG code to insert into the output. '''
209
-
def __init__(self, content, defs=(), **kwargs):
210
-
super().__init__(**kwargs)
223
+
class Raw(DrawingBasicElement):
224
+
'''Raw unescaped text to include in the SVG output.
226
+
Special XML characters like '<' and '&' in the content may have unexpected
227
+
effects or completely break the resulting SVG.
230
+
def __init__(self, content, defs=()):
213
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
234
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
216
-
outputFile.write(self.content)
217
-
def getSvgDefs(self):
237
+
output_file.write(self.content)
238
+
def get_svg_defs(self):
240
+
def check_children_allowed(self):
241
+
raise RuntimeError('{} does not support children'.format(type(self)))
class Use(DrawingBasicElement):
221
-
''' A copy of another element
244
+
'''A copy of another element, drawn at a given position
223
-
The other element becomes an SVG def shared between all Use elements
224
-
that reference it. '''
246
+
The referenced element becomes an SVG def shared between all Use elements
247
+
that reference it. Useful for drawings with many copies of similar shapes.
248
+
Additional arguments like `fill='red'` will be used as the default for this
249
+
copy of the shapes.
226
-
def __init__(self, otherElem, x, y, **kwargs):
228
-
if isinstance(otherElem, str) and not otherElem.startswith('#'):
229
-
otherElem = '#' + otherElem
230
-
super().__init__(xlink__href=otherElem, x=x, y=y, **kwargs)
252
+
def __init__(self, other_elem, x, y, **kwargs):
253
+
if isinstance(other_elem, str) and not other_elem.startswith('#'):
254
+
other_elem = '#' + other_elem
255
+
super().__init__(xlink__href=other_elem, x=x, y=y, **kwargs)
class Animate(DrawingBasicElement):
233
-
''' Animation for a specific property of another element
258
+
'''Animation for a specific property of another element.
260
+
This should be added as a child of the element to animate. Otherwise the
261
+
referenced other element and this element must both be added to the drawing.
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.
263
+
Useful SVG attributes:
264
+
- repeatCount: 0, 1, ..., 'indefinite'
def __init__(self, attributeName, dur, from_or_values=None, to=None,
240
-
begin=None, otherElem=None, **kwargs):
268
+
begin=None, other_elem=None, **kwargs):
247
-
if isinstance(otherElem, str) and not otherElem.startswith('#'):
248
-
otherElem = '#' + otherElem
275
+
if isinstance(other_elem, str) and not other_elem.startswith('#'):
276
+
other_elem = '#' + other_elem
kwargs.update(attributeName=attributeName, to=to, dur=dur, begin=begin)
kwargs.setdefault('values', values)
kwargs.setdefault('from_', from_)
252
-
super().__init__(xlink__href=otherElem, **kwargs)
280
+
super().__init__(xlink__href=other_elem, **kwargs)
254
-
def getSvgDefs(self):
282
+
def get_svg_defs(self):
return [v for k, v in self.args.items()
if isinstance(v, DrawingElement)
259
-
def getLinkedElems(self):
260
-
return (self.args['xlink:href'],)
287
+
def get_linked_elems(self):
288
+
elem = self.args['xlink:href']
289
+
return (elem,) if elem is not None else ()
class _Mpath(DrawingBasicElement):
263
-
''' Used by AnimateMotion '''
292
+
'''Used by AnimateMotion.'''
265
-
def __init__(self, otherPath, **kwargs):
266
-
super().__init__(xlink__href=otherPath, **kwargs)
294
+
def __init__(self, other_path, **kwargs):
295
+
super().__init__(xlink__href=other_path, **kwargs)
class AnimateMotion(Animate):
269
-
''' Animation for the motion another element along a path
298
+
'''Animation for the motion of another element along a path.
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.
300
+
This should be added as a child of the element to animate. Otherwise the
301
+
referenced other element and this element must both be added to the drawing.
TAG_NAME = 'animateMotion'
def __init__(self, path, dur, from_or_values=None, to=None, begin=None,
276
-
otherElem=None, **kwargs):
305
+
other_elem=None, **kwargs):
if isinstance(path, DrawingElement):
kwargs.setdefault('attributeName', None)
super().__init__(dur=dur, from_or_values=from_or_values, to=to,
284
-
begin=begin, path=path, otherElem=otherElem, **kwargs)
286
-
self.children.append(_Mpath(pathElem))
313
+
begin=begin, path=path, other_elem=other_elem,
316
+
self.children.append(_Mpath(path_elem))
class AnimateTransform(Animate):
289
-
''' Animation for the transform property of another element
319
+
'''Animation for the transform property of another element.
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.
321
+
This should be added as a child of the element to animate. Otherwise the
322
+
referenced other element and this element must both be added to the drawing.
TAG_NAME = 'animateTransform'
def __init__(self, type, dur, from_or_values, to=None, begin=None,
296
-
attributeName='transform', otherElem=None, **kwargs):
326
+
attributeName='transform', other_elem=None, **kwargs):
super().__init__(attributeName, dur=dur, from_or_values=from_or_values,
298
-
to=to, begin=begin, type=type, otherElem=otherElem,
328
+
to=to, begin=begin, type=type, other_elem=other_elem,
302
-
''' Animation for a specific property of another element that sets the new
303
-
value without a transition.
332
+
'''Animation for a specific property of another element that sets the new
333
+
value without a transition.
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.
335
+
This should be added as a child of the element to animate. Otherwise the
336
+
referenced other element and this element must both be added to the drawing.
def __init__(self, attributeName, dur, to=None, begin=None,
310
-
otherElem=None, **kwargs):
340
+
other_elem=None, **kwargs):
super().__init__(attributeName, dur=dur, from_or_values=None,
312
-
to=to, begin=begin, otherElem=otherElem, **kwargs)
342
+
to=to, begin=begin, other_elem=other_elem, **kwargs)
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.
345
+
'''Animation configuration specifying when it is safe to discard another
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.
348
+
Use this when an element will no longer be visible after an animation.
349
+
This should be added as a child of the element to animate. Otherwise the
350
+
referenced other element and this element must both be added to the drawing.
def __init__(self, attributeName, begin=None, **kwargs):
kwargs.setdefault('attributeName', None)
kwargs.setdefault('to', None)
kwargs.setdefault('dur', None)
326
-
super().__init__(from_or_values=None, begin=begin, otherElem=None,
357
+
super().__init__(from_or_values=None, begin=begin, other_elem=None,
class Image(DrawingBasicElement):
330
-
''' A linked or embedded raster image '''
361
+
'''A linked or embedded image.'''
···
MIME_DEFAULT = 'image/png'
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. '''
378
+
mime_type=None, **kwargs):
380
+
Specify either the path or data argument. If path is used and embed is
381
+
True, the image file is embedded in a data URI.
if path is None and data is None:
raise ValueError('Either path or data arguments must be given')
353
-
if mimeType is None and path is not None:
386
+
if mime_type is None and path is not None:
ext = os.path.splitext(path)[1].lower()
356
-
mimeType = self.MIME_MAP[ext]
389
+
mime_type = self.MIME_MAP[ext]
358
-
mimeType = self.MIME_DEFAULT
391
+
mime_type = self.MIME_DEFAULT
warnings.warn('Unknown image file type "{}"'.format(ext),
361
-
if mimeType is None:
362
-
mimeType = self.MIME_DEFAULT
394
+
if mime_type is None:
395
+
mime_type = self.MIME_DEFAULT
warnings.warn('Unspecified image type; assuming png', Warning)
···
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)
405
+
uri = url_encode.bytes_as_data_uri(data, mime=mime_type)
406
+
super().__init__(x=x, y=y, width=width, height=height, xlink__href=uri,
class Text(DrawingParentElement):
410
+
'''A line or multiple lines of text, optionally placed along a path.
412
+
Additional keyword arguments are output as additional arguments to the SVG
413
+
node e.g. fill='red', font_size=20, letter_spacing=1.5.
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.
415
+
Useful SVG attributes:
416
+
- text_anchor: start, middle, end
417
+
- dominant_baseline: auto, central, middle, hanging, text-top, ...
418
+
See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text
384
-
CairoSVG bug with letter spacing text on a path: The first two letters
385
-
are always spaced as if letter_spacing=1. '''
420
+
CairoSVG bug with letter spacing text on a path: The first two letters are
421
+
always spaced as if letter_spacing=1.
388
-
def __new__(cls, text, *args, path=None, id=None, _skipCheck=False,
425
+
def __new__(cls, text, *args, path=None, id=None, _skip_check=False,
# Check for the special case of multi-line text on a path
# This is inconsistently implemented by renderers so we return a group
# of single-line text on paths instead.
393
-
if path is not None and not _skipCheck:
394
-
text, _ = cls._handleTextArgument(text, True)
430
+
if path is not None and not _skip_check:
431
+
text, _ = cls._handle_text_argument(text, True)
for i, line in enumerate(text):
subtext = [None] * len(text)
401
-
g.append(Text(subtext, *args, path=path, _skipCheck=True,
438
+
g.append(Text(subtext, *args, path=path, _skip_check=True,
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):
442
+
def __init__(self, text, font_size, x=None, y=None, *, center=False,
443
+
line_height=1, line_offset=0, path=None, start_offset=None,
444
+
path_args=None, tspan_args=None, cairo_fix=True,
445
+
_skip_check=False, **kwargs):
# Check argument requirements
if x is None or y is None:
"__init__() missing required arguments: 'x' and 'y' "
"are required unless 'path' is specified")
if x is not None or y is not None:
"__init__() conflicting arguments: 'x' and 'y' "
"should not be used when 'path' is specified")
424
-
if pathArgs is None:
426
-
if startOffset is not None:
427
-
pathArgs.setdefault('startOffset', startOffset)
428
-
if tspanArgs is None:
430
-
onPath = path is not None
457
+
if path_args is None:
459
+
if start_offset is not None:
460
+
path_args.setdefault('startOffset', start_offset)
461
+
if tspan_args is None:
463
+
on_path = path is not None
432
-
text, singleLine = self._handleTextArgument(text, forceMulti=onPath)
433
-
numLines = len(text)
465
+
text, single_line = self._handle_text_argument(
466
+
text, force_multi=on_path)
467
+
num_lines = len(text)
436
-
centerCompat = False
437
-
if center and valign is None:
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
471
+
kwargs.setdefault('text_anchor', 'middle')
472
+
if path is None and single_line:
473
+
kwargs.setdefault('dominant_baseline', 'central')
446
-
lineOffset += 0.4 - lineHeight * (numLines - 1) / 2
447
-
elif valign == 'top':
449
-
elif valign == 'bottom':
450
-
lineOffset += -lineHeight * (numLines - 1)
452
-
dy = '{}em'.format(lineOffset)
453
-
kwargs.setdefault('dy', dy)
476
+
line_offset -= line_height * (num_lines - 1) / 2
# Text alignment on a path
if kwargs.get('text_anchor') == 'start':
457
-
pathArgs.setdefault('startOffset', '0')
480
+
path_args.setdefault('startOffset', '0')
elif kwargs.get('text_anchor') == 'middle':
459
-
pathArgs.setdefault('startOffset', '50%')
482
+
path_args.setdefault('startOffset', '50%')
elif kwargs.get('text_anchor') == 'end':
461
-
if cairoFix and 'startOffset' not in pathArgs:
484
+
if cairo_fix and 'startOffset' not in path_args:
# Fix CairoSVG not drawing the last character with aligned
464
-
tspanArgs.setdefault('dx', -1)
465
-
pathArgs.setdefault('startOffset', '100%')
487
+
tspan_args.setdefault('dx', -1)
488
+
path_args.setdefault('startOffset', '100%')
467
-
super().__init__(x=x, y=y, font_size=fontSize, **kwargs)
468
-
self._textPath = None
470
-
self.escapedText = xml.escape(text[0])
490
+
super().__init__(x=x, y=y, font_size=font_size, **kwargs)
491
+
self._text_path = None
493
+
self.escaped_text = xml.escape(text[0])
# Add elements for each line of text
473
-
self.escapedText = ''
496
+
self.escaped_text = ''
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)
500
+
dy = '{}em'.format(line_offset if i == 0 else line_height)
501
+
self.append_line(line, x=x, dy=dy, **tspan_args)
480
-
self._textPath = _TextPath(path, **pathArgs)
503
+
self._text_path = _TextPath(path, **path_args)
assert sum(bool(line) for line in text) <= 1, (
'Logic error, __new__ should handle multi-line paths')
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)
509
+
dy = '{}em'.format(line_offset + i*line_height)
510
+
tspan = TSpan(line, dy=dy, **tspan_args)
511
+
self._text_path.append(tspan)
512
+
self.append(self._text_path)
490
-
def _handleTextArgument(text, forceMulti=False):
514
+
def _handle_text_argument(text, force_multi=False):
# Handle multi-line text (contains '\n' or is a list of strings)
492
-
singleLine = isinstance(text, str)
if isinstance(text, str):
494
-
singleLine = '\n' not in text and not forceMulti
517
+
single_line = '\n' not in text and not force_multi
text = tuple(text.splitlines())
523
+
single_line = False
503
-
return text, singleLine
504
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
525
+
return text, single_line
526
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
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()
513
-
for child in children:
514
-
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
529
+
output_file.write(self.escaped_text)
530
+
def write_children_content(self, id_map, is_duplicate, output_file,
532
+
children = self.all_children()
517
-
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
518
-
def appendLine(self, line, **kwargs):
519
-
if self._textPath is not None:
534
+
child.write_svg_element(id_map, is_duplicate, output_file, dry_run)
535
+
def append_line(self, line, **kwargs):
536
+
if self._text_path is not None:
raise ValueError('appendLine is not supported for text on a path')
self.append(TSpan(line, **kwargs))
class _TextPath(DrawingParentElement):
def __init__(self, path, **kwargs):
super().__init__(xlink__href=path, **kwargs)
class _TextContainingElement(DrawingBasicElement):
''' A private parent class used for elements that only have plain text
def __init__(self, text, **kwargs):
super().__init__(**kwargs)
535
-
self.escapedText = xml.escape(text)
536
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
552
+
self.escaped_text = xml.escape(text)
553
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
539
-
outputFile.write(self.escapedText)
556
+
output_file.write(self.escaped_text)
class TSpan(_TextContainingElement):
''' A line of text within the Text element. '''
class Title(_TextContainingElement):
546
-
''' A title element.
563
+
'''A title element.
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
565
+
This element can be appended with shape.append_title("Your title!"), which
566
+
can be useful for adding a tooltip or on-hover text display to an element.
class Rectangle(DrawingBasicElement):
557
-
Additional keyword arguments are output as additional arguments to the
558
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
573
+
Additional keyword arguments are output as additional arguments to the SVG
574
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, x, y, width, height, **kwargs):
565
-
super().__init__(x=x, y=y, width=width, height=height,
578
+
super().__init__(x=x, y=y, width=width, height=height, **kwargs)
class Circle(DrawingBasicElement):
571
-
Additional keyword arguments are output as additional properties to the
572
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
583
+
Additional keyword arguments are output as additional arguments to the SVG
584
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, cx, cy, r, **kwargs):
super().__init__(cx=cx, cy=cy, r=r, **kwargs)
class Ellipse(DrawingBasicElement):
584
-
Additional keyword arguments are output as additional properties to the
585
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
593
+
Additional keyword arguments are output as additional arguments to the SVG
594
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, cx, cy, rx, ry, **kwargs):
super().__init__(cx=cx, cy=cy, rx=rx, ry=ry, **kwargs)
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
603
+
In most cases, use Arc instead of ArcLine. ArcLine uses the
604
+
stroke-dasharray SVG property to make the edge of a circle look like an arc.
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:
606
+
Additional keyword arguments are output as additional arguments to the SVG
607
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
609
+
def __init__(self, cx, cy, r, start_deg, end_deg, **kwargs):
610
+
if end_deg - start_deg == 360:
super().__init__(cx, cy, r, **kwargs)
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:
613
+
start_deg, end_deg = (-end_deg) % 360, (-start_deg) % 360
614
+
arc_deg = (end_deg - start_deg) % 360
616
+
return math.radians(deg) * r
617
+
whole_len = 2 * math.pi * r
618
+
if end_deg == start_deg:
613
-
dashes = "0 {}".format(wholeLen+2)
614
-
#elif endDeg >= startDeg:
616
-
startLen = arcLen(startDeg)
617
-
arcLen = arcLen(arcDeg)
618
-
offLen = wholeLen - arcLen
620
-
dashes = "{} {}".format(arcLen, offLen)
622
-
# firstLen = arcLen(endDeg)
623
-
# secondLen = arcLen(360-startDeg)
624
-
# gapLen = wholeLen - firstLen - secondLen
626
-
# dashes = "{} {} {}".format(firstLen, gapLen, secondLen)
620
+
dashes = "0 {}".format(whole_len+2)
622
+
start_len = arc_len(start_deg)
623
+
arc_len = arc_len(arc_deg)
624
+
off_len = whole_len - arc_len
625
+
offset = -start_len
626
+
dashes = "{} {}".format(arc_len, off_len)
super().__init__(cx, cy, r, stroke_dasharray=dashes,
stroke_dashoffset=offset, **kwargs)
class Path(DrawingBasicElement):
631
-
''' An arbitrary path
631
+
'''An arbitrary path.
633
-
Path Supports building an SVG path by calling instance methods
634
-
corresponding to path commands.
633
+
Path Supports building an SVG path by calling instance methods corresponding
636
-
Additional keyword arguments are output as additional properties to the
637
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
636
+
Complete descriptions of path commands:
637
+
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands
639
+
Additional keyword arguments are output as additional arguments to the SVG
640
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, d='', **kwargs):
super().__init__(d=d, **kwargs)
641
-
def append(self, commandStr, *args):
645
+
def append(self, command_str, *args):
if len(self.args['d']) > 0:
643
-
commandStr = ' ' + commandStr
647
+
command_str = ' ' + command_str
645
-
commandStr = commandStr + ','.join(map(str, args))
646
-
self.args['d'] += commandStr
649
+
command_str = command_str + ','.join(map(str, args))
650
+
self.args['d'] += command_str
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')
653
+
'''Start a new curve section from this point.'''
654
+
return self.append('M', x, y)
655
+
def m(self, dx, dy):
656
+
'''Start a new curve section from this point (relative coordinates).'''
657
+
return self.append('m', dx, dy)
659
+
'''Draw a line to this point.'''
660
+
return self.append('L', x, y)
661
+
def l(self, dx, dy):
662
+
'''Draw a line to this point (relative coordinates).'''
663
+
return self.append('l', dx, dy)
665
+
'''Draw a horizontal line to this x coordinate.'''
666
+
return self.append('H', x)
668
+
'''Draw a horizontal line to this relative x coordinate.'''
669
+
return self.append('h', dx)
671
+
'''Draw a horizontal line to this y coordinate.'''
672
+
return self.append('V', y)
674
+
'''Draw a horizontal line to this relative y coordinate.'''
675
+
return self.append('v', dy)
677
+
'''Draw a line back to the previous m or M point.'''
678
+
return self.append('Z')
def C(self, cx1, cy1, cx2, cy2, ex, ey):
658
-
return self.append('C', cx1, -cy1, cx2, -cy2, ex, -ey)
680
+
'''Draw a cubic Bezier curve.'''
681
+
return self.append('C', cx1, cy1, cx2, cy2, ex, ey)
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,
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)
683
+
'''Draw a cubic Bezier curve (relative coordinates).'''
684
+
return self.append('c', cx1, cy1, cx2, cy2, ex, ey)
685
+
def S(self, cx2, cy2, ex, ey):
686
+
'''Draw a cubic Bezier curve, transitioning smoothly from the previous.
688
+
return self.append('S', cx2, cy2, ex, ey)
689
+
def s(self, cx2, cy2, ex, ey):
690
+
'''Draw a cubic Bezier curve, transitioning smoothly from the previous
691
+
(relative coordinates).
693
+
return self.append('s', cx2, cy2, ex, ey)
694
+
def Q(self, cx, cy, ex, ey):
695
+
'''Draw a quadratic Bezier curve.'''
696
+
return self.append('Q', cx, cy, ex, ey)
697
+
def q(self, cx, cy, ex, ey):
698
+
'''Draw a quadratic Bezier curve (relative coordinates).'''
699
+
return self.append('q', cx, cy, ex, ey)
700
+
def T(self, ex, ey):
701
+
'''Draw a quadratic Bezier curve, transitioning soothly from the
704
+
return self.append('T', ex, ey)
705
+
def t(self, ex, ey):
706
+
'''Draw a quadratic Bezier curve, transitioning soothly from the
707
+
previous (relative coordinates).
709
+
return self.append('t', ex, ey)
710
+
def A(self, rx, ry, rot, large_arc, sweep, ex, ey):
711
+
'''Draw a circular or elliptical arc.
714
+
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#elliptical_arc_curve
716
+
return self.append('A', rx, ry, rot, int(bool(large_arc)),
717
+
int(bool(sweep)), ex, ey)
718
+
def a(self, rx, ry, rot, large_arc, sweep, ex, ey):
719
+
'''Draw a circular or elliptical arc (relative coordinates).
722
+
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#elliptical_arc_curve
724
+
return self.append('a', rx, ry, rot, int(bool(large_arc)),
725
+
int(bool(sweep)), ex, ey)
726
+
def arc(self, cx, cy, r, start_deg, end_deg, cw=False, include_m=True,
728
+
'''Draw a circular arc, controlled by center, radius, and start/end
731
+
large_arc = (end_deg - start_deg) % 360 > 180
732
+
start_rad, end_rad = start_deg*math.pi/180, end_deg*math.pi/180
733
+
sx, sy = r*math.cos(start_rad), -r*math.sin(start_rad)
734
+
ex, ey = r*math.cos(end_rad), -r*math.sin(end_rad)
684
-
return self.A(r, r, 0, largeArc ^ cw, cw, cx+ex, cy+ey)
739
+
return self.A(r, r, 0, large_arc ^ cw, cw, cx+ex, cy+ey)
687
-
''' A sequence of connected lines (or a polygon)
742
+
'''A sequence of connected lines (or a polygon).
689
-
Additional keyword arguments are output as additional properties to the
690
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
744
+
Additional keyword arguments are output as additional arguments to the SVG
745
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, sx, sy, *points, close=False, **kwargs):
super().__init__(d='', **kwargs)
···
703
-
Additional keyword arguments are output as additional properties to the
704
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
759
+
Additional keyword arguments are output as additional arguments to the SVG
760
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, sx, sy, ex, ey, **kwargs):
super().__init__(sx, sy, ex, ey, close=False, **kwargs)
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):
768
+
Additional keyword arguments are output as additional arguments to the SVG
769
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
771
+
def __init__(self, cx, cy, r, start_deg, end_deg, cw=False, **kwargs):
super().__init__(d='', **kwargs)
715
-
self.arc(cx, cy, r, startDeg, endDeg, cw=cw, includeM=True)
773
+
self.arc(cx, cy, r, start_deg, end_deg, cw=cw, include_m=True)