···
···
import xml.sax.saxutils as xml
from collections import defaultdict
-
elementsModule = sys.modules[__name__]
-
# TODO: Support drawing ellipses without manually using Path
-
def writeXmlNodeArgs(args, outputFile):
for k, v in args.items():
if isinstance(v, DrawingElement):
-
v = 'url(#{})'.format(v.id)
-
outputFile.write(' {}="{}"'.format(k,v))
-
''' Base class for drawing elements
-
Subclasses must implement writeSvgElement '''
-
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
raise NotImplementedError('Abstract base class')
-
def getLinkedElems(self):
-
def writeSvgDefs(self, idGen, isDuplicate, outputFile, dryRun):
-
for defn in self.getSvgDefs():
-
if isDuplicate(defn): continue
-
defn.writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
-
defn.writeSvgElement(idGen, isDuplicate, outputFile, dryRun,
class DrawingBasicElement(DrawingElement):
-
''' Base class for SVG drawing elements that are a single node with no
def __init__(self, **args):
for k, v in args.items():
···
-
self.orderedChildren = defaultdict(list)
-
def checkChildrenAllowed(self):
-
if not self.hasContent:
-
'{} does not support children'.format(type(self)))
-
''' Returns self.children and self.orderedChildren as a single list. '''
output = list(self.children)
-
for z in sorted(self.orderedChildren):
-
output.extend(self.orderedChildren[z])
return self.args.get('id', None)
-
self.args['id'] = newId
-
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
-
children = self.allChildren()
-
if isDuplicate(self) and self.id is None:
-
for elem in self.getLinkedElems():
-
self.writeContent(idGen, isDuplicate, outputFile, dryRun)
-
self.writeChildrenContent(idGen, isDuplicate, outputFile,
-
if isDuplicate(self) and not forceDup:
-
outputFile.write('<use xlink:href="#{}" />'.format(self.id))
-
outputFile.write(self.TAG_NAME)
-
writeXmlNodeArgs(self.args, outputFile)
-
if not self.hasContent and not children:
-
outputFile.write(' />')
-
self.writeContent(idGen, isDuplicate, outputFile, dryRun)
-
self.writeChildrenContent(idGen, isDuplicate, outputFile,
-
outputFile.write(self.TAG_NAME)
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
-
''' Override in a subclass to add data between the start and end
-
tags. This will not be called if hasContent is False. '''
raise RuntimeError('This element has no content')
-
def writeChildrenContent(self, idGen, isDuplicate, outputFile, dryRun):
-
''' Override in a subclass to add data between the start and end
-
tags. This will not be called if hasContent is False. '''
-
children = self.allChildren()
-
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
-
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
return [v for v in self.args.values()
if isinstance(v, DrawingElement)]
-
def writeSvgDefs(self, idGen, isDuplicate, outputFile, dryRun):
-
super().writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
-
for child in self.allChildren():
-
child.writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
if isinstance(other, type(self)):
return (self.TAG_NAME == other.TAG_NAME and
self.args == other.args and
self.children == other.children and
-
self.orderedChildren == other.orderedChildren)
-
def appendAnim(self, animateElement):
-
self.children.append(animateElement)
-
def extendAnim(self, animateIterable):
-
self.children.extend(animateIterable)
-
def appendTitle(self, text, **kwargs):
self.children.append(Title(text, **kwargs))
class DrawingParentElement(DrawingBasicElement):
-
''' Base class for SVG elements that can have child nodes '''
-
def __init__(self, children=(), orderedChildren=None, **args):
self.children = list(children)
-
self.orderedChildren.update(
-
(z, list(v)) for z, v in orderedChildren.items())
-
if self.children or self.orderedChildren:
-
self.checkChildrenAllowed()
def draw(self, obj, *, z=None, **kwargs):
-
if not hasattr(obj, 'writeSvgElement'):
-
elements = obj.toDrawables(elements=elementsModule, **kwargs)
-
self.extend(elements, z=z)
def append(self, element, *, z=None):
-
self.checkChildrenAllowed()
-
self.orderedChildren[z].append(element)
self.children.append(element)
def extend(self, iterable, *, z=None):
-
self.checkChildrenAllowed()
-
self.orderedChildren[z].extend(iterable)
self.children.extend(iterable)
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
class NoElement(DrawingElement):
''' A drawing element that has no effect '''
-
def __init__(self): pass
-
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
if isinstance(other, type(self)):
···
class Group(DrawingParentElement):
-
''' A group of drawing elements
-
Any transform will apply to its children and other attributes will be
-
inherited by its children. '''
-
''' Any any SVG code to insert into the output. '''
-
def __init__(self, content, defs=(), **kwargs):
-
super().__init__(**kwargs)
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
-
outputFile.write(self.content)
class Use(DrawingBasicElement):
-
''' A copy of another element
-
The other element becomes an SVG def shared between all Use elements
-
def __init__(self, otherElem, x, y, **kwargs):
-
if isinstance(otherElem, str) and not otherElem.startswith('#'):
-
otherElem = '#' + otherElem
-
super().__init__(xlink__href=otherElem, x=x, y=y, **kwargs)
class Animate(DrawingBasicElement):
-
''' Animation for a specific property of another element
-
This should be added as a child of the element to animate. Otherwise
-
the other element and this element must both be added to the drawing.
def __init__(self, attributeName, dur, from_or_values=None, to=None,
-
begin=None, otherElem=None, **kwargs):
-
if isinstance(otherElem, str) and not otherElem.startswith('#'):
-
otherElem = '#' + otherElem
kwargs.update(attributeName=attributeName, to=to, dur=dur, begin=begin)
kwargs.setdefault('values', values)
kwargs.setdefault('from_', from_)
-
super().__init__(xlink__href=otherElem, **kwargs)
return [v for k, v in self.args.items()
if isinstance(v, DrawingElement)
-
def getLinkedElems(self):
-
return (self.args['xlink:href'],)
class _Mpath(DrawingBasicElement):
-
''' Used by AnimateMotion '''
-
def __init__(self, otherPath, **kwargs):
-
super().__init__(xlink__href=otherPath, **kwargs)
class AnimateMotion(Animate):
-
''' Animation for the motion another element along a path
-
This should be added as a child of the element to animate. Otherwise
-
the 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,
-
otherElem=None, **kwargs):
if isinstance(path, DrawingElement):
kwargs.setdefault('attributeName', None)
super().__init__(dur=dur, from_or_values=from_or_values, to=to,
-
begin=begin, path=path, otherElem=otherElem, **kwargs)
-
self.children.append(_Mpath(pathElem))
class AnimateTransform(Animate):
-
''' Animation for the transform property of another element
-
This should be added as a child of the element to animate. Otherwise
-
the 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,
-
attributeName='transform', otherElem=None, **kwargs):
super().__init__(attributeName, dur=dur, from_or_values=from_or_values,
-
to=to, begin=begin, type=type, otherElem=otherElem,
-
''' Animation for a specific property of another element that sets the new
-
value without a transition.
-
This should be added as a child of the element to animate. Otherwise
-
the other element and this element must both be added to the drawing.
def __init__(self, attributeName, dur, to=None, begin=None,
-
otherElem=None, **kwargs):
super().__init__(attributeName, dur=dur, from_or_values=None,
-
to=to, begin=begin, otherElem=otherElem, **kwargs)
-
''' Animation configuration specifying when it is safe to discard another
-
element. E.g. when it will no longer be visible after an animation.
-
This should be added as a child of the element to animate. Otherwise
-
the 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)
-
super().__init__(from_or_values=None, begin=begin, otherElem=None,
class Image(DrawingBasicElement):
-
''' A linked or embedded raster image '''
···
MIME_DEFAULT = 'image/png'
def __init__(self, x, y, width, height, path=None, data=None, embed=False,
-
mimeType=None, **kwargs):
-
''' Specify either the path or data argument. If path is used and
-
embed is 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')
-
if mimeType is None and path is not None:
ext = os.path.splitext(path)[1].lower()
-
mimeType = self.MIME_MAP[ext]
-
mimeType = self.MIME_DEFAULT
warnings.warn('Unknown image file type "{}"'.format(ext),
-
mimeType = self.MIME_DEFAULT
warnings.warn('Unspecified image type; assuming png', Warning)
···
-
encData = base64.b64encode(data).decode(encoding='ascii')
-
uri = 'data:{};base64,{}'.format(mimeType, encData)
-
super().__init__(x=x, y=-y-height, width=width, height=height,
-
xlink__href=uri, **kwargs)
class Text(DrawingParentElement):
-
Additional keyword arguments are output as additional arguments to the
-
SVG node e.g. fill='red', font_size=20, text_anchor='middle',
-
CairoSVG bug with letter spacing text on a path: The first two letters
-
are always spaced as if letter_spacing=1. '''
-
def __new__(cls, text, *args, path=None, id=None, _skipCheck=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.
-
if path is not None and not _skipCheck:
-
text, _ = cls._handleTextArgument(text, True)
for i, line in enumerate(text):
subtext = [None] * len(text)
-
g.append(Text(subtext, *args, path=path, _skipCheck=True,
return super().__new__(cls)
-
def __init__(self, text, fontSize, x=None, y=None, *, center=False,
-
valign=None, lineHeight=1, lineOffset=0, path=None,
-
startOffset=None, pathArgs=None, tspanArgs=None,
-
cairoFix=True, _skipCheck=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")
-
if startOffset is not None:
-
pathArgs.setdefault('startOffset', startOffset)
-
onPath = path is not None
-
text, singleLine = self._handleTextArgument(text, forceMulti=onPath)
-
if center and valign is None:
-
centerCompat = singleLine and not onPath
-
if center and kwargs.get('text_anchor') is None:
-
kwargs['text_anchor'] = 'middle'
-
if centerCompat: # Backwards compatible centering
-
lineOffset += 0.5 * center
-
lineOffset += 0.4 - lineHeight * (numLines - 1) / 2
-
elif valign == 'bottom':
-
lineOffset += -lineHeight * (numLines - 1)
-
dy = '{}em'.format(lineOffset)
-
kwargs.setdefault('dy', dy)
# Text alignment on a path
if kwargs.get('text_anchor') == 'start':
-
pathArgs.setdefault('startOffset', '0')
elif kwargs.get('text_anchor') == 'middle':
-
pathArgs.setdefault('startOffset', '50%')
elif kwargs.get('text_anchor') == 'end':
-
if cairoFix and 'startOffset' not in pathArgs:
# Fix CairoSVG not drawing the last character with aligned
-
tspanArgs.setdefault('dx', -1)
-
pathArgs.setdefault('startOffset', '100%')
-
super().__init__(x=x, y=y, font_size=fontSize, **kwargs)
-
self.escapedText = xml.escape(text[0])
# Add elements for each line of text
for i, line in enumerate(text):
-
dy = '{}em'.format(lineOffset if i == 0 else lineHeight)
-
self.appendLine(line, x=x, dy=dy, **tspanArgs)
-
self._textPath = _TextPath(path, **pathArgs)
assert sum(bool(line) for line in text) <= 1, (
'Logic error, __new__ should handle multi-line paths')
for i, line in enumerate(text):
-
dy = '{}em'.format(lineOffset + i*lineHeight)
-
tspan = TSpan(line, dy=dy, **tspanArgs)
-
self._textPath.append(tspan)
-
self.append(self._textPath)
-
def _handleTextArgument(text, forceMulti=False):
# Handle multi-line text (contains '\n' or is a list of strings)
-
singleLine = isinstance(text, str)
if isinstance(text, str):
-
singleLine = '\n' not in text and not forceMulti
text = tuple(text.splitlines())
-
return text, singleLine
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
-
outputFile.write(self.escapedText)
-
def writeChildrenContent(self, idGen, isDuplicate, outputFile, dryRun):
-
''' Override in a subclass to add data between the start and end
-
tags. This will not be called if hasContent is False. '''
-
children = self.allChildren()
-
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
-
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
-
def appendLine(self, line, **kwargs):
-
if self._textPath 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)
-
self.escapedText = xml.escape(text)
-
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
-
outputFile.write(self.escapedText)
class TSpan(_TextContainingElement):
''' A line of text within the Text element. '''
class Title(_TextContainingElement):
-
This element can be appended with shape.appendTitle("Your title!"),
-
which can be useful for adding a tooltip or on-hover text display
class Rectangle(DrawingBasicElement):
-
Additional keyword arguments are output as additional arguments to the
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
def __init__(self, x, y, width, height, **kwargs):
-
super().__init__(x=x, y=y, width=width, height=height,
class Circle(DrawingBasicElement):
-
Additional keyword arguments are output as additional properties to the
-
SVG 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):
-
Additional keyword arguments are output as additional properties to the
-
SVG 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)
-
In most cases, use Arc instead of ArcLine. ArcLine uses the
-
stroke-dasharray SVG property to make the edge of a circle look like
-
Additional keyword arguments are output as additional arguments to the
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
-
def __init__(self, cx, cy, r, startDeg, endDeg, **kwargs):
-
if endDeg - startDeg == 360:
super().__init__(cx, cy, r, **kwargs)
-
startDeg, endDeg = (-endDeg) % 360, (-startDeg) % 360
-
arcDeg = (endDeg - startDeg) % 360
-
def arcLen(deg): return math.radians(deg) * r
-
wholeLen = 2 * math.pi * r
-
dashes = "0 {}".format(wholeLen+2)
-
#elif endDeg >= startDeg:
-
startLen = arcLen(startDeg)
-
arcLen = arcLen(arcDeg)
-
offLen = wholeLen - arcLen
-
dashes = "{} {}".format(arcLen, offLen)
-
# firstLen = arcLen(endDeg)
-
# secondLen = arcLen(360-startDeg)
-
# gapLen = wholeLen - firstLen - secondLen
-
# dashes = "{} {} {}".format(firstLen, gapLen, secondLen)
super().__init__(cx, cy, r, stroke_dasharray=dashes,
stroke_dashoffset=offset, **kwargs)
class Path(DrawingBasicElement):
-
Path Supports building an SVG path by calling instance methods
-
corresponding to path commands.
-
Additional keyword arguments are output as additional properties to the
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
def __init__(self, d='', **kwargs):
super().__init__(d=d, **kwargs)
-
def append(self, commandStr, *args):
if len(self.args['d']) > 0:
-
commandStr = ' ' + commandStr
-
commandStr = commandStr + ','.join(map(str, args))
-
self.args['d'] += commandStr
-
def M(self, x, y): return self.append('M', x, -y)
-
def m(self, dx, dy): return self.append('m', dx, -dy)
-
def L(self, x, y): return self.append('L', x, -y)
-
def l(self, dx, dy): return self.append('l', dx, -dy)
-
def H(self, x): return self.append('H', x)
-
def h(self, dx): return self.append('h', dx)
-
def V(self, y): return self.append('V', -y)
-
def v(self, dy): return self.append('v', -dy)
-
def Z(self): return self.append('Z')
def C(self, cx1, cy1, cx2, cy2, ex, ey):
-
return self.append('C', cx1, -cy1, cx2, -cy2, ex, -ey)
def c(self, cx1, cy1, cx2, cy2, ex, ey):
-
return self.append('c', cx1, -cy1, cx2, -cy2, ex, -ey)
-
def S(self, cx2, cy2, ex, ey): return self.append('S', cx2, -cy2, ex, -ey)
-
def s(self, cx2, cy2, ex, ey): return self.append('s', cx2, -cy2, ex, -ey)
-
def Q(self, cx, cy, ex, ey): return self.append('Q', cx, -cy, ex, -ey)
-
def q(self, cx, cy, ex, ey): return self.append('q', cx, -cy, ex, -ey)
-
def T(self, ex, ey): return self.append('T', ex, -ey)
-
def t(self, ex, ey): return self.append('t', ex, -ey)
-
def A(self, rx, ry, rot, largeArc, sweep, ex, ey):
-
return self.append('A', rx, ry, rot, int(bool(largeArc)),
-
int(bool(sweep)), ex, -ey)
-
def a(self, rx, ry, rot, largeArc, sweep, ex, ey):
-
return self.append('a', rx, ry, rot, int(bool(largeArc)),
-
int(bool(sweep)), ex, -ey)
-
def arc(self, cx, cy, r, startDeg, endDeg, cw=False, includeM=True,
-
''' Uses A() to draw a circular arc '''
-
largeArc = (endDeg - startDeg) % 360 > 180
-
startRad, endRad = startDeg*math.pi/180, endDeg*math.pi/180
-
sx, sy = r*math.cos(startRad), r*math.sin(startRad)
-
ex, ey = r*math.cos(endRad), r*math.sin(endRad)
-
return self.A(r, r, 0, largeArc ^ cw, cw, cx+ex, cy+ey)
-
''' A sequence of connected lines (or a polygon)
-
Additional keyword arguments are output as additional properties to the
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
def __init__(self, sx, sy, *points, close=False, **kwargs):
super().__init__(d='', **kwargs)
···
-
Additional keyword arguments are output as additional properties to the
-
SVG 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)
-
Additional keyword arguments are output as additional properties to the
-
SVG node e.g. fill="red", stroke="#ff4477", stroke_width=2. '''
-
def __init__(self, cx, cy, r, startDeg, endDeg, cw=False, **kwargs):
super().__init__(d='', **kwargs)
-
self.arc(cx, cy, r, startDeg, endDeg, cw=cw, includeM=True)
···
···
import xml.sax.saxutils as xml
from collections import defaultdict
+
from . import defs, url_encode
+
def write_xml_node_args(args, output_file, id_map=None):
for k, v in args.items():
if isinstance(v, DrawingElement):
+
if id_map and id(v) in id_map:
+
mapped_id = id_map[id(v)]
+
v = '#{}'.format(mapped_id)
+
v = 'url(#{})'.format(mapped_id)
+
output_file.write(' {}="{}"'.format(k,v))
+
'''Base class for drawing elements.
+
Subclasses must implement write_svg_element.
+
def write_svg_element(self, id_map, is_duplicate, output_file, dry_run,
raise NotImplementedError('Abstract base class')
+
def get_svg_defs(self):
+
def get_linked_elems(self):
+
def write_svg_defs(self, id_map, is_duplicate, output_file, dry_run):
+
for defn in self.get_svg_defs():
+
defn.write_svg_defs(id_map, is_duplicate, output_file, dry_run)
+
defn.write_svg_element(
+
id_map, is_duplicate, output_file, dry_run, force_dup=True)
+
output_file.write('\n')
class DrawingBasicElement(DrawingElement):
+
'''Base class for SVG drawing elements that are a single node with no child
def __init__(self, **args):
for k, v in args.items():
···
+
self.ordered_children = defaultdict(list)
+
def check_children_allowed(self):
+
if not self.has_content:
+
'{} does not support children'.format(type(self)))
+
def all_children(self):
+
'''Return self.children and self.ordered_children as a single list.'''
output = list(self.children)
+
for z in sorted(self.ordered_children):
+
output.extend(self.ordered_children[z])
return self.args.get('id', None)
+
def write_svg_element(self, id_map, is_duplicate, output_file, dry_run,
+
children = self.all_children()
+
if is_duplicate(self) and self.id is None:
+
for elem in self.get_linked_elems():
+
self.write_content(id_map, is_duplicate, output_file, dry_run)
+
self.write_children_content(
+
id_map, is_duplicate, output_file, dry_run)
+
if is_duplicate(self) and not force_dup:
+
if id_map and id(self) in id_map:
+
mapped_id = id_map[id(self)]
+
output_file.write('<use xlink:href="#{}" />'.format(mapped_id))
+
output_file.write(self.TAG_NAME)
+
override_args = self.args
+
override_args = dict(override_args)
+
override_args['id'] = id_map[id(self)]
+
write_xml_node_args(override_args, output_file, id_map)
+
if not self.has_content and not children:
+
output_file.write(' />')
+
self.write_content(id_map, is_duplicate, output_file, dry_run)
+
self.write_children_content(
+
id_map, is_duplicate, output_file, dry_run)
+
output_file.write('</')
+
output_file.write(self.TAG_NAME)
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
+
'''Override in a subclass to add data between the start and end tags.
+
This will not be called if has_content is False.
raise RuntimeError('This element has no content')
+
def write_children_content(self, id_map, is_duplicate, output_file,
+
'''Override in a subclass to add data between the start and end tags.
+
This will not be called if has_content is False.
+
children = self.all_children()
+
child.write_svg_element(
+
id_map, is_duplicate, output_file, dry_run)
+
output_file.write('\n')
+
child.write_svg_element(id_map, is_duplicate, output_file, dry_run)
+
output_file.write('\n')
+
def get_svg_defs(self):
return [v for v in self.args.values()
if isinstance(v, DrawingElement)]
+
def write_svg_defs(self, id_map, is_duplicate, output_file, dry_run):
+
super().write_svg_defs(id_map, is_duplicate, output_file, dry_run)
+
for child in self.all_children():
+
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
+
self.ordered_children == other.ordered_children)
+
def append_anim(self, animate_element):
+
self.children.append(animate_element)
+
def extend_anim(self, animate_iterable):
+
self.children.extend(animate_iterable)
+
def append_title(self, text, **kwargs):
self.children.append(Title(text, **kwargs))
class DrawingParentElement(DrawingBasicElement):
+
'''Base class for SVG elements that can have child nodes.'''
+
def __init__(self, children=(), ordered_children=None, **args):
self.children = list(children)
+
self.ordered_children.update(
+
(z, list(v)) for z, v in ordered_children.items())
+
if self.children or self.ordered_children:
+
self.check_children_allowed()
def draw(self, obj, *, z=None, **kwargs):
+
if not hasattr(obj, 'write_svg_element'):
+
elements = obj.to_drawables(**kwargs)
+
if hasattr(elements, 'write_svg_element'):
+
self.append(elements, z=z)
+
self.extend(elements, z=z)
def append(self, element, *, z=None):
+
self.check_children_allowed()
+
self.ordered_children[z].append(element)
self.children.append(element)
def extend(self, iterable, *, z=None):
+
self.check_children_allowed()
+
self.ordered_children[z].extend(iterable)
self.children.extend(iterable)
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
class NoElement(DrawingElement):
''' A drawing element that has no effect '''
+
def write_svg_element(self, id_map, is_duplicate, output_file, dry_run,
if isinstance(other, type(self)):
···
class Group(DrawingParentElement):
+
'''A group of drawing elements.
+
Any transform will apply to its children and other attributes will be
+
inherited by its children.
+
class Raw(DrawingBasicElement):
+
'''Raw unescaped text to include in the SVG output.
+
Special XML characters like '<' and '&' in the content may have unexpected
+
effects or completely break the resulting SVG.
+
def __init__(self, content, defs=()):
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
+
output_file.write(self.content)
+
def get_svg_defs(self):
+
def check_children_allowed(self):
+
raise RuntimeError('{} does not support children'.format(type(self)))
class Use(DrawingBasicElement):
+
'''A copy of another element, drawn at a given position
+
The referenced element becomes an SVG def shared between all Use elements
+
that reference it. Useful for drawings with many copies of similar shapes.
+
Additional arguments like `fill='red'` will be used as the default for this
+
def __init__(self, other_elem, x, y, **kwargs):
+
if isinstance(other_elem, str) and not other_elem.startswith('#'):
+
other_elem = '#' + other_elem
+
super().__init__(xlink__href=other_elem, x=x, y=y, **kwargs)
class Animate(DrawingBasicElement):
+
'''Animation for a specific property of another element.
+
This should be added as a child of the element to animate. Otherwise the
+
referenced other element and this element must both be added to the drawing.
+
- repeatCount: 0, 1, ..., 'indefinite'
def __init__(self, attributeName, dur, from_or_values=None, to=None,
+
begin=None, other_elem=None, **kwargs):
+
if isinstance(other_elem, str) and not other_elem.startswith('#'):
+
other_elem = '#' + other_elem
kwargs.update(attributeName=attributeName, to=to, dur=dur, begin=begin)
kwargs.setdefault('values', values)
kwargs.setdefault('from_', from_)
+
super().__init__(xlink__href=other_elem, **kwargs)
+
def get_svg_defs(self):
return [v for k, v in self.args.items()
if isinstance(v, DrawingElement)
+
def get_linked_elems(self):
+
elem = self.args['xlink:href']
+
return (elem,) if elem is not None else ()
class _Mpath(DrawingBasicElement):
+
'''Used by AnimateMotion.'''
+
def __init__(self, other_path, **kwargs):
+
super().__init__(xlink__href=other_path, **kwargs)
class AnimateMotion(Animate):
+
'''Animation for the motion of another element along a path.
+
This should be added as a child of the element to animate. Otherwise the
+
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,
+
other_elem=None, **kwargs):
if isinstance(path, DrawingElement):
kwargs.setdefault('attributeName', None)
super().__init__(dur=dur, from_or_values=from_or_values, to=to,
+
begin=begin, path=path, other_elem=other_elem,
+
self.children.append(_Mpath(path_elem))
class AnimateTransform(Animate):
+
'''Animation for the transform property of another element.
+
This should be added as a child of the element to animate. Otherwise the
+
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,
+
attributeName='transform', other_elem=None, **kwargs):
super().__init__(attributeName, dur=dur, from_or_values=from_or_values,
+
to=to, begin=begin, type=type, other_elem=other_elem,
+
'''Animation for a specific property of another element that sets the new
+
value without a transition.
+
This should be added as a child of the element to animate. Otherwise the
+
referenced other element and this element must both be added to the drawing.
def __init__(self, attributeName, dur, to=None, begin=None,
+
other_elem=None, **kwargs):
super().__init__(attributeName, dur=dur, from_or_values=None,
+
to=to, begin=begin, other_elem=other_elem, **kwargs)
+
'''Animation configuration specifying when it is safe to discard another
+
Use this when an element will no longer be visible after an animation.
+
This should be added as a child of the element to animate. Otherwise the
+
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)
+
super().__init__(from_or_values=None, begin=begin, other_elem=None,
class Image(DrawingBasicElement):
+
'''A linked or embedded image.'''
···
MIME_DEFAULT = 'image/png'
def __init__(self, x, y, width, height, path=None, data=None, embed=False,
+
mime_type=None, **kwargs):
+
Specify either the path or data argument. If path is used and embed is
+
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')
+
if mime_type is None and path is not None:
ext = os.path.splitext(path)[1].lower()
+
mime_type = self.MIME_MAP[ext]
+
mime_type = self.MIME_DEFAULT
warnings.warn('Unknown image file type "{}"'.format(ext),
+
mime_type = self.MIME_DEFAULT
warnings.warn('Unspecified image type; assuming png', Warning)
···
+
uri = url_encode.bytes_as_data_uri(data, mime=mime_type)
+
super().__init__(x=x, y=y, width=width, height=height, xlink__href=uri,
class Text(DrawingParentElement):
+
'''A line or multiple lines of text, optionally placed along a path.
+
Additional keyword arguments are output as additional arguments to the SVG
+
node e.g. fill='red', font_size=20, letter_spacing=1.5.
+
- text_anchor: start, middle, end
+
- dominant_baseline: auto, central, middle, hanging, text-top, ...
+
See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text
+
CairoSVG bug with letter spacing text on a path: The first two letters are
+
always spaced as if letter_spacing=1.
+
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.
+
if path is not None and not _skip_check:
+
text, _ = cls._handle_text_argument(text, True)
for i, line in enumerate(text):
subtext = [None] * len(text)
+
g.append(Text(subtext, *args, path=path, _skip_check=True,
return super().__new__(cls)
+
def __init__(self, text, font_size, x=None, y=None, *, center=False,
+
line_height=1, line_offset=0, path=None, start_offset=None,
+
path_args=None, tspan_args=None, cairo_fix=True,
+
_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")
+
if start_offset is not None:
+
path_args.setdefault('startOffset', start_offset)
+
on_path = path is not None
+
text, single_line = self._handle_text_argument(
+
text, force_multi=on_path)
+
kwargs.setdefault('text_anchor', 'middle')
+
if path is None and single_line:
+
kwargs.setdefault('dominant_baseline', 'central')
+
line_offset -= line_height * (num_lines - 1) / 2
# Text alignment on a path
if kwargs.get('text_anchor') == 'start':
+
path_args.setdefault('startOffset', '0')
elif kwargs.get('text_anchor') == 'middle':
+
path_args.setdefault('startOffset', '50%')
elif kwargs.get('text_anchor') == 'end':
+
if cairo_fix and 'startOffset' not in path_args:
# Fix CairoSVG not drawing the last character with aligned
+
tspan_args.setdefault('dx', -1)
+
path_args.setdefault('startOffset', '100%')
+
super().__init__(x=x, y=y, font_size=font_size, **kwargs)
+
self.escaped_text = xml.escape(text[0])
# Add elements for each line of text
for i, line in enumerate(text):
+
dy = '{}em'.format(line_offset if i == 0 else line_height)
+
self.append_line(line, x=x, dy=dy, **tspan_args)
+
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):
+
dy = '{}em'.format(line_offset + i*line_height)
+
tspan = TSpan(line, dy=dy, **tspan_args)
+
self._text_path.append(tspan)
+
self.append(self._text_path)
+
def _handle_text_argument(text, force_multi=False):
# Handle multi-line text (contains '\n' or is a list of strings)
if isinstance(text, str):
+
single_line = '\n' not in text and not force_multi
text = tuple(text.splitlines())
+
return text, single_line
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
+
output_file.write(self.escaped_text)
+
def write_children_content(self, id_map, is_duplicate, output_file,
+
children = self.all_children()
+
child.write_svg_element(id_map, is_duplicate, output_file, dry_run)
+
def append_line(self, line, **kwargs):
+
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)
+
self.escaped_text = xml.escape(text)
+
def write_content(self, id_map, is_duplicate, output_file, dry_run):
+
output_file.write(self.escaped_text)
class TSpan(_TextContainingElement):
''' A line of text within the Text element. '''
class Title(_TextContainingElement):
+
This element can be appended with shape.append_title("Your title!"), which
+
can be useful for adding a tooltip or on-hover text display to an element.
class Rectangle(DrawingBasicElement):
+
Additional keyword arguments are output as additional arguments to the SVG
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, x, y, width, height, **kwargs):
+
super().__init__(x=x, y=y, width=width, height=height, **kwargs)
class Circle(DrawingBasicElement):
+
Additional keyword arguments are output as additional arguments to the SVG
+
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):
+
Additional keyword arguments are output as additional arguments to the SVG
+
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)
+
In most cases, use Arc instead of ArcLine. ArcLine uses the
+
stroke-dasharray SVG property to make the edge of a circle look like an arc.
+
Additional keyword arguments are output as additional arguments to the SVG
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
+
def __init__(self, cx, cy, r, start_deg, end_deg, **kwargs):
+
if end_deg - start_deg == 360:
super().__init__(cx, cy, r, **kwargs)
+
start_deg, end_deg = (-end_deg) % 360, (-start_deg) % 360
+
arc_deg = (end_deg - start_deg) % 360
+
return math.radians(deg) * r
+
whole_len = 2 * math.pi * r
+
if end_deg == start_deg:
+
dashes = "0 {}".format(whole_len+2)
+
start_len = arc_len(start_deg)
+
arc_len = arc_len(arc_deg)
+
off_len = whole_len - arc_len
+
dashes = "{} {}".format(arc_len, off_len)
super().__init__(cx, cy, r, stroke_dasharray=dashes,
stroke_dashoffset=offset, **kwargs)
class Path(DrawingBasicElement):
+
Path Supports building an SVG path by calling instance methods corresponding
+
Complete descriptions of path commands:
+
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands
+
Additional keyword arguments are output as additional arguments to the SVG
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, d='', **kwargs):
super().__init__(d=d, **kwargs)
+
def append(self, command_str, *args):
if len(self.args['d']) > 0:
+
command_str = ' ' + command_str
+
command_str = command_str + ','.join(map(str, args))
+
self.args['d'] += command_str
+
'''Start a new curve section from this point.'''
+
return self.append('M', x, y)
+
'''Start a new curve section from this point (relative coordinates).'''
+
return self.append('m', dx, dy)
+
'''Draw a line to this point.'''
+
return self.append('L', x, y)
+
'''Draw a line to this point (relative coordinates).'''
+
return self.append('l', dx, dy)
+
'''Draw a horizontal line to this x coordinate.'''
+
return self.append('H', x)
+
'''Draw a horizontal line to this relative x coordinate.'''
+
return self.append('h', dx)
+
'''Draw a horizontal line to this y coordinate.'''
+
return self.append('V', y)
+
'''Draw a horizontal line to this relative y coordinate.'''
+
return self.append('v', dy)
+
'''Draw a line back to the previous m or M point.'''
+
return self.append('Z')
def C(self, cx1, cy1, cx2, cy2, ex, ey):
+
'''Draw a cubic Bezier curve.'''
+
return self.append('C', cx1, cy1, cx2, cy2, ex, ey)
def c(self, cx1, cy1, cx2, cy2, ex, ey):
+
'''Draw a cubic Bezier curve (relative coordinates).'''
+
return self.append('c', cx1, cy1, cx2, cy2, ex, ey)
+
def S(self, cx2, cy2, ex, ey):
+
'''Draw a cubic Bezier curve, transitioning smoothly from the previous.
+
return self.append('S', cx2, cy2, ex, ey)
+
def s(self, cx2, cy2, ex, ey):
+
'''Draw a cubic Bezier curve, transitioning smoothly from the previous
+
(relative coordinates).
+
return self.append('s', cx2, cy2, ex, ey)
+
def Q(self, cx, cy, ex, ey):
+
'''Draw a quadratic Bezier curve.'''
+
return self.append('Q', cx, cy, ex, ey)
+
def q(self, cx, cy, ex, ey):
+
'''Draw a quadratic Bezier curve (relative coordinates).'''
+
return self.append('q', cx, cy, ex, ey)
+
'''Draw a quadratic Bezier curve, transitioning soothly from the
+
return self.append('T', ex, ey)
+
'''Draw a quadratic Bezier curve, transitioning soothly from the
+
previous (relative coordinates).
+
return self.append('t', ex, ey)
+
def A(self, rx, ry, rot, large_arc, sweep, ex, ey):
+
'''Draw a circular or elliptical arc.
+
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#elliptical_arc_curve
+
return self.append('A', rx, ry, rot, int(bool(large_arc)),
+
int(bool(sweep)), ex, ey)
+
def a(self, rx, ry, rot, large_arc, sweep, ex, ey):
+
'''Draw a circular or elliptical arc (relative coordinates).
+
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#elliptical_arc_curve
+
return self.append('a', rx, ry, rot, int(bool(large_arc)),
+
int(bool(sweep)), ex, ey)
+
def arc(self, cx, cy, r, start_deg, end_deg, cw=False, include_m=True,
+
'''Draw a circular arc, controlled by center, radius, and start/end
+
large_arc = (end_deg - start_deg) % 360 > 180
+
start_rad, end_rad = start_deg*math.pi/180, end_deg*math.pi/180
+
sx, sy = r*math.cos(start_rad), -r*math.sin(start_rad)
+
ex, ey = r*math.cos(end_rad), -r*math.sin(end_rad)
+
return self.A(r, r, 0, large_arc ^ cw, cw, cx+ex, cy+ey)
+
'''A sequence of connected lines (or a polygon).
+
Additional keyword arguments are output as additional arguments to the SVG
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
def __init__(self, sx, sy, *points, close=False, **kwargs):
super().__init__(d='', **kwargs)
···
+
Additional keyword arguments are output as additional arguments to the SVG
+
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)
+
Additional keyword arguments are output as additional arguments to the SVG
+
node e.g. fill="red", stroke="#ff4477", stroke_width=2.
+
def __init__(self, cx, cy, r, start_deg, end_deg, cw=False, **kwargs):
super().__init__(d='', **kwargs)
+
self.arc(cx, cy, r, start_deg, end_deg, cw=cw, include_m=True)