You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6806 lines
230 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# -*- coding: ISO-8859-1 -*-
from __future__ import division
import re
try:
from collections.abc import MutableSequence # noqa
except ImportError:
from collections import MutableSequence # noqa
from copy import copy
from math import *
from xml.etree.ElementTree import iterparse
try:
from math import tau
except ImportError:
tau = pi * 2
"""
The path elements are derived from regebro's svg.path project ( https://github.com/regebro/svg.path ) with
some of the math from mathandy's svgpathtools project ( https://github.com/mathandy/svgpathtools ).
The goal is to provide svg like path objects and structures. The svg standard 1.1 and elements of 2.0 will
be used to provide much of the decisions within path objects. Such that if there is a question on
implementation if the SVG documentation has a methodology it should be used.
Though not required the SVGImage class acquires new functionality if provided with PIL/Pillow as an import
and the Arc can do exact arc calculations if scipy is installed.
"""
MIN_DEPTH = 5
ERROR = 1e-12
max_depth = 0
# SVG STATIC VALUES
DEFAULT_PPI = 96.0
SVG_NAME_TAG = 'svg'
SVG_ATTR_VERSION = 'version'
SVG_VALUE_VERSION = '1.1'
SVG_ATTR_XMLNS = 'xmlns'
SVG_VALUE_XMLNS = 'http://www.w3.org/2000/svg'
SVG_ATTR_XMLNS_LINK = 'xmlns:xlink'
SVG_VALUE_XLINK = 'http://www.w3.org/1999/xlink'
SVG_ATTR_XMLNS_EV = 'xmlns:ev'
SVG_VALUE_XMLNS_EV = 'http://www.w3.org/2001/xml-events'
XLINK_HREF = '{http://www.w3.org/1999/xlink}href'
SVG_HREF = "href"
SVG_ATTR_WIDTH = 'width'
SVG_ATTR_HEIGHT = 'height'
SVG_ATTR_VIEWBOX = 'viewBox'
SVG_VIEWBOX_TRANSFORM = 'viewbox_transform'
SVG_TAG_PATH = 'path'
SVG_TAG_GROUP = 'g'
SVG_TAG_RECT = 'rect'
SVG_TAG_CIRCLE = 'circle'
SVG_TAG_ELLIPSE = 'ellipse'
SVG_TAG_LINE = 'line'
SVG_TAG_POLYLINE = 'polyline'
SVG_TAG_POLYGON = 'polygon'
SVG_TAG_TEXT = 'text'
SVG_TAG_TSPAN = 'tspan'
SVG_TAG_IMAGE = 'image'
SVG_TAG_DESC = 'desc'
SVG_TAG_STYLE = 'style'
SVG_TAG_DEFS = 'defs'
SVG_TAG_USE = 'use'
SVG_STRUCT_ATTRIB = 'attributes'
SVG_ATTR_ID = 'id'
SVG_ATTR_DATA = 'd'
SVG_ATTR_COLOR = 'color'
SVG_ATTR_FILL = 'fill'
SVG_ATTR_STROKE = 'stroke'
SVG_ATTR_STROKE_WIDTH = 'stroke-width'
SVG_ATTR_TRANSFORM = 'transform'
SVG_ATTR_STYLE = 'style'
SVG_ATTR_CLASS = 'class'
SVG_ATTR_CENTER_X = 'cx'
SVG_ATTR_CENTER_Y = 'cy'
SVG_ATTR_RADIUS_X = 'rx'
SVG_ATTR_RADIUS_Y = 'ry'
SVG_ATTR_RADIUS = 'r'
SVG_ATTR_POINTS = 'points'
SVG_ATTR_PRESERVEASPECTRATIO = 'preserveAspectRatio'
SVG_ATTR_X = 'x'
SVG_ATTR_Y = 'y'
SVG_ATTR_X0 = 'x0'
SVG_ATTR_Y0 = 'y0'
SVG_ATTR_X1 = 'x1'
SVG_ATTR_Y1 = 'y1'
SVG_ATTR_X2 = 'x2'
SVG_ATTR_Y2 = 'y2'
SVG_ATTR_DX = 'dx'
SVG_ATTR_DY = 'dy'
SVG_ATTR_TAG = 'tag'
SVG_ATTR_FONT = 'font'
SVG_ATTR_FONT_FAMILY = 'font-family' # Serif, sans-serif, cursive, fantasy, monospace
SVG_ATTR_FONT_FACE = 'font-face'
SVG_ATTR_FONT_SIZE = 'font-size'
SVG_ATTR_FONT_WEIGHT = 'font-weight' # normal, bold, bolder, lighter, 100-900
SVG_ATTR_TEXT_ANCHOR = 'text-anchor'
SVG_TRANSFORM_MATRIX = 'matrix'
SVG_TRANSFORM_TRANSLATE = 'translate'
SVG_TRANSFORM_SCALE = 'scale'
SVG_TRANSFORM_ROTATE = 'rotate'
SVG_TRANSFORM_SKEW_X = 'skewx'
SVG_TRANSFORM_SKEW_Y = 'skewy'
SVG_TRANSFORM_SKEW = 'skew'
SVG_TRANSFORM_TRANSLATE_X = 'translatex'
SVG_TRANSFORM_TRANSLATE_Y = 'translatey'
SVG_TRANSFORM_SCALE_X = 'scalex'
SVG_TRANSFORM_SCALE_Y = 'scaley'
SVG_VALUE_NONE = 'none'
SVG_VALUE_CURRENT_COLOR = 'currentColor'
PATTERN_WS = r'[\s\t\n]*'
PATTERN_COMMA = r'(?:\s*,\s*|\s+|(?=-))'
PATTERN_FLOAT = '[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?'
PATTERN_LENGTH_UNITS = 'cm|mm|Q|in|pt|pc|px|em|cx|ch|rem|vw|vh|vmin|vmax'
PATTERN_ANGLE_UNITS = 'deg|grad|rad|turn'
PATTERN_TIME_UNITS = 's|ms'
PATTERN_FREQUENCY_UNITS = 'Hz|kHz'
PATTERN_RESOLUTION_UNITS = 'dpi|dpcm|dppx'
PATTERN_PERCENT = '%'
PATTERN_TRANSFORM = SVG_TRANSFORM_MATRIX + '|' \
+ SVG_TRANSFORM_TRANSLATE + '|' \
+ SVG_TRANSFORM_TRANSLATE_X + '|' \
+ SVG_TRANSFORM_TRANSLATE_Y + '|' \
+ SVG_TRANSFORM_SCALE + '|' \
+ SVG_TRANSFORM_SCALE_X + '|' \
+ SVG_TRANSFORM_SCALE_Y + '|' \
+ SVG_TRANSFORM_ROTATE + '|' \
+ SVG_TRANSFORM_SKEW + '|' \
+ SVG_TRANSFORM_SKEW_X + '|' \
+ SVG_TRANSFORM_SKEW_Y
PATTERN_TRANSFORM_UNITS = PATTERN_LENGTH_UNITS + '|' \
+ PATTERN_ANGLE_UNITS + '|' \
+ PATTERN_PERCENT
REGEX_FLOAT = re.compile(PATTERN_FLOAT)
REGEX_COORD_PAIR = re.compile('(%s)%s(%s)' % (PATTERN_FLOAT, PATTERN_COMMA, PATTERN_FLOAT))
REGEX_TRANSFORM_TEMPLATE = re.compile('(?u)(%s)%s\(([^)]+)\)' % (PATTERN_TRANSFORM, PATTERN_WS))
REGEX_TRANSFORM_PARAMETER = re.compile('(%s)%s(%s)?' % (PATTERN_FLOAT, PATTERN_WS, PATTERN_TRANSFORM_UNITS))
REGEX_COLOR_HEX = re.compile(r'^#?([0-9A-Fa-f]{3,8})$')
REGEX_COLOR_RGB = re.compile(
r'rgba?\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*(?:,\s*(%s)\s*)?\)' % (
PATTERN_FLOAT, PATTERN_FLOAT, PATTERN_FLOAT, PATTERN_FLOAT))
REGEX_COLOR_RGB_PERCENT = re.compile(
r'rgba?\(\s*(%s)%%\s*,\s*(%s)%%\s*,\s*(%s)%%\s*(?:,\s*(%s)\s*)?\)' % (
PATTERN_FLOAT, PATTERN_FLOAT, PATTERN_FLOAT, PATTERN_FLOAT))
REGEX_COLOR_HSL = re.compile(
r'hsla?\(\s*(%s)\s*,\s*(%s)%%\s*,\s*(%s)%%\s*(?:,\s*(%s)\s*)?\)' % (
PATTERN_FLOAT, PATTERN_FLOAT, PATTERN_FLOAT, PATTERN_FLOAT))
REGEX_LENGTH = re.compile('(%s)([A-Za-z%%]*)' % PATTERN_FLOAT)
REGEX_CSS_STYLE = re.compile(r'([^{]+)\s*\{\s*([^}]+)\s*\}')
REGEX_CSS_FONT = re.compile(
r'(?:(normal|italic|oblique)\s|(normal|small-caps)\s|(normal|bold|bolder|lighter|\d{3})\s|(normal|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded)\s)*\s*(xx-small|x-small|small|medium|large|x-large|xx-large|larger|smaller|\d+(?:em|pt|pc|px|%))(?:/(xx-small|x-small|small|medium|large|x-large|xx-large|larger|smaller|\d+(?:em|pt|pc|px|%)))?\s*(.*),?\s+(serif|sans-serif|cursive|fantasy|monospace);?')
# PathTokens class.
class PathTokens:
"""Path Tokens is the class for the general outline of how SVG Pathd objects
are stored. Namely, a single non-'e' character and a collection of floating
point numbers. While this is explicitly used for SVG pathd objects the method
for serializing command data in this fashion is also useful as a standalone
class."""
def __init__(self, command_elements):
self.command_elements = command_elements
commands = ''
for k in command_elements:
commands += k
self.COMMAND_RE = re.compile("([%s])" % (commands))
self.elements = None
self.command = None
self.last_command = None
self.parser = None
def _tokenize_path(self, pathdef):
for x in self.COMMAND_RE.split(pathdef):
if x in self.command_elements:
yield x
for token in REGEX_FLOAT.findall(x):
yield token
def get(self):
"""Gets the element from the stack."""
return self.elements.pop()
def pre_execute(self):
"""Called before any command element is executed."""
pass
def post_execute(self):
"""Called after any command element is executed."""
pass
def new_command(self):
"""Called when command element is switched."""
pass
def parse(self, pathdef):
self.elements = list(self._tokenize_path(pathdef))
# Reverse for easy use of .pop()
self.elements.reverse()
while self.elements:
if self.elements[-1] in self.command_elements:
self.last_command = self.command
self.command = self.get()
self.new_command()
else:
if self.command is None:
raise ValueError("Invalid command.") # could be faulty implicit or unaccepted element.
self.pre_execute()
self.command_elements[self.command]()
self.post_execute()
# SVG Path Tokens.
class SVGPathTokens(PathTokens):
"""Utilizes the general PathTokens class to parse SVG pathd strings.
This class has been updated to account for SVG 2.0 version of the zZ command."""
def __init__(self):
PathTokens.__init__(self, {
'M': self.move_to,
'm': self.move_to,
'L': self.line_to,
'l': self.line_to,
"H": self.h_to,
"h": self.h_to,
"V": self.v_to,
"v": self.v_to,
"C": self.cubic_to,
"c": self.cubic_to,
"S": self.smooth_cubic_to,
"s": self.smooth_cubic_to,
"Q": self.quad_to,
"q": self.quad_to,
"T": self.smooth_quad_to,
"t": self.smooth_quad_to,
"A": self.arc_to,
"a": self.arc_to,
"Z": self.close,
"z": self.close
})
self.parser = None
self.absolute = False
def svg_parse(self, parser, pathdef):
self.parser = parser
self.absolute = False
self.parser.start()
self.parse(pathdef)
self.parser.end()
def get_pos(self):
if self.command == 'Z':
return "z" # After Z, all further expected values are also Z.
coord0 = self.get()
if coord0 == 'z' or coord0 == 'Z':
self.command = 'Z'
return "z"
coord1 = self.get()
position = (float(coord0), float(coord1))
if not self.absolute:
current_pos = self.parser.current_point
if current_pos is None:
return position
return [position[0] + current_pos[0], position[1] + current_pos[1]]
return position
def move_to(self):
# Moveto command.
pos = self.get_pos()
self.parser.move(pos)
# Implicit moveto commands are treated as lineto commands.
# So we set command to lineto here, in case there are
# further implicit commands after this moveto.
self.command = 'L'
def line_to(self):
pos = self.get_pos()
self.parser.line(pos)
def h_to(self):
x = float(self.get())
if self.absolute:
self.parser.absolute_h(x)
else:
self.parser.relative_h(x)
def v_to(self):
y = float(self.get())
if self.absolute:
self.parser.absolute_v(y)
else:
self.parser.relative_v(y)
def cubic_to(self):
control1 = self.get_pos()
control2 = self.get_pos()
end = self.get_pos()
self.parser.cubic(control1, control2, end)
def smooth_cubic_to(self):
control2 = self.get_pos()
end = self.get_pos()
self.parser.smooth_cubic(control2, end)
def quad_to(self):
control = self.get_pos()
end = self.get_pos()
self.parser.quad(control, end)
def smooth_quad_to(self):
end = self.get_pos()
self.parser.smooth_quad(end)
def arc_to(self):
rx = float(self.get())
ry = float(self.get())
rotation = float(self.get())
arc = float(self.get())
sweep = float(self.get())
end = self.get_pos()
self.parser.arc(rx, ry, rotation, arc, sweep, end)
def close(self):
# Close path
self.parser.closed()
self.command = None
def new_command(self):
self.absolute = self.command.isupper()
def post_execute(self):
pass
class Length(object):
"""
SVGLength as used in SVG
Length are lazy solving values. Several conversion values are typically unknown by default and length simply
stores that ambiguity. So we can have a length of 50% and without calling .value(relative_length=3000) it will
simply store as 50%. Likewise you can have discrete values like 30cm or 20in which have knowable discrete values
but are not knowable in pixels unless a PPI value is supplied. We can say .value(relative_length=30cm, PPI=96) and
solve this for a value like 12%. We can also convert values between knowable lengths. So 30cm in 300mm regardless
whether we know how to convert this to pixels. 0% is 0 in any units or relative values. We can convert pixels to
pc and pt without issue. We can convert vh, vw, vmax, vmin values if we know viewbox values. We can convert em
values if we know the font_size. We can add values together if they are convertible units. Length("20in") + "3cm".
If .value() cannot solve for the value with the given information then it will return a Length value. If it can
be solved it will return a float.
"""
def __init__(self, *args, **kwargs):
if len(args) == 1:
value = args[0]
if value is None:
self.amount = None
self.units = None
return
s = str(value)
for m in REGEX_LENGTH.findall(s):
self.amount = float(m[0])
self.units = m[1]
return
elif len(args) == 2:
self.amount = args[0]
self.units = args[1]
return
self.amount = 0.0
self.units = ''
def __float__(self):
if self.amount is None:
return None
if self.units == 'pt':
return self.amount * 1.3333
elif self.units == 'pc':
return self.amount * 16.0
return self.amount
def __imul__(self, other):
if isinstance(other, (int, float)):
self.amount *= other
return self
if self.amount == 0.0:
return 0.0
if isinstance(other, str):
other = Length(other)
if isinstance(other, Length):
if other.amount == 0.0:
self.amount = 0.0
return self
if self.units == other.units:
self.amount *= other.amount
return self
if self.units == '%':
self.units = other.units
self.amount = self.amount * other.amount / 100.0
return self
elif other.units == '%':
self.amount = self.amount * other.amount / 100.0
return self
raise ValueError
def __iadd__(self, other):
if not isinstance(other, Length):
other = Length(other)
if self.units == other.units:
self.amount += other.amount
return self
if self.amount == 0:
self.amount = other.amount
self.units = other.units
return self
if other.amount == 0:
return self
if self.units == 'px' or self.units == '':
if other.units == 'px' or other.units == '':
self.amount += other.amount
elif other.units == 'pt':
self.amount += other.amount * 1.3333
elif other.units == 'pc':
self.amount += other.amount * 16.0
else:
raise ValueError
return self
if self.units == 'pt':
if other.units == 'px' or other.units == '':
self.amount += other.amount / 1.3333
elif other.units == 'pc':
self.amount += other.amount * 12.0
else:
raise ValueError
return self
elif self.units == 'pc':
if other.units == 'px' or other.units == '':
self.amount += other.amount / 16.0
elif other.units == 'pt':
self.amount += other.amount / 12.0
else:
raise ValueError
return self
elif self.units == 'cm':
if other.units == 'mm':
self.amount += other.amount / 10.0
elif other.units == 'in':
self.amount += other.amount / 0.393701
else:
raise ValueError
return self
elif self.units == 'mm':
if other.units == 'cm':
self.amount += other.amount * 10.0
elif other.units == 'in':
self.amount += other.amount / 0.0393701
else:
raise ValueError
return self
elif self.units == 'in':
if other.units == 'cm':
self.amount += other.amount * 0.393701
elif other.units == 'mm':
self.amount += other.amount * 0.0393701
else:
raise ValueError
return self
raise ValueError('%s units were not determined.' % self.units)
def __abs__(self):
c = self.__copy__()
c.amount = abs(c.amount)
return c
def __truediv__(self, other):
if isinstance(other, (int, float)):
c = self.__copy__()
c.amount /= other
return c
if self.amount == 0.0:
return 0.0
if isinstance(other, str):
other = Length(other)
if isinstance(other, Length):
if self.units == other.units:
q = self.amount / other.amount
return q # no units
if self.units == 'px' or self.units == '':
if other.units == 'px' or other.units == '':
return self.amount / other.amount
elif other.units == 'pt':
return self.amount / (other.amount * 1.3333)
elif other.units == 'pc':
return self.amount / (other.amount * 16.0)
else:
raise ValueError
if self.units == 'pt':
if other.units == 'px' or other.units == '':
return self.amount / (other.amount / 1.3333)
elif other.units == 'pc':
return self.amount / (other.amount * 12.0)
else:
raise ValueError
if self.units == 'pc':
if other.units == 'px' or other.units == '':
return self.amount / (other.amount / 16.0)
elif other.units == 'pt':
return self.amount / (other.amount / 12.0)
else:
raise ValueError
if self.units == 'cm':
if other.units == 'mm':
return self.amount / (other.amount / 10.0)
elif other.units == 'in':
return self.amount / (other.amount / 0.393701)
else:
raise ValueError
if self.units == 'mm':
if other.units == 'cm':
return self.amount / (other.amount * 10.0)
elif other.units == 'in':
return self.amount / (other.amount / 0.0393701)
else:
raise ValueError
if self.units == 'in':
if other.units == 'cm':
return self.amount / (other.amount * 0.393701)
elif other.units == 'mm':
return self.amount / (other.amount * 0.0393701)
else:
raise ValueError
raise ValueError
__floordiv__ = __truediv__
__div__ = __truediv__
def __lt__(self, other):
return (self - other).amount < 0.0
def __le__(self, other):
return (self - other).amount <= 0.0
def __gt__(self, other):
return (self - other).amount > 0.0
def __ge__(self, other):
return (self - other).amount >= 0.0
def __ne__(self, other):
return not self.__eq__(other)
def __add__(self, other):
if isinstance(other, (str, float, int)):
other = Length(other)
c = self.__copy__()
c += other
return c
__radd__ = __add__
def __mul__(self, other):
c = copy(self)
c *= other
return c
def __rdiv__(self, other):
c = copy(self)
c *= 1.0 / other.amount
return c
def __neg__(self):
s = self.__copy__()
s.amount = -s.amount
return s
def __isub__(self, other):
if isinstance(other, (str, float, int)):
other = Length(other)
self += -other
return self
def __sub__(self, other):
s = self.__copy__()
s -= other
return s
def __rsub__(self, other):
if isinstance(other, (str, float, int)):
other = Length(other)
return (-self) + other
def __copy__(self):
return Length(self.amount, self.units)
__rmul__ = __mul__
def __repr__(self):
return 'Length(\'%s\')' % (str(self))
def __str__(self):
if self.amount is None:
return SVG_VALUE_NONE
return '%s%s' % (Length.str(self.amount), self.units)
def __eq__(self, other):
if other is None:
return False
s = self.in_pixels()
if isinstance(other, (float, int)):
if s is not None:
return abs(s - other) <= ERROR
else:
return other == 0 and self.amount == 0
if isinstance(other, str):
other = Length(other)
if self.amount == other.amount and self.units == other.units:
return True
if s is not None:
o = self.in_pixels()
if abs(s - o) <= ERROR:
return True
s = self.in_inches()
if s is not None:
o = self.in_inches()
if abs(s - o) <= ERROR:
return True
return False
@property
def value_in_units(self):
return self.amount
def in_pixels(self):
if self.units == 'px' or self.units == '':
return self.amount
if self.units == 'pt':
return self.amount / 1.3333
if self.units == 'pc':
return self.amount / 16.0
return None
def in_inches(self):
if self.units == 'mm':
return self.amount * 0.0393701
if self.units == 'cm':
return self.amount * 0.393701
if self.units == 'in':
return self.amount
return None
def to_mm(self, ppi=DEFAULT_PPI, relative_length=None, font_size=None, font_height=None, viewbox=None):
value = self.value(ppi=ppi, relative_length=relative_length, font_size=font_size,
font_height=font_height, viewbox=viewbox)
v = value / (ppi * 0.0393701)
return Length("%smm" % (Length.str(v)))
def to_cm(self, ppi=DEFAULT_PPI, relative_length=None, font_size=None, font_height=None, viewbox=None):
value = self.value(ppi=ppi, relative_length=relative_length,
font_size=font_size, font_height=font_height, viewbox=viewbox)
v = value / (ppi * 0.393701)
return Length("%scm" % (Length.str(v)))
def to_inch(self, ppi=DEFAULT_PPI, relative_length=None, font_size=None, font_height=None, viewbox=None):
value = self.value(ppi=ppi, relative_length=relative_length,
font_size=font_size, font_height=font_height, viewbox=viewbox)
v = value / ppi
return Length("%sin" % (Length.str(v)))
def value(self, ppi=None, relative_length=None, font_size=None, font_height=None, viewbox=None):
if self.amount is None:
return None
if self.units == '%':
if relative_length is None:
return self
fraction = self.amount / 100.0
if isinstance(relative_length, (float, int)):
return fraction * relative_length
elif isinstance(relative_length, (str, Length)):
length = relative_length * self
if isinstance(length, Length):
return length.value(ppi=ppi, font_size=font_size, font_height=font_height, viewbox=viewbox)
return length
return self
if self.units == 'mm':
if ppi is None:
return self
return self.amount * ppi * 0.0393701
if self.units == 'cm':
if ppi is None:
return self
return self.amount * ppi * 0.393701
if self.units == 'in':
if ppi is None:
return self
return self.amount * ppi
if self.units == 'px' or self.units == '':
return self.amount
if self.units == 'pt':
return self.amount * 1.3333
if self.units == 'pc':
return self.amount * 16.0
if self.units == 'em':
if font_size is None:
return self
return self.amount * float(font_size)
if self.units == 'ex':
if font_height is None:
return self
return self.amount * float(font_height)
if self.units == 'vw':
if viewbox is None:
return self
v = Viewbox(viewbox)
return self.amount * v.viewbox_width / 100.0
if self.units == 'vh':
if viewbox is None:
return self
v = Viewbox(viewbox)
return self.amount * v.viewbox_height / 100.0
if self.units == 'vmin':
if viewbox is None:
return self
v = Viewbox(viewbox)
m = min(v.viewbox_height, v.viewbox_height)
return self.amount * m / 100.0
if self.units == 'vmax':
if viewbox is None:
return self
v = Viewbox(viewbox)
m = max(v.viewbox_height, v.viewbox_height)
return self.amount * m / 100.0
try:
return float(self)
except ValueError:
return self
@staticmethod
def str(s):
if isinstance(s, Length):
if s.units == '':
s = s.amount
else:
a = '%.12f' % (s.amount)
if '.' in a:
a = a.rstrip('0').rstrip('.')
return '\'%s%s\'' % (a, s.units)
s = '%.12f' % (s)
if '.' in s:
s = s.rstrip('0').rstrip('.')
return s
class Color(object):
"""
SVG Color Parsing
Parses different forms of defining colors.
Including keyword: https://www.w3.org/TR/SVG11/types.html#ColorKeywords
"""
def __init__(self, *args, **kwargs):
self.value = 0
if len(args) == 0:
r = 0
g = 0
b = 0
if 'red' in kwargs:
r = kwargs['red']
if 'green' in kwargs:
g = kwargs['green']
if 'blue' in kwargs:
b = kwargs['blue']
if 'r' in kwargs:
r = kwargs['r']
if 'g' in kwargs:
g = kwargs['g']
if 'b' in kwargs:
b = kwargs['b']
self.value = Color.rgb_to_int(r, g, b)
if 1 <= len(args) <= 2:
v = args[0]
if isinstance(v, Color):
self.value = v.value
elif isinstance(v, int):
self.value = v
else:
self.value = Color.parse(v)
if len(args) == 2:
self.opacity = float(args[1])
elif len(args) == 3:
r = args[0]
g = args[1]
b = args[2]
self.value = Color.rgb_to_int(r, g, b)
elif len(args) == 4:
r = args[0]
g = args[1]
b = args[2]
opacity = args[3] / 255.0
self.value = Color.rgb_to_int(r, g, b, opacity)
def __int__(self):
return self.value
def __str__(self):
if self.value is None:
return str(self.value)
return self.hex
def __repr__(self):
if self.value is None:
return 'Color(\'%s\')' % (self.value)
return 'Color(\'%s\')' % (self.hex)
def __eq__(self, other):
if self is other:
return True
first = self.value
second = other
if isinstance(second, str):
second = Color(second)
if isinstance(second, Color):
second = second.value
return first == second
def __ne__(self, other):
return not self == other
@staticmethod
def rgb_to_int(r, g, b, opacity=1.0):
if opacity > 1:
opacity = 1.0
if opacity < 0:
opacity = 0
r = Color.crimp(r)
g = Color.crimp(g)
b = Color.crimp(b)
a = Color.crimp(opacity * 255.0)
if a & 0x80 != 0:
a ^= 0x80
a <<= 24
a = ~a
a ^= 0x7FFFFFFF
else:
a <<= 24
r <<= 16
g <<= 8
c = r | g | b | a
return c
@staticmethod
def hsl_to_int(h, s, l, opacity=1.0):
c = Color()
c.opacity = opacity
c.hsl = h, s, l
return c.value
@staticmethod
def parse(color_string):
"""Parse SVG color, will return a set value."""
if color_string is None or color_string == SVG_VALUE_NONE:
return None
match = REGEX_COLOR_HEX.match(color_string)
if match:
return Color.parse_color_hex(color_string)
match = REGEX_COLOR_RGB.match(color_string)
if match:
return Color.parse_color_rgb(match.groups())
match = REGEX_COLOR_RGB_PERCENT.match(color_string)
if match:
return Color.parse_color_rgbp(match.groups())
match = REGEX_COLOR_HSL.match(color_string)
if match:
return Color.parse_color_hsl(match.groups())
return Color.parse_color_lookup(color_string)
@staticmethod
def parse_color_lookup(v):
"""Parse SVG Color by Keyword on dictionary lookup"""
if not isinstance(v, str):
return Color.rgb_to_int(0, 0, 0)
else:
v = v.replace(' ', '').lower()
if v == "transparent":
return Color.rgb_to_int(0, 0, 0, 0.0)
if v == "aliceblue":
return Color.rgb_to_int(250, 248, 255)
if v == "aliceblue":
return Color.rgb_to_int(240, 248, 255)
if v == "antiquewhite":
return Color.rgb_to_int(250, 235, 215)
if v == "aqua":
return Color.rgb_to_int(0, 255, 255)
if v == "aquamarine":
return Color.rgb_to_int(127, 255, 212)
if v == "azure":
return Color.rgb_to_int(240, 255, 255)
if v == "beige":
return Color.rgb_to_int(245, 245, 220)
if v == "bisque":
return Color.rgb_to_int(255, 228, 196)
if v == "black":
return Color.rgb_to_int(0, 0, 0)
if v == "blanchedalmond":
return Color.rgb_to_int(255, 235, 205)
if v == "blue":
return Color.rgb_to_int(0, 0, 255)
if v == "blueviolet":
return Color.rgb_to_int(138, 43, 226)
if v == "brown":
return Color.rgb_to_int(165, 42, 42)
if v == "burlywood":
return Color.rgb_to_int(222, 184, 135)
if v == "cadetblue":
return Color.rgb_to_int(95, 158, 160)
if v == "chartreuse":
return Color.rgb_to_int(127, 255, 0)
if v == "chocolate":
return Color.rgb_to_int(210, 105, 30)
if v == "coral":
return Color.rgb_to_int(255, 127, 80)
if v == "cornflowerblue":
return Color.rgb_to_int(100, 149, 237)
if v == "cornsilk":
return Color.rgb_to_int(255, 248, 220)
if v == "crimson":
return Color.rgb_to_int(220, 20, 60)
if v == "cyan":
return Color.rgb_to_int(0, 255, 255)
if v == "darkblue":
return Color.rgb_to_int(0, 0, 139)
if v == "darkcyan":
return Color.rgb_to_int(0, 139, 139)
if v == "darkgoldenrod":
return Color.rgb_to_int(184, 134, 11)
if v == "darkgray":
return Color.rgb_to_int(169, 169, 169)
if v == "darkgreen":
return Color.rgb_to_int(0, 100, 0)
if v == "darkgrey":
return Color.rgb_to_int(169, 169, 169)
if v == "darkkhaki":
return Color.rgb_to_int(189, 183, 107)
if v == "darkmagenta":
return Color.rgb_to_int(139, 0, 139)
if v == "darkolivegreen":
return Color.rgb_to_int(85, 107, 47)
if v == "darkorange":
return Color.rgb_to_int(255, 140, 0)
if v == "darkorchid":
return Color.rgb_to_int(153, 50, 204)
if v == "darkred":
return Color.rgb_to_int(139, 0, 0)
if v == "darksalmon":
return Color.rgb_to_int(233, 150, 122)
if v == "darkseagreen":
return Color.rgb_to_int(143, 188, 143)
if v == "darkslateblue":
return Color.rgb_to_int(72, 61, 139)
if v == "darkslategray":
return Color.rgb_to_int(47, 79, 79)
if v == "darkslategrey":
return Color.rgb_to_int(47, 79, 79)
if v == "darkturquoise":
return Color.rgb_to_int(0, 206, 209)
if v == "darkviolet":
return Color.rgb_to_int(148, 0, 211)
if v == "deeppink":
return Color.rgb_to_int(255, 20, 147)
if v == "deepskyblue":
return Color.rgb_to_int(0, 191, 255)
if v == "dimgray":
return Color.rgb_to_int(105, 105, 105)
if v == "dimgrey":
return Color.rgb_to_int(105, 105, 105)
if v == "dodgerblue":
return Color.rgb_to_int(30, 144, 255)
if v == "firebrick":
return Color.rgb_to_int(178, 34, 34)
if v == "floralwhite":
return Color.rgb_to_int(255, 250, 240)
if v == "forestgreen":
return Color.rgb_to_int(34, 139, 34)
if v == "fuchsia":
return Color.rgb_to_int(255, 0, 255)
if v == "gainsboro":
return Color.rgb_to_int(220, 220, 220)
if v == "ghostwhite":
return Color.rgb_to_int(248, 248, 255)
if v == "gold":
return Color.rgb_to_int(255, 215, 0)
if v == "goldenrod":
return Color.rgb_to_int(218, 165, 32)
if v == "gray":
return Color.rgb_to_int(128, 128, 128)
if v == "grey":
return Color.rgb_to_int(128, 128, 128)
if v == "green":
return Color.rgb_to_int(0, 128, 0)
if v == "greenyellow":
return Color.rgb_to_int(173, 255, 47)
if v == "honeydew":
return Color.rgb_to_int(240, 255, 240)
if v == "hotpink":
return Color.rgb_to_int(255, 105, 180)
if v == "indianred":
return Color.rgb_to_int(205, 92, 92)
if v == "indigo":
return Color.rgb_to_int(75, 0, 130)
if v == "ivory":
return Color.rgb_to_int(255, 255, 240)
if v == "khaki":
return Color.rgb_to_int(240, 230, 140)
if v == "lavender":
return Color.rgb_to_int(230, 230, 250)
if v == "lavenderblush":
return Color.rgb_to_int(255, 240, 245)
if v == "lawngreen":
return Color.rgb_to_int(124, 252, 0)
if v == "lemonchiffon":
return Color.rgb_to_int(255, 250, 205)
if v == "lightblue":
return Color.rgb_to_int(173, 216, 230)
if v == "lightcoral":
return Color.rgb_to_int(240, 128, 128)
if v == "lightcyan":
return Color.rgb_to_int(224, 255, 255)
if v == "lightgoldenrodyellow":
return Color.rgb_to_int(250, 250, 210)
if v == "lightgray":
return Color.rgb_to_int(211, 211, 211)
if v == "lightgreen":
return Color.rgb_to_int(144, 238, 144)
if v == "lightgrey":
return Color.rgb_to_int(211, 211, 211)
if v == "lightpink":
return Color.rgb_to_int(255, 182, 193)
if v == "lightsalmon":
return Color.rgb_to_int(255, 160, 122)
if v == "lightseagreen":
return Color.rgb_to_int(32, 178, 170)
if v == "lightskyblue":
return Color.rgb_to_int(135, 206, 250)
if v == "lightslategray":
return Color.rgb_to_int(119, 136, 153)
if v == "lightslategrey":
return Color.rgb_to_int(119, 136, 153)
if v == "lightsteelblue":
return Color.rgb_to_int(176, 196, 222)
if v == "lightyellow":
return Color.rgb_to_int(255, 255, 224)
if v == "lime":
return Color.rgb_to_int(0, 255, 0)
if v == "limegreen":
return Color.rgb_to_int(50, 205, 50)
if v == "linen":
return Color.rgb_to_int(250, 240, 230)
if v == "magenta":
return Color.rgb_to_int(255, 0, 255)
if v == "maroon":
return Color.rgb_to_int(128, 0, 0)
if v == "mediumaquamarine":
return Color.rgb_to_int(102, 205, 170)
if v == "mediumblue":
return Color.rgb_to_int(0, 0, 205)
if v == "mediumorchid":
return Color.rgb_to_int(186, 85, 211)
if v == "mediumpurple":
return Color.rgb_to_int(147, 112, 219)
if v == "mediumseagreen":
return Color.rgb_to_int(60, 179, 113)
if v == "mediumslateblue":
return Color.rgb_to_int(123, 104, 238)
if v == "mediumspringgreen":
return Color.rgb_to_int(0, 250, 154)
if v == "mediumturquoise":
return Color.rgb_to_int(72, 209, 204)
if v == "mediumvioletred":
return Color.rgb_to_int(199, 21, 133)
if v == "midnightblue":
return Color.rgb_to_int(25, 25, 112)
if v == "mintcream":
return Color.rgb_to_int(245, 255, 250)
if v == "mistyrose":
return Color.rgb_to_int(255, 228, 225)
if v == "moccasin":
return Color.rgb_to_int(255, 228, 181)
if v == "navajowhite":
return Color.rgb_to_int(255, 222, 173)
if v == "navy":
return Color.rgb_to_int(0, 0, 128)
if v == "oldlace":
return Color.rgb_to_int(253, 245, 230)
if v == "olive":
return Color.rgb_to_int(128, 128, 0)
if v == "olivedrab":
return Color.rgb_to_int(107, 142, 35)
if v == "orange":
return Color.rgb_to_int(255, 165, 0)
if v == "orangered":
return Color.rgb_to_int(255, 69, 0)
if v == "orchid":
return Color.rgb_to_int(218, 112, 214)
if v == "palegoldenrod":
return Color.rgb_to_int(238, 232, 170)
if v == "palegreen":
return Color.rgb_to_int(152, 251, 152)
if v == "paleturquoise":
return Color.rgb_to_int(175, 238, 238)
if v == "palevioletred":
return Color.rgb_to_int(219, 112, 147)
if v == "papayawhip":
return Color.rgb_to_int(255, 239, 213)
if v == "peachpuff":
return Color.rgb_to_int(255, 218, 185)
if v == "peru":
return Color.rgb_to_int(205, 133, 63)
if v == "pink":
return Color.rgb_to_int(255, 192, 203)
if v == "plum":
return Color.rgb_to_int(221, 160, 221)
if v == "powderblue":
return Color.rgb_to_int(176, 224, 230)
if v == "purple":
return Color.rgb_to_int(128, 0, 128)
if v == "red":
return Color.rgb_to_int(255, 0, 0)
if v == "rosybrown":
return Color.rgb_to_int(188, 143, 143)
if v == "royalblue":
return Color.rgb_to_int(65, 105, 225)
if v == "saddlebrown":
return Color.rgb_to_int(139, 69, 19)
if v == "salmon":
return Color.rgb_to_int(250, 128, 114)
if v == "sandybrown":
return Color.rgb_to_int(244, 164, 96)
if v == "seagreen":
return Color.rgb_to_int(46, 139, 87)
if v == "seashell":
return Color.rgb_to_int(255, 245, 238)
if v == "sienna":
return Color.rgb_to_int(160, 82, 45)
if v == "silver":
return Color.rgb_to_int(192, 192, 192)
if v == "skyblue":
return Color.rgb_to_int(135, 206, 235)
if v == "slateblue":
return Color.rgb_to_int(106, 90, 205)
if v == "slategray":
return Color.rgb_to_int(112, 128, 144)
if v == "slategrey":
return Color.rgb_to_int(112, 128, 144)
if v == "snow":
return Color.rgb_to_int(255, 250, 250)
if v == "springgreen":
return Color.rgb_to_int(0, 255, 127)
if v == "steelblue":
return Color.rgb_to_int(70, 130, 180)
if v == "tan":
return Color.rgb_to_int(210, 180, 140)
if v == "teal":
return Color.rgb_to_int(0, 128, 128)
if v == "thistle":
return Color.rgb_to_int(216, 191, 216)
if v == "tomato":
return Color.rgb_to_int(255, 99, 71)
if v == "turquoise":
return Color.rgb_to_int(64, 224, 208)
if v == "violet":
return Color.rgb_to_int(238, 130, 238)
if v == "wheat":
return Color.rgb_to_int(245, 222, 179)
if v == "white":
return Color.rgb_to_int(255, 255, 255)
if v == "whitesmoke":
return Color.rgb_to_int(245, 245, 245)
if v == "yellow":
return Color.rgb_to_int(255, 255, 0)
if v == "yellowgreen":
return Color.rgb_to_int(154, 205, 50)
return Color.rgb_to_int(0, 0, 0)
@staticmethod
def parse_color_hex(hex_string):
"""Parse SVG Color by Hex String"""
h = hex_string.lstrip('#')
size = len(h)
if size == 8:
return int(h[:8], 16)
elif size == 6:
s = '{0}'.format(h[:6])
q = (~int(s, 16) & 0xFFFFFF)
v = -1 ^ q
return v
elif size == 4:
s = h[0] + h[0] + h[1] + h[1] + h[2] + h[2] + h[3] + h[3]
return int(s, 16)
elif size == 3:
s = '{0}{0}{1}{1}{2}{2}'.format(h[0], h[1], h[2])
q = (~int(s, 16) & 0xFFFFFF)
v = -1 ^ q
return v
return Color.rgb_to_int(0, 0, 0)
@staticmethod
def parse_color_rgb(values):
"""Parse SVG Color, RGB value declarations """
r = int(values[0])
g = int(values[1])
b = int(values[2])
if values[3] is not None:
opacity = float(values[3])
else:
opacity = 1
return Color.rgb_to_int(r, g, b, opacity)
@staticmethod
def parse_color_rgbp(values):
"""Parse SVG color, RGB percent value declarations"""
ratio = 255.0 / 100.0
r = round(float(values[0]) * ratio)
g = round(float(values[1]) * ratio)
b = round(float(values[2]) * ratio)
if values[3] is not None:
opacity = float(values[3])
else:
opacity = 1
return Color.rgb_to_int(r, g, b, opacity)
@staticmethod
def parse_color_hsl(values):
"""Parse SVG color, HSL value declarations"""
h = Angle.parse(values[0])
h = h.as_turns
s = float(values[1]) / 100.0
if s > 1:
s = 1.0
if s < 0:
s = 0.0
l = float(values[2]) / 100.0
if l > 1:
l = 1.0
if l < 0:
l = 0.0
if values[3] is not None:
opacity = float(values[3])
else:
opacity = 1
return Color.hsl_to_int(h, s, l, opacity)
@property
def opacity(self):
return self.alpha / 255.0
@opacity.setter
def opacity(self, opacity):
a = int(round(opacity * 255.0))
a = Color.crimp(a)
self.alpha = a
@property
def alpha(self):
return (self.value >> 24) & 0xFF
@alpha.setter
def alpha(self, a):
a = Color.crimp(a)
self.value &= 0xFFFFFF
self.value = int(self.value)
if a & 0x80 != 0:
a ^= 0x80
a <<= 24
a = ~a
a ^= 0x7FFFFFFF
else:
a <<= 24
self.value |= a
@property
def red(self):
return (self.value >> 16) & 0xFF
@red.setter
def red(self, r):
r = int(r & 0xFF)
self.value &= ~0xFF0000
r <<= 16
self.value |= r
@property
def green(self):
return (self.value >> 8) & 0xFF
@green.setter
def green(self, g):
g = int(g & 0xFF)
self.value &= ~0xFF00
g <<= 8
self.value |= g
@property
def blue(self):
return self.value & 0xFF
@blue.setter
def blue(self, b):
b = int(b & 0xFF)
self.value &= ~0xFF
self.value |= b
@property
def hexa(self):
return '#%02x%02x%02x%02x' % (self.alpha, self.red, self.green, self.blue)
@property
def hex(self):
if self.alpha == 0xFF:
return '#%02x%02x%02x' % (self.red, self.green, self.blue)
else:
return '#%02x%02x%02x%02x' % (self.alpha, self.red, self.green, self.blue)
@property
def hue(self):
r = self.red / 255.0
g = self.green / 255.0
b = self.blue / 255.0
var_min = min(r, g, b)
var_max = max(r, g, b)
delta_max = var_max - var_min
if delta_max == 0:
return 0
dr = (((var_max - r) / 6.0) + delta_max / 2.0) / delta_max
dg = (((var_max - g) / 6.0) + delta_max / 2.0) / delta_max
db = (((var_max - b) / 6.0) + delta_max / 2.0) / delta_max
if r == var_max:
h = db - dg
elif g == var_max:
h = (1.0 / 3.0) + dr - db
else: # db == max_v
h = (2.0 / 3.0) + dg - dr
if h < 0:
h += 1
if h > 1:
h -= 1
return h
@hue.setter
def hue(self, v):
h, s, l = self.hsl
self.hsl = v, s, l
@property
def saturation(self):
r = self.red / 255.0
g = self.green / 255.0
b = self.blue / 255.0
min_v = min(r, g, b)
max_v = max(r, g, b)
delta = max_v - min_v
if max_v == min_v:
return 0.0
if (max_v + min_v) < 1:
return delta / (max_v + min_v)
else:
return delta / (2.0 - max_v - min_v)
@saturation.setter
def saturation(self, v):
h, s, l = self.hsl
self.hsl = h, v, l
@property
def lightness(self):
r = self.red / 255.0
g = self.green / 255.0
b = self.blue / 255.0
min_v = min(r, g, b)
max_v = max(r, g, b)
return (max_v + min_v) / 2.0
@lightness.setter
def lightness(self, v):
h, s, l = self.hsl
self.hsl = h, s, v
@property
def intensity(self):
r = self.red
g = self.green
b = self.blue
return (r + b + g) / 768.0
@property
def brightness(self):
r = self.red
g = self.green
b = self.blue
cmax = max(r, g, b)
return cmax / 255.0
@property
def blackness(self):
return 1.0 - self.brightness
@property
def luminance(self):
r = self.red / 255.0
g = self.green / 255.0
b = self.blue / 255.0
return r * 0.3 + g * 0.59 + b * 0.11
@property
def luma(self):
r = self.red / 255.0
g = self.green / 255.0
b = self.blue / 255.0
return r * 0.2126 + g * 0.7152 + b * 0.0722
@staticmethod
def over(c1, c2):
"""
Porter Duff Alpha compositing operation over.
Returns c1 over c2. This is the standard painter algorithm.
"""
if isinstance(c1, str):
c1 = Color.parse(c1)
elif isinstance(c1, int):
c1 = Color(c1)
if isinstance(c2, str):
c2 = Color.parse(c2)
elif isinstance(c2, int):
c2 = Color(c2)
r1 = c1.red
g1 = c1.green
b1 = c1.blue
a1 = c1.alpha
if a1 == 255:
return c1.value
if a1 == 0:
return c2.value
r2 = c2.red
g2 = c2.green
b2 = c2.blue
a2 = c2.alpha
q = 255.0 - a1
sr = r1 * a1 * 255.0 + r2 * a2 * q
sg = g1 * a1 * 255.0 + g2 * a2 * q
sb = b1 * a1 * 255.0 + b2 * a2 * q
sa = a1 * 255.0 + a2 * q
sr /= sa
sg /= sa
sb /= sa
sa /= (255.0 * 255.0)
return Color.rgb_to_int(sr, sg, sb, sa)
@staticmethod
def distance(c1, c2):
return sqrt(Color.distance_sq(c1, c2))
@staticmethod
def distance_sq(c1, c2):
"""
Function returns the square of colordistance. The square of the color distance will always be closer than the
square of another color distance.
Rather than naive Euclidean distance we use Compuphase's Redmean color distance.
https://www.compuphase.com/cmetric.htm
It's computationally simple, and empirical tests finds it to be on par with LabDE2000.
:param c1: first color
:param c2: second color
:return: square of color distance
"""
if isinstance(c1, str):
c1 = Color(c1)
elif isinstance(c1, int):
c1 = Color(c1)
if isinstance(c2, str):
c2 = Color(c2)
elif isinstance(c2, int):
c2 = Color(c2)
red_mean = int((c1.red + c2.red) / 2.0)
r = c1.red - c2.red
g = c1.green - c2.green
b = c1.blue - c2.blue
return (((512 + red_mean) * r * r) >> 8) + 4 * g * g + ((767 - red_mean) * b * b) >> 8
@staticmethod
def crimp(v):
if v > 255:
return 255
if v < 0:
return 0
return int(v)
@property
def hsl(self):
return self.hue, self.saturation, self.lightness
@hsl.setter
def hsl(self, value):
if not isinstance(value, tuple):
return
h, s, l = value
def hue_2_rgb(v1, v2, vh):
if vh < 0:
vh += 1
if vh > 1:
vh -= 1
if 6.0 * vh < 1.0:
return v1 + (v2 - v1) * 6.0 * vh
if 2.0 * vh < 1:
return v2
if 3 * vh < 2.0:
return v1 + (v2 - v1) * ((2.0 / 3.0) - vh) * 6.0
return v1
if s == 0.0:
r = 255.0 * l
g = 255.0 * l
b = 255.0 * l
else:
if l < 0.5:
v2 = l * (1.0 + s)
else:
v2 = (l + s) - (s * l)
v1 = 2 * l - v2
r = 255.0 * hue_2_rgb(v1, v2, h + (1.0 / 3.0))
g = 255.0 * hue_2_rgb(v1, v2, h)
b = 255.0 * hue_2_rgb(v1, v2, h - (1.0 / 3.0))
self.value = self.rgb_to_int(r, g, b)
def distance_to(self, other):
return Color.distance(self, other)
def blend(self, other, opacity=None):
"""
Blends the given color with the current color.
"""
if opacity is None:
self.value = Color.over(other, self)
else:
color = Color(other)
color.opacity = opacity
self.value = Color.over(color, self)
class Point:
"""Point is a general subscriptable point class with .x and .y as well as [0] and [1]
For compatibility with regebro svg.path we accept complex numbers as points x + yj,
and provide .real and .imag as properties. As well as float and integer values as (v,0) elements.
With regard to SVG 7.15.1 defining SVGPoint this class provides for matrix transformations.
Points are only positions in real Euclidean space. This class is not intended to interact with
the Length class.
"""
def __init__(self, x, y=None):
if x is not None and y is None:
if isinstance(x, str):
string_x, string_y = REGEX_COORD_PAIR.findall(x)[0]
x = float(string_x)
y = float(string_y)
else:
try: # try subscription.
y = x[1]
x = x[0]
except TypeError:
try: # Try .x .y
y = x.y
x = x.x
except AttributeError:
try: # try .imag .real complex values.
y = x.imag
x = x.real
except AttributeError:
# Unknown.
x = 0
y = 0
self.x = x
self.y = y
def __key(self):
return (self.x, self.y)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
a0 = self[0]
a1 = self[1]
if isinstance(other, str):
try:
other = Point(other)
except IndexError: # This string doesn't parse to a point.
return False
if isinstance(other, (Point, list, tuple)):
b0 = other[0]
b1 = other[1]
elif isinstance(other, complex):
b0 = other.real
b1 = other.imag
else:
return NotImplemented
try:
c0 = abs(a0 - b0) <= ERROR
c1 = abs(a1 - b1) <= ERROR
except TypeError:
return False
return c0 and c1
def __ne__(self, other):
return not self == other
def __getitem__(self, item):
if item == 0:
return self.x
elif item == 1:
return self.y
else:
raise IndexError
def __setitem__(self, key, value):
if key == 0:
self.x = value
elif key == 1:
self.y = value
else:
raise IndexError
def __repr__(self):
x_str = Length.str(self.x)
y_str = Length.str(self.y)
return 'Point(%s,%s)' % (x_str, y_str)
def __copy__(self):
return Point(self.x, self.y)
def __str__(self):
x_str = ('%.12G' % (self.x))
if '.' in x_str:
x_str = x_str.rstrip('0').rstrip('.')
y_str = ('%.12G' % (self.y))
if '.' in y_str:
y_str = y_str.rstrip('0').rstrip('.')
return "%s,%s" % (x_str, y_str)
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
v = other.point_in_matrix_space(self)
self[0] = v[0]
self[1] = v[1]
elif isinstance(other, (int, float)): # Emulates complex point multiplication by real.
self.x *= other
self.y *= other
else:
return NotImplemented
return self
def __mul__(self, other):
if isinstance(other, (Matrix, str, int, float)):
n = copy(self)
n *= other
return n
__rmul__ = __mul__
def __iadd__(self, other):
if isinstance(other, (Point, tuple, list)):
self[0] += other[0]
self[1] += other[1]
elif isinstance(other, complex):
self[0] += other.real
self[1] += other.imag
elif isinstance(other, (float, int)):
self[0] += other
else:
return NotImplemented
return self
def __add__(self, other):
if isinstance(other, (Point, tuple, list, complex, int, float)):
n = copy(self)
n += other
return n
__radd__ = __add__
def __isub__(self, other):
if isinstance(other, (Point, tuple, list)):
self[0] -= other[0]
self[1] -= other[1]
elif isinstance(other, complex):
self[0] -= other.real
self[1] -= other.imag
elif isinstance(other, (float, int)):
self[0] -= other
else:
return NotImplemented
return self
def __sub__(self, other):
if isinstance(other, (Point, tuple, list, complex, int, float)):
n = copy(self)
n -= other
return n
def __rsub__(self, other):
if isinstance(other, (Point, tuple, list)):
x = other[0] - self[0]
y = other[1] - self[1]
elif isinstance(other, complex):
x = other.real - self[0]
y = other.imag - self[1]
elif isinstance(other, (float, int)):
x = other - self[0]
y = self[1]
else:
return NotImplemented
return Point(x, y)
def __abs__(self):
return hypot(self.x, self.y)
def __pow__(self, other):
r_raised = abs(self) ** other
argz_multiplied = self.argz() * other
real_part = round(r_raised * cos(argz_multiplied))
imag_part = round(r_raised * sin(argz_multiplied))
return self.__class__(real_part, imag_part)
def conjugate(self):
return self.__class__(self.real, -self.imag)
def argz(self):
return atan(self.imag / self.real)
@property
def real(self):
"""Emulate svg.path use of complex numbers"""
return self.x
@property
def imag(self):
"""Emulate svg.path use of complex numbers"""
return self.y
def matrix_transform(self, matrix):
v = matrix.point_in_matrix_space(self)
self[0] = v[0]
self[1] = v[1]
return self
def move_towards(self, p2, amount=1):
if not isinstance(p2, Point):
p2 = Point(p2)
self.x = amount * (p2[0] - self[0]) + self[0]
self.y = amount * (p2[1] - self[1]) + self[1]
def distance_to(self, p2):
if not isinstance(p2, Point):
p2 = Point(p2)
return Point.distance(self, p2)
def angle_to(self, p2):
if not isinstance(p2, Point):
p2 = Point(p2)
return Point.angle(self, p2)
def polar_to(self, angle, distance):
return Point.polar(self, angle, distance)
def reflected_across(self, p):
m = Point(p)
m += p
m -= self
return m
@staticmethod
def orientation(p, q, r):
"""Determine the clockwise, linear, or counterclockwise orientation of the given points"""
val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])
if val == 0:
return 0
elif val > 0:
return 1
else:
return 2
@staticmethod
def convex_hull(pts):
if len(pts) == 0:
return
points = sorted(set(pts), key=lambda p: p[0])
first_point_on_hull = points[0]
point_on_hull = first_point_on_hull
while True:
yield point_on_hull
endpoint = point_on_hull
for t in points:
if point_on_hull is endpoint \
or Point.orientation(point_on_hull, t, endpoint) == 2:
endpoint = t
point_on_hull = endpoint
if first_point_on_hull is point_on_hull:
break
@staticmethod
def distance(p1, p2):
dx = p1[0] - p2[0]
dy = p1[1] - p2[1]
dx *= dx
dy *= dy
return sqrt(dx + dy)
@staticmethod
def polar(p1, angle, r):
dx = cos(angle) * r
dy = sin(angle) * r
return Point(p1[0] + dx, p1[1] + dy)
@staticmethod
def angle(p1, p2):
return Angle.radians(atan2(p2[1] - p1[1], p2[0] - p1[0]))
@staticmethod
def towards(p1, p2, amount):
tx = amount * (p2[0] - p1[0]) + p1[0]
ty = amount * (p2[1] - p1[1]) + p1[1]
return Point(tx, ty)
class Angle(float):
"""CSS Angle defines as used in SVG/CSS"""
def __repr__(self):
return 'Angle(%.12f)' % self
def __copy__(self):
return Angle(self)
def __eq__(self, other):
# Python 2
c1 = abs((self % tau) - (other % tau)) <= 1e-11
return c1
def normalized(self):
return Angle(self % tau)
@classmethod
def parse(cls, angle_string):
if not isinstance(angle_string, str):
return
angle_string = angle_string.lower()
if angle_string.endswith('deg'):
return Angle.degrees(float(angle_string[:-3]))
if angle_string.endswith('grad'):
return Angle.gradians(float(angle_string[:-4]))
if angle_string.endswith('rad'): # Must be after 'grad' since 'grad' ends with 'rad' too.
return Angle.radians(float(angle_string[:-3]))
if angle_string.endswith('turn'):
return Angle.turns(float(angle_string[:-4]))
if angle_string.endswith('%'):
return Angle.turns(float(angle_string[:-1]) / 100.0)
return Angle.degrees(float(angle_string))
@classmethod
def radians(cls, radians):
return cls(radians)
@classmethod
def degrees(cls, degrees):
return cls(tau * degrees / 360.0)
@classmethod
def gradians(cls, gradians):
return cls(tau * gradians / 400.0)
@classmethod
def turns(cls, turns):
return cls(tau * turns)
@property
def as_radians(self):
return self
@property
def as_degrees(self):
return self * 360.0 / tau
@property
def as_positive_degrees(self):
v = self.as_degrees
while v < 0:
v += 360.0
return v
@property
def as_gradians(self):
return self * 400.0 / tau
@property
def as_turns(self):
return self / tau
def is_orthogonal(self):
return (self % (tau / 4)) == 0
class Matrix:
""""
Provides svg matrix interfacing.
SVG 7.15.3 defines the matrix form as:
[a c e]
[b d f]
While e and f are defined as floats, they can be for limited periods defined as a Length.
With regard to CSS, it's reasonable to perform operations like 'transform(20cm, 20cm)' and
expect these to be treated consistently. Performing other matrix operations in a consistent
way. However, render must be called to change these parameters into float locations prior to
any operation which might be used to transform a point or polyline or path object.
"""
def __init__(self, *components, **kwargs):
self.a = 1.0
self.b = 0.0
self.c = 0.0
self.d = 1.0
self.e = 0.0
self.f = 0.0
len_args = len(components)
if len_args == 0:
pass
elif len_args == 1:
m = components[0]
if isinstance(m, str):
self.parse(m)
self.render(**kwargs)
else:
self.a = m[0]
self.b = m[1]
self.c = m[2]
self.d = m[3]
self.e = m[4]
self.f = m[5]
else:
self.a = components[0]
self.b = components[1]
self.c = components[2]
self.d = components[3]
self.e = components[4]
self.f = components[5]
self.render(**kwargs)
def __ne__(self, other):
return not self.__eq__(other)
def __eq__(self, other):
if other is None:
return False
if isinstance(other, str):
other = Matrix(other)
if not isinstance(other, Matrix):
return False
if abs(self.a - other.a) > 1e-12:
return False
if abs(self.b - other.b) > 1e-12:
return False
if abs(self.c - other.c) > 1e-12:
return False
if abs(self.d - other.d) > 1e-12:
return False
if self.e != other.e and abs(self.e - other.e) > 1e-12:
return False
if self.f != other.f and abs(self.f - other.f) > 1e-12:
return False
return True
def __len__(self):
return 6
def __invert__(self):
m = self.__copy__()
return m.inverse()
def __matmul__(self, other):
m = copy(self)
m.__imatmul__(other)
return m
def __rmatmul__(self, other):
m = copy(other)
m.__imatmul__(self)
return m
def __imatmul__(self, other):
if isinstance(other, str):
other = Matrix(other)
self.a, self.b, self.c, self.d, self.e, self.f = Matrix.matrix_multiply(self, other)
return self
__mul__ = __matmul__
__rmul__ = __rmatmul__
__imul__ = __imatmul__
def __getitem__(self, item):
if item == 0:
return float(self.a)
elif item == 1:
return float(self.b)
elif item == 2:
return float(self.c)
elif item == 3:
return float(self.d)
elif item == 4:
return self.e
elif item == 5:
return self.f
def __setitem__(self, key, value):
if key == 0:
self.a = value
elif key == 1:
self.b = value
elif key == 2:
self.c = value
elif key == 3:
self.d = value
elif key == 4:
self.e = value
elif key == 5:
self.f = value
def __repr__(self):
return 'Matrix(%s, %s, %s, %s, %s, %s)' % \
(Length.str(self.a), Length.str(self.b),
Length.str(self.c), Length.str(self.d),
Length.str(self.e), Length.str(self.f))
def __copy__(self):
return Matrix(self.a, self.b, self.c, self.d, self.e, self.f)
def __str__(self):
"""
Many of SVG's graphics operations utilize 2x3 matrices of the form:
:returns string representation of matrix.
"""
return "[%3f, %3f,\n %3f, %3f, %s, %s]" % \
(self.a, self.c, self.b, self.d, self.e, self.f)
def parse(self, transform_str):
"""Parses the svg transform string.
Transforms from SVG 1.1 have a smaller complete set of operations. Whereas in SVG 2.0 they gain
the CSS transforms and the additional functions and parsing that go with that. This parse is
compatible with SVG 1.1 and the SVG 2.0 which includes the CSS 2d superset.
CSS transforms have scalex() scaley() translatex(), translatey(), and skew() (deprecated).
2D CSS angles haves units: "deg" tau / 360, "rad" tau/tau, "grad" tau/400, "turn" tau.
2D CSS distances have length/percentages: "px", "cm", "mm", "in", "pt", etc. (+|-)?d+%
In the case of percentages there must be a known height and width to properly create a matrix out of that.
"""
if not transform_str:
return
if not isinstance(transform_str, str):
raise TypeError('Must provide a string to parse')
for sub_element in REGEX_TRANSFORM_TEMPLATE.findall(transform_str.lower()):
name = sub_element[0]
params = tuple(REGEX_TRANSFORM_PARAMETER.findall(sub_element[1]))
params = [mag + units for mag, units in params]
if SVG_TRANSFORM_MATRIX == name:
params = map(float, params)
self.pre_cat(*params)
elif SVG_TRANSFORM_TRANSLATE == name:
try:
x_param = Length(params[0]).value()
except IndexError:
continue
try:
y_param = Length(params[1]).value()
self.pre_translate(x_param, y_param)
except IndexError:
self.pre_translate(x_param)
elif SVG_TRANSFORM_TRANSLATE_X == name:
self.pre_translate(Length(params[0]).value(), 0)
elif SVG_TRANSFORM_TRANSLATE_Y == name:
self.pre_translate(0, Length(params[0]).value())
elif SVG_TRANSFORM_SCALE == name:
params = map(float, params)
self.pre_scale(*params)
elif SVG_TRANSFORM_SCALE_X == name:
self.pre_scale(float(params[0]), 1)
elif SVG_TRANSFORM_SCALE_Y == name:
self.pre_scale(1, float(params[0]))
elif SVG_TRANSFORM_ROTATE == name:
angle = Angle.parse(params[0])
try:
x_param = Length(params[1]).value()
except IndexError:
self.pre_rotate(angle)
continue
try:
y_param = Length(params[2]).value()
self.pre_rotate(angle, x_param, y_param)
except IndexError:
self.pre_rotate(angle, x_param)
elif SVG_TRANSFORM_SKEW == name:
angle_a = Angle.parse(params[0])
try:
angle_b = Angle.parse(params[1])
except IndexError: # this isn't valid.
continue
try:
x_param = Length(params[2]).value()
except IndexError:
self.pre_skew(angle_a, angle_b)
continue
try:
y_param = Length(params[3]).value()
self.pre_skew(angle_a, angle_b, x_param, y_param)
except IndexError:
self.pre_skew(angle_a, angle_b, x_param)
elif SVG_TRANSFORM_SKEW_X == name:
angle_a = Angle.parse(params[0])
try:
x_param = Length(params[1]).value()
except IndexError:
self.pre_skew_x(angle_a)
continue
try:
y_param = Length(params[2]).value()
self.pre_skew_x(angle_a, x_param, y_param)
except IndexError:
self.pre_skew_x(angle_a, x_param)
elif SVG_TRANSFORM_SKEW_Y == name:
angle_b = Angle.parse(params[0])
try:
x_param = Length(params[1]).value()
except IndexError:
self.pre_skew_y(angle_b)
continue
try:
y_param = Length(params[2]).value()
self.pre_skew_y(angle_b, x_param, y_param)
except IndexError:
self.pre_skew_y(angle_b, x_param)
return self
def render(self, ppi=None, relative_length=None, width=None, height=None,
font_size=None, font_height=None, viewbox=None, **kwargs):
"""
Provides values to turn trans_x and trans_y values into user units floats rather
than Lengths by giving the required information to perform the conversions.
"""
if width is None and relative_length is not None:
width = relative_length
if height is None and relative_length is not None:
height = relative_length
if isinstance(self.e, Length):
self.e = self.e.value(ppi=ppi, relative_length=width, font_size=font_size,
font_height=font_height, viewbox=viewbox)
if isinstance(self.f, Length):
self.f = self.f.value(ppi=ppi, relative_length=height, font_size=font_size,
font_height=font_height, viewbox=viewbox)
return self
def value_trans_x(self):
return self.e
def value_trans_y(self):
return self.f
def value_scale_x(self):
return float(self.a)
def value_scale_y(self):
return float(self.d)
def value_skew_x(self):
return float(self.b)
def value_skew_y(self):
return float(self.c)
def reset(self):
"""Resets matrix to identity."""
self.a = 1.0
self.b = 0.0
self.c = 0.0
self.d = 1.0
self.e = 0.0
self.f = 0.0
def inverse(self):
"""
SVG Matrix:
[a c e]
[b d f]
"""
m00 = self.a
m01 = self.c
m02 = self.e
m10 = self.b
m11 = self.d
m12 = self.f
determinant = m00 * m11 - m01 * m10
inverse_determinant = 1.0 / determinant
self.a = m11 * inverse_determinant
self.c = -m01 * inverse_determinant
self.b = -m10 * inverse_determinant
self.d = m00 * inverse_determinant
self.e = (m01 * m12 - m02 * m11) * inverse_determinant
self.f = (m10 * m02 - m00 * m12) * inverse_determinant
return self
def vector(self):
"""
provide the matrix suitable for multiplying vectors. This will be the matrix with the same rotation and scale
aspects but with no translation. This matrix is for multiplying vector elements where the position doesn't
matter but the scaling and rotation do.
:return:
"""
return Matrix(self.a, self.b, self.c, self.d, 0.0, 0.0)
def is_identity(self):
return self.a == 1 and self.b == 0 and self.c == 0 and self.d == 1 and self.e == 0 and self.f == 0
def post_cat(self, *components):
mx = Matrix(*components)
self.__imatmul__(mx)
def post_scale(self, sx=1.0, sy=None, x=0.0, y=0.0):
if sy is None:
sy = sx
if x is None:
x = 0.0
if y is None:
y = 0.0
if x == 0 and y == 0:
self.post_cat(Matrix.scale(sx, sy))
else:
self.post_translate(-x, -y)
self.post_scale(sx, sy)
self.post_translate(x, y)
def post_scale_x(self, sx=1.0, x=0.0, y=0.0):
self.post_scale(sx, 1, x, y)
def post_scale_y(self, sy=1.0, x=0.0, y=0.0):
self.post_scale(1, sy, x, y)
def post_translate(self, tx=0.0, ty=0.0):
self.post_cat(Matrix.translate(tx, ty))
def post_translate_x(self, tx=0.0):
self.post_translate(tx, 0.0)
def post_translate_y(self, ty=0.0):
self.post_translate(0.0, ty)
def post_rotate(self, angle, x=0.0, y=0.0):
if x is None:
x = 0.0
if y is None:
y = 0.0
if x == 0 and y == 0:
self.post_cat(Matrix.rotate(angle)) # self %= self.get_rotate(theta)
else:
matrix = Matrix()
matrix.post_translate(-x, -y)
matrix.post_cat(Matrix.rotate(angle))
matrix.post_translate(x, y)
self.post_cat(matrix)
def post_skew(self, angle_a=0.0, angle_b=0.0, x=0.0, y=0.0):
if x is None:
x = 0
if y is None:
y = 0
if x == 0 and y == 0:
self.post_cat(Matrix.skew(angle_a, angle_b))
else:
self.post_translate(-x, -y)
self.post_skew(angle_a, angle_b)
self.post_translate(x, y)
def post_skew_x(self, angle_a=0.0, x=0.0, y=0.0):
self.post_skew(angle_a, 0.0, x, y)
def post_skew_y(self, angle_b=0.0, x=0.0, y=0.0):
self.post_skew(0.0, angle_b, x, y)
def pre_cat(self, *components):
mx = Matrix(*components)
self.a, self.b, self.c, self.d, self.e, self.f = Matrix.matrix_multiply(mx, self)
def pre_scale(self, sx=1.0, sy=None, x=0.0, y=0.0):
if sy is None:
sy = sx
if x is None:
x = 0.0
if y is None:
y = 0.0
if x == 0 and y == 0:
self.pre_cat(Matrix.scale(sx, sy))
else:
self.pre_translate(x, y)
self.pre_scale(sx, sy)
self.pre_translate(-x, -y)
def pre_scale_x(self, sx=1.0, x=0.0, y=0.0):
self.pre_scale(sx, 1, x, y)
def pre_scale_y(self, sy=1.0, x=0.0, y=0.0):
self.pre_scale(1, sy, x, y)
def pre_translate(self, tx=0.0, ty=0.0):
self.pre_cat(Matrix.translate(tx, ty))
def pre_translate_x(self, tx=0.0):
self.pre_translate(tx, 0.0)
def pre_translate_y(self, ty=0.0):
self.pre_translate(0.0, ty)
def pre_rotate(self, angle, x=0.0, y=0.0):
if x is None:
x = 0
if y is None:
y = 0
if x == 0 and y == 0:
self.pre_cat(Matrix.rotate(angle))
else:
self.pre_translate(x, y)
self.pre_rotate(angle)
self.pre_translate(-x, -y)
def pre_skew(self, angle_a=0.0, angle_b=0.0, x=0.0, y=0.0):
if x is None:
x = 0
if y is None:
y = 0
if x == 0 and y == 0:
self.pre_cat(Matrix.skew(angle_a, angle_b))
else:
self.pre_translate(x, y)
self.pre_skew(angle_a, angle_b)
self.pre_translate(-x, -y)
def pre_skew_x(self, angle_a=0.0, x=0.0, y=0.0):
self.pre_skew(angle_a, 0, x, y)
def pre_skew_y(self, angle_b=0.0, x=0.0, y=0.0):
self.pre_skew(0.0, angle_b, x, y)
def point_in_inverse_space(self, v0):
inverse = Matrix(self)
inverse.inverse()
return inverse.point_in_matrix_space(v0)
def point_in_matrix_space(self, v0):
return Point(v0[0] * self.a + v0[1] * self.c + 1 * self.e,
v0[0] * self.b + v0[1] * self.d + 1 * self.f)
def transform_point(self, v):
nx = v[0] * self.a + v[1] * self.c + 1 * self.e
ny = v[0] * self.b + v[1] * self.d + 1 * self.f
v[0] = nx
v[1] = ny
return v
def transform_vector(self, v):
"""
Applies the transformation without the translation.
"""
nx = v[0] * self.a + v[1] * self.c
ny = v[0] * self.b + v[1] * self.d
v[0] = nx
v[1] = ny
return v
@classmethod
def scale(cls, sx=1.0, sy=None):
if sy is None:
sy = sx
return cls(sx, 0,
0, sy, 0, 0)
@classmethod
def scale_x(cls, sx=1.0):
return cls.scale(sx, 1.0)
@classmethod
def scale_y(cls, sy=1.0):
return cls.scale(1.0, sy)
@classmethod
def translate(cls, tx=0.0, ty=0.0):
"""SVG Matrix:
[a c e]
[b d f]
"""
return cls(1.0, 0.0,
0.0, 1.0, tx, ty)
@classmethod
def translate_x(cls, tx=0.0):
return cls.translate(tx, 0)
@classmethod
def translate_y(cls, ty=0.0):
return cls.translate(0.0, ty)
@classmethod
def rotate(cls, angle=0.0):
ct = cos(angle)
st = sin(angle)
return cls(ct, st,
-st, ct, 0.0, 0.0)
@classmethod
def skew(cls, angle_a=0.0, angle_b=0.0):
aa = tan(angle_a)
bb = tan(angle_b)
return cls(1.0, bb,
aa, 1.0, 0.0, 0.0)
@classmethod
def skew_x(cls, angle=0.0):
return cls.skew(angle, 0.0)
@classmethod
def skew_y(cls, angle=0.0):
return cls.skew(0.0, angle)
@classmethod
def identity(cls):
"""
1, 0, 0,
0, 1, 0,
"""
return cls()
@staticmethod
def matrix_multiply(m, s):
"""
[a c e] [a c e] [a b 0]
[b d f] % [b d f] = [c d 0]
[0 0 1] [0 0 1] [e f 1]
:param m0: matrix operand
:param m1: matrix operand
:return: muliplied matrix.
"""
r0 = s.a * m.a + s.c * m.b + s.e * 0, \
s.a * m.c + s.c * m.d + s.e * 0, \
s.a * m.e + s.c * m.f + s.e * 1
r1 = s.b * m.a + s.d * m.b + s.f * 0, \
s.b * m.c + s.d * m.d + s.f * 0, \
s.b * m.e + s.d * m.f + s.f * 1
return float(r0[0]), float(r1[0]), float(r0[1]), float(r1[1]), r0[2], r1[2]
class SVGElement(object):
"""
Any element within the SVG namespace.
if args[0] is a dict or SVGElement class the value is used to seed the values.
Else, the values consist of the kwargs used. The priority is such that kwargs
will overwrite any previously set value.
If additional args exist these will be passed to property_by_args
"""
def __init__(self, *args, **kwargs):
self.id = None
self.values = None
if len(args) >= 1:
s = args[0]
if isinstance(s, dict):
args = args[1:]
self.values = dict(s)
self.values.update(kwargs)
elif isinstance(s, SVGElement):
args = args[1:]
self.property_by_object(s)
self.property_by_args(*args)
return
if self.values is None:
self.values = dict(kwargs)
self.property_by_values(self.values)
if len(args) != 0:
self.property_by_args(*args)
def property_by_args(self, *args):
pass
def property_by_object(self, obj):
self.id = obj.id
self.values = dict(obj.values)
def property_by_values(self, values):
self.id = values.get(SVG_ATTR_ID)
class Group(SVGElement, list):
"""
Group Container element can have children.
SVG 2.0 <g> are defined in:
5.2. Grouping: the g element
"""
# TODO: SVG group objects must actually possess transformation matrices.
def __init__(self, *args, **kwargs):
SVGElement.__init__(self, *args, **kwargs)
list.__init__(self)
if len(args) >= 1:
s = args[0]
if isinstance(s, Group):
self.extend(list(map(copy, s)))
return
def __copy__(self):
return Group(self)
def select(self, conditional=None):
"""
Finds all flattened subobjects of this group for which the conditional returns
true.
:param conditional: function taking element and returns True or False if matching
"""
if conditional is None:
def conditional(item):
return True
for subitem in self:
if not conditional(subitem):
continue
yield subitem
if isinstance(subitem, Group):
for s in subitem.select(conditional):
yield s
def reify(self):
pass
class Transformable(SVGElement):
"""Any element that is transformable and has a transform property."""
def __init__(self, *args, **kwargs):
self.transform = None
self.apply = None
SVGElement.__init__(self, *args, **kwargs)
def property_by_object(self, s):
SVGElement.property_by_object(self, s)
self.transform = Matrix(s.transform)
self.apply = s.apply
def property_by_values(self, values):
SVGElement.property_by_values(self, values)
self.transform = Matrix(self.values.get(SVG_ATTR_TRANSFORM, ''))
self.apply = bool(self.values.get('apply', True))
def __mul__(self, other):
if isinstance(other, (Matrix, str)):
n = copy(self)
n *= other
return n
return NotImplemented
__rmul__ = __mul__
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
self.transform *= other
return self
def __abs__(self):
"""
The absolute value is taken to be the actual shape transformed.
:return: transformed version of the given shape.
"""
m = copy(self)
m.reify()
return m
def reify(self):
"""
Realizes the transform to the attributes. Such that the attributes become actualized and the transform
simplifies towards the identity matrix. In many cases it will become the identity matrix. In other cases the
transformed shape cannot be represented through the properties alone. And shall keep those parts of the
transform required preserve equivalency.
The default method will be called by submethods but will only scale properties like stroke_width which should
scale with the transform.
"""
return self
def render(self, **kwargs):
"""
Renders the transformable by performing any required length conversion operations into pixels. The element
will be the pixel-length form.
"""
self.transform.render(**kwargs)
return self
def bbox(self, transformed=True):
"""
Returns the bounding box of the given object.
:param transformed: whether this is the transformed bounds or default.
:return:
"""
raise NotImplementedError
@property
def rotation(self):
if not self.apply:
return Angle.degrees(0)
prx = Point(1, 0)
prx *= self.transform
origin = Point(0, 0)
origin *= self.transform
return origin.angle_to(prx)
class GraphicObject(SVGElement):
"""Any drawn element."""
def __init__(self, *args, **kwargs):
self.stroke = None
self.fill = None
SVGElement.__init__(self, *args, **kwargs)
def property_by_object(self, s):
if s.fill is None:
self.fill = None
else:
self.fill = Color(s.fill)
if s.stroke is None:
self.stroke = None
else:
self.stroke = Color(s.stroke)
def property_by_values(self, values):
if SVG_ATTR_STROKE in values:
stroke = values[SVG_ATTR_STROKE]
if stroke is None:
self.stroke = None
else:
self.stroke = Color(stroke)
if SVG_ATTR_FILL in values:
fill = values[SVG_ATTR_FILL]
if fill is None:
self.fill = None
else:
self.fill = Color(fill)
class Shape(GraphicObject, Transformable):
"""
SVG Shapes are several SVG items defined in SVG 1.1 9.1
https://www.w3.org/TR/SVG11/shapes.html
These shapes are circle, ellipse, line, polyline, polygon, and path.
All shapes have methods:
d(relative, transform): provides path_d string for the shape.
reify(): Applies transform of the shape to modify the shape attributes.
render(): Ensure that the shape properties have real space values.
bbox(transformed): Provides the bounding box for the given shape.
All shapes must implement:
__repr__(), with a call to _repr_shape()
__copy__()
All shapes have attributes:
id: SVG ID attributes. (SVGElement)
transform: SVG Matrix to apply to this shape. (Transformable)
apply: Determine whether transform should be applied. (Transformable)
fill: SVG color of the shape fill. (GraphicObject)
stroke: SVG color of the shape stroke. (GraphicObject)
"""
def __init__(self, *args, **kwargs):
Transformable.__init__(self, *args, **kwargs)
GraphicObject.__init__(self, *args, **kwargs)
def property_by_object(self, s):
Transformable.property_by_object(self, s)
GraphicObject.property_by_object(self, s)
def property_by_values(self, values):
Transformable.property_by_values(self, values)
GraphicObject.property_by_values(self, values)
def __eq__(self, other):
if not isinstance(other, Shape):
return NotImplemented
if self.fill != other.fill or self.stroke != other.stroke:
return False
first = self
if not isinstance(first, Path):
first = Path(first)
second = other
if not isinstance(second, Path):
second = Path(second)
return first == second
def __ne__(self, other):
if not isinstance(other, Shape):
return NotImplemented
return not self == other
def __iadd__(self, other):
if isinstance(other, Shape):
return Path(self) + Path(other)
return NotImplemented
__add__ = __iadd__
def __matmul__(self, other):
m = copy(self)
m.__imatmul__(other)
return m
def __rmatmul__(self, other):
m = copy(other)
m.__imatmul__(self)
return m
def __imatmul__(self, other):
"""
The % operation with a matrix works much like multiplication except that it automatically reifies the shape.
"""
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
self.transform *= other
self.reify()
return self
def segments(self, transformed=True):
"""
Returns PathSegments which correctly produce this shape.
This should be implemented by subclasses.
"""
raise NotImplementedError
def d(self, relative=False, transformed=True):
"""
Returns the path_d string of the shape.
:param relative: Returns path_d in relative form.
:param transformed: Return path_d, with applied transform.
:return: path_d string
"""
return Path(self.segments(transformed=transformed)).d(relative=relative)
def bbox(self, transformed=True):
"""
Get the bounding box for the given shape.
"""
bbs = [seg.bbox() for seg in self.segments(transformed=False) if not isinstance(Close, Move)]
try:
xmins, ymins, xmaxs, ymaxs = list(zip(*bbs))
except ValueError:
return None # No bounding box items existed. So no bounding box.
xmin = min(xmins)
xmax = max(xmaxs)
ymin = min(ymins)
ymax = max(ymaxs)
if transformed:
p0 = self.transform.transform_point([xmin, ymin])
p1 = self.transform.transform_point([xmin, ymax])
p2 = self.transform.transform_point([xmax, ymin])
p3 = self.transform.transform_point([xmax, ymax])
xmin = min(p0[0], p1[0], p2[0], p3[0])
ymin = min(p0[1], p1[1], p2[1], p3[1])
xmax = max(p0[0], p1[0], p2[0], p3[0])
ymax = max(p0[1], p1[1], p2[1], p3[1])
return xmin, ymin, xmax, ymax
def _init_shape(self, *args):
"""
Generic SVG parsing of args. In those cases where the shape accepts finite elements we can process the last
four elements of the shape with this code. This will happen in simpleline, roundshape, and rect. It will not
happen in polyshape or paths since these can accept infinite arguments.
"""
arg_length = len(args)
if arg_length >= 1:
if args[0] is not None:
self.transform = Matrix(args[0])
if arg_length >= 2:
if args[1] is not None:
self.stroke = Color(args[1])
if arg_length >= 3:
if args[2] is not None:
self.fill = Color(args[2])
if arg_length >= 4:
if args[3] is not None:
self.apply = bool(args[3])
def _repr_shape(self, values):
"""
Generic pieces of repr shape.
"""
if not self.transform.is_identity():
values.append('transform=%s' % repr(self.transform))
if self.stroke is not None:
values.append('stroke=\'%s\'' % self.stroke)
if self.fill is not None:
values.append('fill=\'%s\'' % self.fill)
if self.apply is not None and not self.apply:
values.append('apply=%s' % self.apply)
if self.id is not None:
values.append('id=\'%s\'' % self.id)
def _name(self):
return self.__class__.__name__
class PathSegment:
"""
Path Segments are the base class for all the segment within a Path.
These are defined in SVG 1.1 8.3 and SVG 2.0 9.3
https://www.w3.org/TR/SVG11/paths.html#PathData
https://www.w3.org/TR/SVG2/paths.html#PathElement
These segments define a 1:1 relationship with the path_d or path data attribute, denoted in
SVG by the 'd' attribute. These are moveto, closepath, lineto, and the curves which are cubic
bezier curves, quadratic bezier curves, and elliptical arc. These are classed as Move, Close,
Line, CubicBezier, QuadraticBezier, and Arc. And in path_d are denoted as M, Z, L, C, Q, A.
There are lowercase versions of these commands. And for C, and Q there are S and T which are
smooth versions. For lines there are also V and H commands which denote vertical and horizontal
versions of the line command.
The major difference between paths in 1.1 and 2.0 is the use of Z to truncate a command to close.
"M0,0C 0,100 100,0 z is valid in 2.0 since the last z replaces the 0,0. These are read by
svg.elements but they are not written.
"""
def __init__(self):
self.start = None
self.end = None
def __mul__(self, other):
if isinstance(other, (Matrix, str)):
n = copy(self)
n *= other
return n
return NotImplemented
__rmul__ = __mul__
def __iadd__(self, other):
if isinstance(other, PathSegment):
path = Path(self, other)
return path
elif isinstance(other, str):
path = Path(self) + other
return path
return NotImplemented
__add__ = __iadd__
def __str__(self):
d = self.d()
if self.start is not None:
return 'M %s %s' % (self.start, d)
return d
def __iter__(self):
self.n = -1
return self
def __next__(self):
self.n += 1
try:
val = self[self.n]
if val is None:
self.n += 1
val = self[self.n]
return val
except IndexError:
raise StopIteration
next = __next__
@staticmethod
def segment_length(curve, start=0.0, end=1.0, start_point=None, end_point=None, error=ERROR, min_depth=MIN_DEPTH,
depth=0):
"""Recursively approximates the length by straight lines"""
if start_point is None:
start_point = curve.point(start)
if end_point is None:
end_point = curve.point(end)
mid = (start + end) / 2
mid_point = curve.point(mid)
length = abs(end_point - start_point)
first_half = abs(mid_point - start_point)
second_half = abs(end_point - mid_point)
length2 = first_half + second_half
if (length2 - length > error) or (depth < min_depth):
# Calculate the length of each segment:
depth += 1
return (PathSegment.segment_length(curve, start, mid, start_point, mid_point,
error, min_depth, depth) +
PathSegment.segment_length(curve, mid, end, mid_point, end_point,
error, min_depth, depth))
# This is accurate enough.
return length2
def _line_length(self, start=0.0, end=1.0, error=ERROR, min_depth=MIN_DEPTH):
return PathSegment.segment_length(self, start, end, error=error, min_depth=min_depth)
def bbox(self):
"""returns the bounding box for the segment.
xmin, ymin, xmax, ymax
"""
xs = [p[0] for p in self if p is not None]
ys = [p[1] for p in self if p is not None]
xmin = min(xs)
xmax = max(xs)
ymin = min(ys)
ymax = max(ys)
return xmin, ymin, xmax, ymax
def reverse(self):
end = self.end
self.end = self.start
self.start = end
def point(self, position):
return self.end
def length(self, error=ERROR, min_depth=MIN_DEPTH):
return 0
def d(self, current_point=None, smooth=False):
"""If current point is None, the function will return the absolute form. If it contains a point,
it will give the value relative to that point."""
raise NotImplementedError
class Move(PathSegment):
"""Represents move commands. Does nothing, but is there to handle
paths that consist of only move commands, which is valid, but pointless.
Also serve as a bridge to make discontinuous paths into continuous paths
with non-drawn sections.
"""
def __init__(self, *args, **kwargs):
"""
Move commands most importantly go to a place. So if one location is given, that's the end point.
If two locations are given then first is the start location.
Move(p) where p is the End point.
Move(s,e) where s is the Start point, e is the End point.
Move(p, start=s) where p is End point, s is the Start point.
Move(p, end=e) where p is the Start point, e is the End point.
Move(start=s, end=e) where s is the Start point, e is the End point.
"""
PathSegment.__init__(self)
self.end = None
self.start = None
if len(args) == 0:
if 'end' in kwargs:
self.end = kwargs['end']
if 'start' in kwargs:
self.start = kwargs['start']
elif len(args) == 1:
if len(kwargs) == 0:
self.end = args[0]
else:
if 'end' in kwargs:
self.start = args[0]
self.end = kwargs['end']
elif 'start' in kwargs:
self.start = kwargs['start']
self.end = args[0]
elif len(args) == 2:
self.start = args[0]
self.end = args[1]
if self.start is not None:
self.start = Point(self.start)
if self.end is not None:
self.end = Point(self.end)
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
if self.start is not None:
self.start *= other
if self.end is not None:
self.end *= other
return self
def __repr__(self):
if self.start is None:
return 'Move(end=%s)' % repr(self.end)
else:
return 'Move(start=%s, end=%s)' % (repr(self.start), repr(self.end))
def __copy__(self):
return Move(self.start, self.end)
def __eq__(self, other):
if not isinstance(other, Move):
return NotImplemented
return self.start == other.start and self.end == other.end
def __ne__(self, other):
if not isinstance(other, Move):
return NotImplemented
return not self == other
def __len__(self):
return 2
def __getitem__(self, item):
if item == 0:
return self.start
elif item == 1:
return self.end
else:
raise IndexError
def d(self, current_point=None, smooth=False):
if current_point is None:
return 'M %s' % (self.end)
else:
return 'm %s' % (self.end - current_point)
class Close(PathSegment):
"""Represents close commands. If this exists at the end of the shape then the shape is closed.
the methodology of a single flag close fails in a couple ways. You can have multi-part shapes
which can close or not close several times.
"""
def __init__(self, start=None, end=None):
PathSegment.__init__(self)
self.end = None
self.start = None
if start is not None:
self.start = Point(start)
if end is not None:
self.end = Point(end)
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
if self.start is not None:
self.start *= other
if self.end is not None:
self.end *= other
return self
def __repr__(self):
if self.start is None and self.end is None:
return 'Close()'
s = self.start
if s is not None:
s = repr(s)
e = self.end
if e is not None:
e = repr(e)
return 'Close(start=%s, end=%s)' % (s, e)
def __copy__(self):
return Close(self.start, self.end)
def __eq__(self, other):
if not isinstance(other, Close):
return NotImplemented
return self.start == other.start and self.end == other.end
def __ne__(self, other):
if not isinstance(other, Close):
return NotImplemented
return not self == other
def __len__(self):
return 2
def __getitem__(self, item):
if item == 0:
return self.start
elif item == 1:
return self.end
else:
raise IndexError
def point(self, position):
return Point.towards(self.start, self.end, position)
def length(self, error=None, min_depth=None):
if self.start is not None and self.end is not None:
return Point.distance(self.end, self.start)
else:
return 0
def d(self, current_point=None, smooth=False):
if current_point is None:
return 'Z'
else:
return 'z'
class Line(PathSegment):
"""Represents line commands."""
def __init__(self, start, end):
PathSegment.__init__(self)
self.end = None
self.start = None
if start is not None:
self.start = Point(start)
if end is not None:
self.end = Point(end)
def __repr__(self):
if self.start is None:
return 'Line(end=%s)' % (repr(self.end))
return 'Line(start=%s, end=%s)' % (repr(self.start), repr(self.end))
def __copy__(self):
return Line(self.start, self.end)
def __eq__(self, other):
if not isinstance(other, Line):
return NotImplemented
return self.start == other.start and self.end == other.end
def __ne__(self, other):
if not isinstance(other, Line):
return NotImplemented
return not self == other
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
if self.start is not None:
self.start *= other
if self.end is not None:
self.end *= other
return self
def __len__(self):
return 2
def __getitem__(self, item):
if item == 0:
return self.start
elif item == 1:
return self.end
else:
raise IndexError
def point(self, position):
return Point.towards(self.start, self.end, position)
def length(self, error=None, min_depth=None):
return Point.distance(self.end, self.start)
def closest_segment_point(self, p, respect_bounds=True):
""" Gives the t value of the point on the line closest to the given point. """
a = self.start
b = self.end
vAPx = p[0] - a[0]
vAPy = p[1] - a[1]
vABx = b[0] - a[0]
vABy = b[1] - a[1]
sqDistanceAB = vABx * vABx + vABy * vABy
ABAPproduct = vABx * vAPx + vABy * vAPy
if sqDistanceAB == 0:
return 0 # Line is point.
amount = ABAPproduct / sqDistanceAB
if respect_bounds:
if amount > 1:
amount = 1
if amount < 0:
amount = 0
return self.point(amount)
def d(self, current_point=None, smooth=False):
if current_point is None:
return 'L %s' % (self.end)
else:
return 'l %s' % (self.end - current_point)
class QuadraticBezier(PathSegment):
"""Represents Quadratic Bezier commands."""
def __init__(self, start, control, end):
PathSegment.__init__(self)
self.end = None
self.control = None
self.start = None
if start is not None:
self.start = Point(start)
if control is not None:
self.control = Point(control)
if end is not None:
self.end = Point(end)
def __repr__(self):
return 'QuadraticBezier(start=%s, control=%s, end=%s)' % (
repr(self.start), repr(self.control), repr(self.end))
def __copy__(self):
return QuadraticBezier(self.start, self.control, self.end)
def __eq__(self, other):
if not isinstance(other, QuadraticBezier):
return NotImplemented
return self.start == other.start and self.end == other.end and \
self.control == other.control
def __ne__(self, other):
if not isinstance(other, QuadraticBezier):
return NotImplemented
return not self == other
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
if self.start is not None:
self.start *= other
if self.control is not None:
self.control *= other
if self.end is not None:
self.end *= other
return self
def __len__(self):
return 3
def __getitem__(self, item):
if item == 0:
return self.start
elif item == 1:
return self.control
elif item == 2:
return self.end
raise IndexError
def point(self, position):
"""Calculate the x,y position at a certain position of the path"""
x0, y0 = self.start
x1, y1 = self.control
x2, y2 = self.end
x = (1 - position) * (1 - position) * x0 + 2 * (1 - position) * position * x1 + position * position * x2
y = (1 - position) * (1 - position) * y0 + 2 * (1 - position) * position * y1 + position * position * y2
return Point(x, y)
def bbox(self):
"""
Returns the bounding box for the quadratic bezier curve.
"""
n = self.start[0] - self.control[0]
d = self.start[0] - 2 * self.control[0] + self.end[0]
if d != 0:
t = n / d
else:
t = 0.5
if 0 < t < 1:
x_values = [self.start[0], self.end[0], self.point(t)[0]]
else:
x_values = [self.start[0], self.end[0]]
n = self.start[1] - self.control[1]
d = self.start[1] - 2 * self.control[1] + self.end[1]
if d != 0:
t = n / d
else:
t = 0.5
if 0 < t < 1:
y_values = [self.start[1], self.end[1], self.point(t)[1]]
else:
y_values = [self.start[1], self.end[1]]
return min(x_values), min(y_values), max(x_values), max(y_values)
def length(self, error=None, min_depth=None):
"""Calculate the length of the path up to a certain position"""
a = self.start - 2 * self.control + self.end
b = 2 * (self.control - self.start)
try:
# For an explanation of this case, see
# http://www.malczak.info/blog/quadratic-bezier-curve-length/
A = 4 * (a.real ** 2 + a.imag ** 2)
B = 4 * (a.real * b.real + a.imag * b.imag)
C = b.real ** 2 + b.imag ** 2
Sabc = 2 * sqrt(A + B + C)
A2 = sqrt(A)
A32 = 2 * A * A2
C2 = 2 * sqrt(C)
BA = B / A2
s = (A32 * Sabc + A2 * B * (Sabc - C2) + (4 * C * A - B ** 2) *
log((2 * A2 + BA + Sabc) / (BA + C2))) / (4 * A32)
except (ZeroDivisionError, ValueError):
# a_dot_b = a.real * b.real + a.imag * b.imag
if abs(a) < 1e-10:
s = abs(b)
else:
k = abs(b) / abs(a)
if k >= 2:
s = abs(b) - abs(a)
else:
s = abs(a) * (k ** 2 / 2 - k + 1)
return s
def is_smooth_from(self, previous):
"""Checks if this segment would be a smooth segment following the previous"""
if isinstance(previous, QuadraticBezier):
return (self.start == previous.end and
(self.control - self.start) == (previous.end - previous.control))
else:
return self.control == self.start
def d(self, current_point=None, smooth=False):
if smooth:
if current_point is None:
return 'T %s' % (self.end)
else:
return 't %s' % (self.end - current_point)
else:
if current_point is None:
return 'Q %s %s' % (self.control, self.end)
else:
return 'q %s %s' % (self.control - current_point, self.end - current_point)
class CubicBezier(PathSegment):
"""Represents Cubic Bezier commands."""
def __init__(self, start, control1, control2, end):
PathSegment.__init__(self)
self.end = None
self.control1 = None
self.control2 = None
self.start = None
if start is not None:
self.start = Point(start)
if control1 is not None:
self.control1 = Point(control1)
if control2 is not None:
self.control2 = Point(control2)
if end is not None:
self.end = Point(end)
def __repr__(self):
return 'CubicBezier(start=%s, control1=%s, control2=%s, end=%s)' % (
repr(self.start), repr(self.control1), repr(self.control2), repr(self.end))
def __copy__(self):
return CubicBezier(self.start, self.control1, self.control2, self.end)
def __eq__(self, other):
if not isinstance(other, CubicBezier):
return NotImplemented
return self.start == other.start and self.end == other.end and \
self.control1 == other.control1 and self.control2 == other.control2
def __ne__(self, other):
if not isinstance(other, CubicBezier):
return NotImplemented
return not self == other
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
if self.start is not None:
self.start *= other
if self.control1 is not None:
self.control1 *= other
if self.control2 is not None:
self.control2 *= other
if self.end is not None:
self.end *= other
return self
def __len__(self):
return 4
def __getitem__(self, item):
if item == 0:
return self.start
elif item == 1:
return self.control1
elif item == 2:
return self.control2
elif item == 3:
return self.end
else:
raise IndexError
def reverse(self):
PathSegment.reverse(self)
c2 = self.control2
self.control2 = self.control1
self.control1 = c2
def point(self, position):
"""Calculate the x,y position at a certain position of the path"""
x0, y0 = self.start
x1, y1 = self.control1
x2, y2 = self.control2
x3, y3 = self.end
x = (1 - position) * (1 - position) * (1 - position) * x0 + \
3 * (1 - position) * (1 - position) * position * x1 + \
3 * (1 - position) * position * position * x2 + \
position * position * position * x3
y = (1 - position) * (1 - position) * (1 - position) * y0 + \
3 * (1 - position) * (1 - position) * position * y1 + \
3 * (1 - position) * position * position * y2 + \
position * position * position * y3
return Point(x, y)
def bbox(self):
"""returns the tight fitting bounding box of the bezier curve.
Code by:
https://github.com/mathandy/svgpathtools
"""
xmin, xmax = self._real_minmax(0)
ymin, ymax = self._real_minmax(1)
return xmin, ymin, xmax, ymax
def _real_minmax(self, v):
"""returns the minimum and maximum for a real cubic bezier, with a non-zero denom
Code by:
https://github.com/mathandy/svgpathtools
"""
local_extremizers = [0, 1]
a = [c[v] for c in self]
denom = a[0] - 3 * a[1] + 3 * a[2] - a[3]
if abs(denom) >= 1e-12:
delta = a[1] ** 2 -\
(a[0] + a[1]) * a[2] + \
a[2] ** 2 + \
(a[0] - a[1]) * a[3]
if delta >= 0: # otherwise no local extrema
sqdelta = sqrt(delta)
tau = a[0] - 2 * a[1] + a[2]
r1 = (tau + sqdelta) / denom
r2 = (tau - sqdelta) / denom
if 0 < r1 < 1:
local_extremizers.append(r1)
if 0 < r2 < 1:
local_extremizers.append(r2)
else:
local_extremizers.append(0.5)
local_extrema = [self.point(t)[v] for t in local_extremizers]
return min(local_extrema), max(local_extrema)
def length(self, error=ERROR, min_depth=MIN_DEPTH):
"""Calculate the length of the path up to a certain position"""
return self._line_length(0, 1, error, min_depth)
def is_smooth_from(self, previous):
"""Checks if this segment would be a smooth segment following the previous"""
if isinstance(previous, CubicBezier):
return (self.start == previous.end and
(self.control1 - self.start) == (previous.end - previous.control2))
else:
return self.control1 == self.start
def d(self, current_point=None, smooth=False):
if smooth:
if current_point is None:
return 'S %s %s' % (self.control2, self.end)
else:
return 's %s %s' % (self.control2 - current_point, self.end - current_point)
else:
if current_point is None:
return 'C %s %s %s' % (self.control1, self.control2, self.end)
else:
return 'c %s %s %s' % (
self.control1 - current_point, self.control2 - current_point, self.end - current_point)
class Arc(PathSegment):
def __init__(self, *args, **kwargs):
"""
Represents Arc commands.
Arc objects can take different parameters to create arcs.
Since we expect taking in SVG parameters. We accept SVG parameterization which is:
start, rx, ry, rotation, arc_flag, sweep_flag, end.
To do matrix transitions, the native parameterization is start, end, center, prx, pry, sweep
'start, end, center, prx, pry' are points and sweep amount is a t value in tau radians.
If points are modified by an affine transformation, the arc is transformed.
There is a special case for when the scale factor inverts, it inverts the sweep.
Note: t-values are not angles from center in ellipical arcs. These are the same thing in
circular arcs. But, here t is a parameterization around the ellipse, as if it were a circle.
The position on the arc is (a * cos(t), b * sin(t)). If r-major was 0 for example. The
positions would all fall on the x-axis. And the angle from center would all be either 0 or
tau/2. However, since t is the parameterization we can conceptualize it as a position on a
circle which is then scaled and rotated by a matrix.
prx is the point at t 0 in the ellipse.
pry is the point at t tau/4 in the ellipse.
prx -> center -> pry should form a right triangle.
The rotation can be defined as the angle from center to prx. Since prx is located at
t(0) its deviation can only be the result of a rotation.
Sweep is a value in t.
The sweep angle can be a value greater than tau and less than -tau.
However if this is the case, conversion back to Path.d() is expected to fail.
We can denote these arc events but not as a single command.
should equal sweep or mod thereof.
start_t + sweep = end_t
"""
PathSegment.__init__(self)
self.start = None
self.end = None
self.center = None
self.prx = None
self.pry = None
self.sweep = None
if len(args) == 6 and isinstance(args[1], complex):
self._svg_complex_parameterize(*args)
return
elif len(kwargs) == 6 and 'rotation' in kwargs:
self._svg_complex_parameterize(**kwargs)
return
elif len(args) == 7:
# This is an svg parameterized call.
# A: rx ry x-axis-rotation large-arc-flag sweep-flag x y
self._svg_parameterize(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
return
if 'left' in kwargs and 'right' in kwargs and 'top' in kwargs and 'bottom' in kwargs:
left = kwargs['left']
right = kwargs['right']
top = kwargs['top']
bottom = kwargs['bottom']
self.center = Point((left + right) / 2.0, (top + bottom) / 2.0)
rx = (right - left) / 2.0
ry = (bottom - top) / 2.0
self.prx = Point(self.center[0] + rx, self.center[1])
self.pry = Point(self.center[0], self.center[1] + ry)
len_args = len(args)
if len_args > 0:
if args[0] is not None:
self.start = Point(args[0])
if len_args > 1:
if args[1] is not None:
self.end = Point(args[1])
if len_args > 2:
if args[2] is not None:
self.center = Point(args[2])
if len_args > 3:
if args[3] is not None:
self.prx = Point(args[3])
if len_args > 4:
if args[4] is not None:
self.pry = Point(args[4])
if len_args > 5:
self.sweep = args[5]
return # The args gave us everything.
if 'start' in kwargs:
self.start = Point(kwargs['start'])
if 'end' in kwargs:
self.end = Point(kwargs['end'])
if 'center' in kwargs:
self.center = Point(kwargs['center'])
if 'prx' in kwargs:
self.prx = Point(kwargs['prx'])
if 'pry' in kwargs:
self.pry = Point(kwargs['pry'])
if 'sweep' in kwargs:
self.sweep = kwargs['sweep']
cw = True # Clockwise default. (sometimes needed)
if self.start is not None and self.end is not None and self.center is None:
# Start and end, but no center.
# Solutions require a radius, a control point, or a bulge
control = None
sagitta = None
if 'bulge' in kwargs:
bulge = float(kwargs['bulge'])
sagitta = bulge * self.start.distance_to(self.end) / 2.0
elif 'sagitta' in kwargs:
sagitta = float(kwargs['sagitta'])
if sagitta is not None:
control = Point.towards(self.start, self.end, 0.5)
angle = self.start.angle_to(self.end)
control = control.polar_to(angle - tau / 4, sagitta)
if 'control' in kwargs: # Control is any additional point on the arc.
control = Point(kwargs['control'])
if control is not None:
delta_a = control - self.start
delta_b = self.end - control
try:
slope_a = delta_a[1] / delta_a[0]
except ZeroDivisionError:
slope_a = float('inf')
try:
slope_b = delta_b[1] / delta_b[0]
except ZeroDivisionError:
slope_b = float('inf')
ab_mid = Point.towards(self.start, control, 0.5)
bc_mid = Point.towards(control, self.end, 0.5)
if delta_a[1] == 0: # slope_a == 0
cx = ab_mid[0]
if delta_b[0] == 0: # slope_b == inf
cy = bc_mid[1]
else:
cy = bc_mid[1] + (bc_mid.x - cx) / slope_b
elif delta_b[1] == 0: # slope_b == 0
cx = bc_mid[0]
if delta_a[1] == 0: # slope_a == inf
cy = ab_mid[1]
else:
cy = ab_mid[1] + (ab_mid[0] - cx) / slope_a
elif delta_a[0] == 0: # slope_a == inf
cy = ab_mid[1]
cx = slope_b * (bc_mid[1] - cy) + bc_mid[0]
elif delta_b[0] == 0: # slope_b == inf
cy = bc_mid[1]
cx = slope_a * (ab_mid[1] - cy) + ab_mid[0]
elif slope_a == slope_b:
cx = ab_mid[0]
cy = ab_mid[1]
else:
cx = (slope_a * slope_b * (ab_mid[1] - bc_mid[1])
- slope_a * bc_mid[0]
+ slope_b * ab_mid[0]) / (slope_b - slope_a)
cy = ab_mid[1] - (cx - ab_mid[0]) / slope_a
self.center = Point(cx, cy)
cw = bool(Point.orientation(self.start, control, self.end) == 2)
elif 'r' in kwargs:
r = kwargs['r']
mid = Point((self.start[0] + self.end[0]) / 2.0, (self.start[1] + self.end[1]) / 2.0)
q = Point.distance(self.start, self.end)
hq = q / 2.0
if r < hq:
kwargs['r'] = r = hq # Correct potential math domain error.
self.center = Point(
mid[0] + sqrt(r ** 2 - hq ** 2) * (self.start[1] - self.end[1]) / q,
mid[1] + sqrt(r ** 2 - hq ** 2) * (self.end[0] - self.start[0]) / q
)
cw = bool(Point.orientation(self.start, self.center, self.end) == 1)
if 'ccw' in kwargs and kwargs['ccw'] and cw or not cw:
# ccw arg exists, is true, and we found the cw center, or we didn't find the cw center.
self.center = Point(
mid[0] - sqrt(r ** 2 - hq ** 2) * (self.start[1] - self.end[1]) / q,
mid[1] - sqrt(r ** 2 - hq ** 2) * (self.end[0] - self.start[0]) / q
)
elif 'rx' in kwargs and 'ry' in kwargs:
# This formulation will assume p1 and p2 are both axis aligned.
rx = kwargs['rx']
ry = kwargs['ry']
# We will assume rx == abs(self.start[0] - self.end[0])
self.center = Point(self.start[0], self.end[1])
cw = bool(Point.orientation(self.start, self.center, self.end) == 1)
if 'ccw' in kwargs and kwargs['ccw'] and cw or not cw:
self.center = Point(self.end[0], self.start[1])
self.sweep = tau / 4.0
if self.center is None:
return # Center must be solvable.
if 'r' in kwargs:
r = kwargs['r']
if self.prx is None:
self.prx = Point(self.center[0] + r, self.center[1])
if self.pry is None:
self.pry = Point(self.center[0], self.center[1] + r)
if 'rx' in kwargs:
rx = kwargs['rx']
if self.prx is None:
if 'rotation' in kwargs:
theta = kwargs['rotation']
self.prx = Point.polar(self.center, theta, rx)
else:
self.prx = Point(self.center[0] + rx, self.center[1])
if 'ry' in kwargs:
ry = kwargs['ry']
if self.pry is None:
if 'rotation' in kwargs:
theta = kwargs['rotation']
theta += tau / 4.0
self.pry = Point.polar(self.center, theta, ry)
else:
self.pry = Point(self.center[0], self.center[1] + ry)
if self.start is not None and (self.prx is None or self.pry is None):
radius_s = Point.distance(self.center, self.start)
self.prx = Point(self.center[0] + radius_s, self.center[1])
self.pry = Point(self.center[0], self.center[1] + radius_s)
if self.end is not None and (self.prx is None or self.pry is None):
radius_e = Point.distance(self.center, self.end)
self.prx = Point(self.center[0] + radius_e, self.center[1])
self.pry = Point(self.center[0], self.center[1] + radius_e)
if self.sweep is None and self.start is not None and self.end is not None:
start_t = self.get_start_t()
end_t = self.get_end_t()
self.sweep = end_t - start_t
if 'ccw' in kwargs:
cw = not bool(kwargs['ccw'])
if cw and self.sweep < 0:
self.sweep += tau
if not cw and self.sweep > 0:
self.sweep -= tau
if self.sweep is not None and self.start is not None and self.end is None:
start_t = self.get_start_t()
end_t = start_t + self.sweep
self.end = self.point_at_t(end_t)
if self.sweep is not None and self.start is None and self.end is not None:
end_t = self.get_end_t()
start_t = end_t - self.sweep
self.end = self.point_at_t(start_t)
def __repr__(self):
return 'Arc(%s, %s, %s, %s, %s, %s)' % (
repr(self.start), repr(self.end), repr(self.center), repr(self.prx), repr(self.pry), self.sweep)
def __copy__(self):
return Arc(self.start, self.end, self.center, self.prx, self.pry, self.sweep)
def __eq__(self, other):
if not isinstance(other, Arc):
return NotImplemented
return self.start == other.start and self.end == other.end and \
self.prx == other.prx and self.pry == other.pry and \
self.center == other.center and self.sweep == other.sweep
def __ne__(self, other):
if not isinstance(other, Arc):
return NotImplemented
return not self == other
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
if self.start is not None:
self.start *= other
if self.center is not None:
self.center *= other
if self.end is not None:
self.end *= other
if self.prx is not None:
self.prx *= other
if self.pry is not None:
self.pry *= other
if other.value_scale_x() < 0:
self.sweep = -self.sweep
if other.value_scale_y() < 0:
self.sweep = -self.sweep
return self
def __len__(self):
return 5
def __getitem__(self, item):
if item == 0:
return self.start
elif item == 1:
return self.end
elif item == 2:
return self.center
elif item == 3:
return self.prx
elif item == 4:
return self.pry
raise IndexError
@property
def theta(self):
"""legacy property"""
return Angle.radians(self.get_start_t()).as_positive_degrees
@property
def delta(self):
"""legacy property"""
return Angle.radians(self.sweep).as_degrees
def reverse(self):
PathSegment.reverse(self)
self.sweep = -self.sweep
def point(self, position):
if self.start == self.end and self.sweep == 0:
# This is equivalent of omitting the segment
return self.start
t = self.get_start_t() + self.sweep * position
return self.point_at_t(t)
def _integral_length(self):
def ellipse_part_integral(t1, t2, a, b, n=100000):
# function to integrate
def f(t):
return sqrt(1 - (1 - a ** 2 / b ** 2) * sin(t) ** 2)
start = min(t1, t2)
seg_len = abs(t1 - t2) / n
return b * sum(f(start + seg_len * i) * seg_len for i in range(1, n + 1))
start_angle = self.get_start_t()
end_angle = start_angle + self.sweep
return ellipse_part_integral(start_angle, end_angle, self.rx, self.ry)
def _exact_length(self):
"""scipy is not a dependency. However, if scipy exists this function will find the
exact arc length. By default .length() delegates to here and on failure uses the
fallback method."""
from scipy.special import ellipeinc
a = self.rx
b = self.ry
phi = self.get_start_t()
m = 1 - (a / b) ** 2
d1 = ellipeinc(phi, m)
phi = phi + self.sweep
m = 1 - (a / b) ** 2
d2 = ellipeinc(phi, m)
return b * abs(d2 - d1)
def length(self, error=ERROR, min_depth=MIN_DEPTH):
"""The length of an elliptical arc segment requires numerical
integration, and in that case it's simpler to just do a geometric
approximation, as for cubic bezier curves.
"""
if self.sweep == 0:
return 0
if self.start == self.end and self.sweep == 0:
# This is equivalent of omitting the segment
return 0
a = self.rx
b = self.ry
d = abs(a - b)
if d < ERROR: # This is a circle.
return abs(self.rx * self.sweep)
try:
return self._exact_length()
except ImportError:
return self._line_length(error=error, min_depth=min_depth)
def _svg_complex_parameterize(self, start, radius, rotation, arc_flag, sweep_flag, end):
"""Parameterization with complex radius and having rotation factors."""
self._svg_parameterize(Point(start), radius.real, radius.imag, rotation, arc_flag, sweep_flag, Point(end))
def _svg_parameterize(self, start, rx, ry, rotation, large_arc_flag, sweep_flag, end):
"""Conversion from svg parameterization, our chosen native native form.
http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes """
large_arc_flag = bool(large_arc_flag)
sweep_flag = bool(sweep_flag)
start = Point(start)
self.start = start
end = Point(end)
self.end = end
if start == end or rx == 0 or ry == 0:
# If start is equal to end, there are infinite number of circles so these void out.
# We still permit this kind of arc, but SVG parameterization cannot be used to achieve it.
self.sweep = 0
self.prx = Point(start)
self.pry = Point(start)
self.center = Point(start)
return
cosr = cos(radians(rotation))
sinr = sin(radians(rotation))
dx = (start.real - end.real) / 2
dy = (start.imag - end.imag) / 2
x1prim = cosr * dx + sinr * dy
x1prim_sq = x1prim * x1prim
y1prim = -sinr * dx + cosr * dy
y1prim_sq = y1prim * y1prim
rx_sq = rx * rx
ry_sq = ry * ry
# Correct out of range radii
radius_check = (x1prim_sq / rx_sq) + (y1prim_sq / ry_sq)
if radius_check > 1:
rx *= sqrt(radius_check)
ry *= sqrt(radius_check)
rx_sq = rx * rx
ry_sq = ry * ry
t1 = rx_sq * y1prim_sq
t2 = ry_sq * x1prim_sq
c = sqrt(abs((rx_sq * ry_sq - t1 - t2) / (t1 + t2)))
if large_arc_flag == sweep_flag:
c = -c
cxprim = c * rx * y1prim / ry
cyprim = -c * ry * x1prim / rx
center = Point((cosr * cxprim - sinr * cyprim) +
((start.real + end.real) / 2),
(sinr * cxprim + cosr * cyprim) +
((start.imag + end.imag) / 2))
ux = (x1prim - cxprim) / rx
uy = (y1prim - cyprim) / ry
vx = (-x1prim - cxprim) / rx
vy = (-y1prim - cyprim) / ry
n = sqrt(ux * ux + uy * uy)
p = ux
theta = degrees(acos(p / n))
if uy < 0:
theta = -theta
theta = theta % 360
n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
p = ux * vx + uy * vy
d = p / n
# In certain cases the above calculation can through inaccuracies
# become just slightly out of range, f ex -1.0000000000000002.
if d > 1.0:
d = 1.0
elif d < -1.0:
d = -1.0
delta = degrees(acos(d))
if (ux * vy - uy * vx) < 0:
delta = -delta
delta = delta % 360
if not sweep_flag:
delta -= 360
# built parameters, delta, theta, center
rotate_matrix = Matrix()
rotate_matrix.post_rotate(Angle.degrees(rotation).as_radians, center[0], center[1])
self.center = center
self.prx = Point(center[0] + rx, center[1])
self.pry = Point(center[0], center[1] + ry)
self.prx.matrix_transform(rotate_matrix)
self.pry.matrix_transform(rotate_matrix)
self.sweep = Angle.degrees(delta).as_radians
def as_quad_curves(self):
sweep_limit = tau / 12
arc_required = int(ceil(abs(self.sweep) / sweep_limit))
if arc_required == 0:
return
slice = self.sweep / float(arc_required)
current_t = self.get_start_t()
p_start = self.start
theta = self.get_rotation()
cos_theta = cos(theta)
sin_theta = sin(theta)
a = self.rx
b = self.ry
cx = self.center[0]
cy = self.center[1]
for i in range(0, arc_required):
next_t = current_t + slice
mid_t = (next_t + current_t) / 2
p_end = self.point_at_t(next_t)
if i == arc_required - 1:
p_end = self.end
cos_mid_t = cos(mid_t)
sin_mid_t = sin(mid_t)
alpha = (4.0 - cos(slice)) / 3.0
px = cx + alpha * (a * cos_mid_t * cos_theta - b * sin_mid_t * sin_theta)
py = cy + alpha * (a * cos_mid_t * sin_theta + b * sin_mid_t * cos_theta)
yield QuadraticBezier(p_start, (px, py), p_end)
p_start = p_end
current_t = next_t
def as_cubic_curves(self):
sweep_limit = tau / 12
arc_required = int(ceil(abs(self.sweep) / sweep_limit))
if arc_required == 0:
return
slice = self.sweep / float(arc_required)
theta = self.get_rotation()
rx = self.rx
ry = self.ry
p_start = self.start
current_t = self.get_start_t()
x0 = self.center[0]
y0 = self.center[1]
cos_theta = cos(theta)
sin_theta = sin(theta)
for i in range(0, arc_required):
next_t = current_t + slice
alpha = sin(slice) * (sqrt(4 + 3 * pow(tan((slice) / 2.0), 2)) - 1) / 3.0
cos_start_t = cos(current_t)
sin_start_t = sin(current_t)
ePrimen1x = -rx * cos_theta * sin_start_t - ry * sin_theta * cos_start_t
ePrimen1y = -rx * sin_theta * sin_start_t + ry * cos_theta * cos_start_t
cos_end_t = cos(next_t)
sin_end_t = sin(next_t)
p2En2x = x0 + rx * cos_end_t * cos_theta - ry * sin_end_t * sin_theta
p2En2y = y0 + rx * cos_end_t * sin_theta + ry * sin_end_t * cos_theta
p_end = (p2En2x, p2En2y)
if i == arc_required - 1:
p_end = self.end
ePrimen2x = -rx * cos_theta * sin_end_t - ry * sin_theta * cos_end_t
ePrimen2y = -rx * sin_theta * sin_end_t + ry * cos_theta * cos_end_t
p_c1 = (p_start[0] + alpha * ePrimen1x, p_start[1] + alpha * ePrimen1y)
p_c2 = (p_end[0] - alpha * ePrimen2x, p_end[1] - alpha * ePrimen2y)
yield CubicBezier(p_start, p_c1, p_c2, p_end)
p_start = Point(p_end)
current_t = next_t
def is_circular(self):
a = self.rx
b = self.ry
return a == b
@property
def radius(self):
"""Legacy complex radius property
Point will work like a complex for legacy reasons.
"""
return Point(self.rx, self.ry)
@property
def rx(self):
return Point.distance(self.center, self.prx)
@property
def ry(self):
return Point.distance(self.center, self.pry)
def get_rotation(self):
return Point.angle(self.center, self.prx)
def get_start_angle(self):
"""
:return: Angle from the center point to start point.
"""
return self.angle_at_point(self.start)
def get_end_angle(self):
"""
:return: Angle from the center point to end point.
"""
return self.angle_at_point(self.end)
def get_start_t(self):
"""
start t value in the ellipse.
:return: t parameter of start point.
"""
return self.t_at_point(self.point_at_angle(self.get_start_angle()))
def get_end_t(self):
"""
end t value in the ellipse.
:return: t parameter of start point.
"""
return self.t_at_point(self.point_at_angle(self.get_end_angle()))
def point_at_angle(self, angle):
"""
find the point on the ellipse from the center at the given angle.
Note: For non-circular arcs this is different than point(t).
:param angle: angle from center to find point
:return: point found
"""
angle -= self.get_rotation()
a = self.rx
b = self.ry
if a == b:
return self.point_at_t(angle)
t = atan2(a * tan(angle), b)
tau_1_4 = tau / 4.0
tau_3_4 = 3 * tau_1_4
if tau_3_4 >= abs(angle) % tau > tau_1_4:
t += tau / 2
return self.point_at_t(t)
def angle_at_point(self, p):
"""
find the angle to the point.
:param p: point
:return: angle to given point.
"""
return self.center.angle_to(p)
def t_at_point(self, p):
"""
find the t parameter to at the point.
:param p: point
:return: t parameter to the given point.
"""
angle = self.angle_at_point(p)
angle -= self.get_rotation()
a = self.rx
b = self.ry
t = atan2(a * tan(angle), b)
tau_1_4 = tau / 4.0
tau_3_4 = 3 * tau_1_4
if tau_3_4 >= abs(angle) % tau > tau_1_4:
t += tau / 2
return t
def point_at_t(self, t):
"""
find the point that corresponds to given value t.
Where t=0 is the first point and t=tau is the final point.
In the case of a circle: t = angle.
:param t:
:return:
"""
rotation = self.get_rotation()
a = self.rx
b = self.ry
cx = self.center[0]
cy = self.center[1]
cos_rot = cos(rotation)
sin_rot = sin(rotation)
cos_t = cos(t)
sin_t = sin(t)
px = cx + a * cos_t * cos_rot - b * sin_t * sin_rot
py = cy + a * cos_t * sin_rot + b * sin_t * cos_rot
return Point(px, py)
def get_ellipse(self):
return Ellipse(self.center, self.rx, self.ry, self.get_rotation())
def bbox(self):
"""Find the bounding box of a arc.
Code from: https://github.com/mathandy/svgpathtools
"""
phi = self.get_rotation().as_radians
if cos(phi) == 0:
atan_x = pi / 2
atan_y = 0
elif sin(phi) == 0:
atan_x = 0
atan_y = pi / 2
else:
rx, ry = self.rx, self.ry
atan_x = atan(-(ry / rx) * tan(phi))
atan_y = atan((ry / rx) / tan(phi))
def angle_inv(ang, k): # inverse of angle from Arc.derivative()
return ((ang + pi * k) * (360 / (2 * pi)) - self.theta) / self.delta
xtrema = [self.start[0], self.end[0]]
ytrema = [self.start[1], self.end[1]]
for k in range(-4, 5):
tx = angle_inv(atan_x, k)
ty = angle_inv(atan_y, k)
if 0 <= tx <= 1:
xtrema.append(self.point(tx)[0])
if 0 <= ty <= 1:
ytrema.append(self.point(ty)[1])
return min(xtrema), min(ytrema), max(xtrema), max(ytrema)
def d(self, current_point=None, smooth=False):
if current_point is None:
return 'A %G,%G %G %d,%d %s' % (
self.rx,
self.ry,
self.get_rotation().as_degrees,
int(abs(self.sweep) > (tau / 2.0)),
int(self.sweep >= 0),
self.end)
else:
return 'a %G,%G %G %d,%d %s' % (
self.rx,
self.ry,
self.get_rotation().as_degrees,
int(abs(self.sweep) > (tau / 2.0)),
int(self.sweep >= 0),
self.end - current_point)
class Path(Shape, MutableSequence):
"""
A Path is a Mutable sequence of path segments
It is a generalized shape which can map out all the other shapes.
Each PathSegment object maps a particular command. Each one exists only once in each path and every point contained
within the object is also unique. We attempt to internally maintain some validity. Each end point should link
to the following segments start point. And each close point should connect from the preceding segments endpoint to
the last Move command.
These are soft checks made only at the time of addition and some manipulations. Modifying the points of the segments
can and will cause path invalidity. Some SVG invalid operations are permitted such as arcs longer than tau radians
or beginning sequences without a move. The expectation is that these will eventually be used as part of a valid path
so these fragment paths are permitted. In some cases these invalid paths will still have consistent path_d values,
in other cases, there will be no valid methods to reproduce these.
"""
def __init__(self, *args, **kwargs):
Shape.__init__(self, *args, **kwargs)
self._length = None
self._lengths = None
self._segments = list()
if len(args) != 1:
self._segments.extend(args)
else:
s = args[0]
if isinstance(s, Subpath):
self._segments.extend(s.segments(transformed=False))
Shape.__init__(self, s._path)
elif isinstance(s, Shape):
self._segments.extend(s.segments(transformed=False))
elif isinstance(s, str):
self._segments = list()
self.parse(s)
elif isinstance(s, tuple):
# We have no guarantee of the validity of the source data
self._segments.extend(s)
self.validate_connections()
elif isinstance(s, list):
# We have no guarantee of the validity of the source data
self._segments.extend(s)
self.validate_connections()
elif isinstance(s, PathSegment):
self._segments.append(s)
if SVG_ATTR_DATA in self.values:
if not self.values.get('pathd_loaded', False):
self.parse(self.values[SVG_ATTR_DATA])
self.values['pathd_loaded'] = True
def __copy__(self):
path = Path(self)
segs = path._segments
for i in range(0, len(segs)):
segs[i] = copy(segs[i])
return path
def __getitem__(self, index):
return self._segments[index]
def _validate_subpath(self, index):
"""ensure the subpath containing this index is valid."""
for j in range(index, len(self._segments)):
close_search = self._segments[j]
if isinstance(close_search, Move):
return # Not a closed path, subpath is valid.
if isinstance(close_search, Close):
for k in range(index, -1, -1):
move_search = self._segments[k]
if isinstance(move_search, Move):
self._segments[j].end = Point(move_search.end)
return
self._segments[j].end = Point(self._segments[0].end)
return
def _validate_move(self, index):
"""ensure the next closed point from this index points to a valid location."""
for i in range(index + 1, len(self._segments)):
segment = self._segments[i]
if isinstance(segment, Move):
return # Not a closed path, the move is valid.
if isinstance(segment, Close):
segment.end = Point(self._segments[index].end)
return
def _validate_close(self, index):
"""ensure the close element at this position correctly links to the previous move"""
for i in range(index, -1, -1):
segment = self._segments[i]
if isinstance(segment, Move):
self._segments[index].end = Point(segment.end)
return
self._segments[index].end = Point(self._segments[0].end)
# If move is never found, just the end point of the first element.
def _validate_connection(self, index, prefer_second=False):
"""
Validates the connection at the index.
Connection 0 is the connection between getitem(0) and getitem(1)
prefer_second is for those cases where failing the connection requires replacing
a existing value. It will prefer the authority of right side, second value.
"""
if index < 0 or index + 1 >= len(self._segments):
return # This connection doesn't exist.
first = self._segments[index]
second = self._segments[index + 1]
if first.end is not None and second.start is None:
second.start = Point(first.end)
elif first.end is None and second.start is not None:
first.end = Point(second.start)
elif first.end != second.start:
# The two values exist but are not equal. One must replace the other.
if prefer_second:
first.end = Point(second.start)
else:
second.start = Point(first.end)
def __setitem__(self, index, new_element):
if isinstance(new_element, str):
new_element = Path(new_element)
if len(new_element) == 0:
return
new_element = new_element[0]
self._segments[index] = new_element
self._length = None
self._validate_connection(index - 1)
self._validate_connection(index)
if isinstance(new_element, Move):
self._validate_move(index)
if isinstance(new_element, Close):
self._validate_close(index)
def __delitem__(self, index):
original_element = self._segments[index]
del self._segments[index]
self._length = None
self._validate_connection(index - 1)
if isinstance(original_element, (Close, Move)):
self._validate_subpath(index)
def __iadd__(self, other):
if isinstance(other, str):
self.parse(other)
elif isinstance(other, (Path, Subpath)):
self.extend(map(copy, list(other)))
elif isinstance(other, Shape):
self.parse(other.d())
elif isinstance(other, PathSegment):
self.append(other)
else:
return NotImplemented
return self
def __add__(self, other):
n = copy(self)
n += other
return n
def __radd__(self, other):
if isinstance(other, str):
path = Path(other)
path.extend(map(copy, self._segments))
return path
elif isinstance(other, PathSegment):
path = copy(self)
path.insert(0, other)
return path
else:
return NotImplemented
def __len__(self):
return len(self._segments)
def __str__(self):
return self.d()
def __repr__(self):
values = []
if len(self) > 0:
values.append(', '.join(repr(x) for x in self._segments))
self._repr_shape(values)
params = ", ".join(values)
name = self._name()
return "%s(%s)" % (name, params)
def __eq__(self, other):
if isinstance(other, str):
return self.__eq__(Path(other))
if not isinstance(other, Path):
return NotImplemented
if len(self) != len(other):
return False
p = abs(self)
q = abs(other)
for s, o in zip(q._segments, p._segments):
if not s == o:
return False
return True
def __ne__(self, other):
if not isinstance(other, (Path, str)):
return NotImplemented
return not self == other
def parse(self, pathdef):
"""Parses the SVG path."""
tokens = SVGPathTokens()
tokens.svg_parse(self, pathdef)
def validate_connections(self):
"""
Force validate all connections.
This will scan path connections and link any adjacent elements together by replacing any None points or causing
the start position of the next element to equal the end position of the previous. This should only be needed
when combining paths and elements together. Close elements are always connected to the last Move element or to
the end position of the first element in the list. The start element of the first segment may or may not be
None.
"""
zpoint = None
last_segment = None
for segment in self._segments:
if zpoint is None or isinstance(segment, Move):
zpoint = segment.end
if last_segment is not None:
if segment.start is None and last_segment.end is not None:
segment.start = Point(last_segment.end)
elif last_segment.end is None and segment.start is not None:
last_segment.end = Point(segment.start)
elif last_segment.end != segment.start:
segment.start = Point(last_segment.end)
if isinstance(segment, Close) and zpoint is not None and segment.end != zpoint:
segment.end = Point(zpoint)
last_segment = segment
@property
def first_point(self):
"""First point along the Path. This is the start point of the first segment unless it starts
with a Move command with a None start in which case first point is that Move's destination."""
if len(self._segments) == 0:
return None
if self._segments[0].start is not None:
return Point(self._segments[0].start)
return Point(self._segments[0].end)
@property
def current_point(self):
if len(self._segments) == 0:
return None
return Point(self._segments[-1].end)
@property
def z_point(self):
"""
Z is the destination of the last Move. It can mean, but doesn't necessarily mean the first_point in the path.
This behavior of Z is defined in svg spec:
http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
"""
end_pos = None
for segment in reversed(self._segments):
if isinstance(segment, Move):
end_pos = segment.end
break
if end_pos is None:
try:
end_pos = self._segments[0].end
except IndexError:
pass # entire path is "z".
return end_pos
@property
def smooth_point(self):
"""Returns the smoothing control point for the smooth commands.
With regards to the SVG standard if the last command was a curve the smooth
control point is the reflection of the previous control point.
If the last command was not a curve, the smooth_point is coincident with the current.
https://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands
"""
if len(self._segments) == 0:
return None
start_pos = self.current_point
last_segment = self._segments[-1]
if isinstance(last_segment, QuadraticBezier):
previous_control = last_segment.control
return previous_control.reflected_across(start_pos)
elif isinstance(last_segment, CubicBezier):
previous_control = last_segment.control2
return previous_control.reflected_across(start_pos)
return start_pos
def start(self):
pass
def end(self):
pass
def move(self, *points):
end_pos = points[0]
start_pos = self.current_point
self.append(Move(start_pos, end_pos))
if len(points) > 1:
self.line(*points[1:])
def line(self, *points):
start_pos = self.current_point
end_pos = points[0]
if end_pos == 'z':
self.append(Line(start_pos, self.z_point))
self.closed()
return
self.append(Line(start_pos, end_pos))
if len(points) > 1:
self.line(*points[1:])
def absolute_v(self, *y_points):
y_pos = y_points[0]
start_pos = self.current_point
self.append(Line(start_pos, Point(start_pos[0], y_pos)))
if len(y_points) > 1:
self.absolute_v(*y_points[1:])
def relative_v(self, *dys):
dy = dys[0]
start_pos = self.current_point
self.append(Line(start_pos, Point(start_pos[0], start_pos[1] + dy)))
if len(dys) > 1:
self.relative_v(*dys[1:])
def absolute_h(self, *x_points):
x_pos = x_points[0]
start_pos = self.current_point
self.append(Line(start_pos, Point(x_pos, start_pos[1])))
if len(x_points) > 1:
self.absolute_h(*x_points[1:])
def relative_h(self, *dxs):
dx = dxs[0]
start_pos = self.current_point
self.append(Line(start_pos, Point(start_pos[0] + dx, start_pos[1])))
if len(dxs) > 1:
self.relative_h(*dxs[1:])
def smooth_quad(self, *points):
"""Smooth curve. First control point is the "reflection" of
the second control point in the previous path."""
start_pos = self.current_point
control1 = self.smooth_point
end_pos = points[0]
if end_pos == 'z':
self.append(QuadraticBezier(start_pos, control1, self.z_point))
self.closed()
return
self.append(QuadraticBezier(start_pos, control1, end_pos))
if len(points) > 1:
self.smooth_quad(*points[1:])
def quad(self, *points):
start_pos = self.current_point
control = points[0]
if control == 'z':
self.append(QuadraticBezier(start_pos, self.z_point, self.z_point))
self.closed()
return
end_pos = points[1]
if end_pos == 'z':
self.append(QuadraticBezier(start_pos, control, self.z_point))
self.closed()
return
self.append(QuadraticBezier(start_pos, control, end_pos))
if len(points) > 2:
self.quad(*points[2:])
def smooth_cubic(self, *points):
"""Smooth curve. First control point is the "reflection" of
the second control point in the previous path."""
start_pos = self.current_point
control1 = self.smooth_point
control2 = points[0]
if control2 == 'z':
self.append(CubicBezier(start_pos, control1, self.z_point, self.z_point))
self.closed()
return
end_pos = points[1]
if end_pos == 'z':
self.append(CubicBezier(start_pos, control1, control2, self.z_point))
self.closed()
return
self.append(CubicBezier(start_pos, control1, control2, end_pos))
if len(points) > 2:
self.smooth_cubic(*points[2:])
def cubic(self, *points):
start_pos = self.current_point
control1 = points[0]
if control1 == 'z':
self.append(CubicBezier(start_pos, self.z_point, self.z_point, self.z_point))
self.closed()
return
control2 = points[1]
if control2 == 'z':
self.append(CubicBezier(start_pos, control1, self.z_point, self.z_point))
self.closed()
return
end_pos = points[2]
if end_pos == 'z':
self.append(CubicBezier(start_pos, control1, control2, self.z_point))
self.closed()
return
self.append(CubicBezier(start_pos, control1, control2, end_pos))
if len(points) > 3:
self.cubic(*points[3:])
def arc(self, *arc_args):
start_pos = self.current_point
rx = arc_args[0]
ry = arc_args[1]
rotation = arc_args[2]
arc = arc_args[3]
sweep = arc_args[4]
end_pos = arc_args[5]
if end_pos == 'z':
self.append(Arc(start_pos, rx, ry, rotation, arc, sweep, self.z_point))
self.closed()
return
self.append(Arc(start_pos, rx, ry, rotation, arc, sweep, end_pos))
if len(arc_args) > 6:
self.arc(*arc_args[6:])
def closed(self):
start_pos = self.current_point
end_pos = self.z_point
self.append(Close(start_pos, end_pos))
def _calc_lengths(self, error=ERROR, min_depth=MIN_DEPTH):
if self._length is not None:
return
lengths = [each.length(error=error, min_depth=min_depth) for each in self._segments]
self._length = sum(lengths)
if self._length == 0:
self._lengths = lengths
else:
self._lengths = [each / self._length for each in lengths]
def point(self, position, error=ERROR):
if len(self._segments) == 0:
return None
# Shortcuts
if position <= 0.0:
return self._segments[0].point(position)
if position >= 1.0:
return self._segments[-1].point(position)
self._calc_lengths(error=error)
if self._length == 0:
i = int(round(position * (len(self._segments) - 1)))
return self._segments[i].point(0.0)
# Find which segment the point we search for is located on:
segment_start = 0
segment_pos = 0
segment = self._segments[0]
for index, segment in enumerate(self._segments):
segment_end = segment_start + self._lengths[index]
if segment_end >= position:
# This is the segment! How far in on the segment is the point?
segment_pos = (position - segment_start) / (segment_end - segment_start)
break
segment_start = segment_end
return segment.point(segment_pos)
def length(self, error=ERROR, min_depth=MIN_DEPTH):
self._calc_lengths(error, min_depth)
return self._length
def append(self, value):
if isinstance(value, str):
value = Path(value)
if len(value) == 0:
return
if len(value) > 1:
self.extend(value)
return
value = value[0]
self._length = None
index = len(self._segments) - 1
self._segments.append(value)
self._validate_connection(index)
if isinstance(value, Close):
self._validate_close(index + 1)
def insert(self, index, value):
if isinstance(value, str):
value = Path(value)
if len(value) == 0:
return
value = value[0]
self._length = None
self._segments.insert(index, value)
self._validate_connection(index - 1)
self._validate_connection(index)
if isinstance(value, Move):
self._validate_move(index)
if isinstance(value, Close):
self._validate_close(index)
def extend(self, iterable):
if isinstance(iterable, str):
iterable = Path(iterable)
self._length = None
index = len(self._segments) - 1
self._segments.extend(iterable)
self._validate_connection(index)
self._validate_subpath(index)
def reverse(self):
if len(self._segments) == 0:
return
prepoint = self._segments[0].start
self._segments[0].start = None
p = Path()
subpaths = list(self.as_subpaths())
for subpath in subpaths:
subpath.reverse()
for subpath in reversed(subpaths):
p += subpath
self._segments = p._segments
self._segments[0].start = prepoint
return self
def subpath(self, index):
subpaths = list(self.as_subpaths())
return subpaths[index]
def count_subpaths(self):
subpaths = list(self.as_subpaths())
return len(subpaths)
def as_subpaths(self):
last = 0
for current, seg in enumerate(self):
if current != last and isinstance(seg, Move):
yield Subpath(self, last, current - 1)
last = current
yield Subpath(self, last, len(self) - 1)
def as_points(self):
"""Returns the list of defining points within path"""
for seg in self:
for p in seg:
if not isinstance(p, Point):
yield Point(p)
else:
yield p
def reify(self):
"""
Realizes the transform to the shape properties.
Path objects reify perfectly.
"""
Transformable.reify(self)
if isinstance(self.transform, Matrix):
for e in self._segments:
e *= self.transform
self.transform.reset()
return self
@staticmethod
def svg_d(segments, relative=False, transformed=True):
if len(segments) == 0:
return ''
if relative:
return Path.svg_d_relative(segments, transformed=transformed)
else:
return Path.svg_d_absolute(segments, transformed=transformed)
@staticmethod
def svg_d_relative(segments, transformed=True):
parts = []
previous_segment = None
p = Point(0)
for segment in segments:
if isinstance(segment, (Move, Line, Arc, Close)):
parts.append(segment.d(p))
elif isinstance(segment, (CubicBezier, QuadraticBezier)):
parts.append(segment.d(p, smooth=segment.is_smooth_from(previous_segment)))
previous_segment = segment
p = previous_segment.end
return ' '.join(parts)
@staticmethod
def svg_d_absolute(segments, transformed=True):
parts = []
previous_segment = None
for segment in segments:
if isinstance(segment, (Move, Line, Arc, Close)):
parts.append(segment.d())
elif isinstance(segment, (CubicBezier, QuadraticBezier)):
parts.append(segment.d(smooth=segment.is_smooth_from(previous_segment)))
previous_segment = segment
return ' '.join(parts)
def d(self, relative=False, transformed=True):
if transformed:
p = self.__copy__()
p.reify()
return Path.svg_d(p._segments, relative)
else:
return Path.svg_d(self._segments, relative)
def segments(self, transformed=True):
if transformed:
return [s * self.transform for s in self._segments]
return self._segments
class Rect(Shape):
"""
SVG Rect shapes are defined in SVG2 10.2
https://www.w3.org/TR/SVG2/shapes.html#RectElement
These have geometric properties x, y, width, height, rx, ry
Geometric properties can be Length values.
Rect(x, y, width, height)
Rect(x, y, width, height, rx, ry)
Rect(x, y, width, height, rx, ry, matrix)
Rect(x, y, width, height, rx, ry, matrix, stroke, fill)
Rect(dict): dictionary values read from svg.
"""
def __init__(self, *args, **kwargs):
self.x = None
self.y = None
self.width = None
self.height = None
self.rx = None
self.ry = None
Shape.__init__(self, *args, **kwargs)
self._validate_rect()
def property_by_object(self, s):
Shape.property_by_object(self, s)
self.x = s.x
self.y = s.y
self.width = s.width
self.height = s.height
self.rx = s.rx
self.ry = s.ry
self._validate_rect()
def property_by_values(self, values):
Shape.property_by_values(self, values)
self.x = Length(values.get(SVG_ATTR_X, 0)).value()
self.y = Length(values.get(SVG_ATTR_Y, 0)).value()
self.width = Length(values.get(SVG_ATTR_WIDTH, 1)).value()
self.height = Length(values.get(SVG_ATTR_HEIGHT, 1)).value()
self.rx = Length(values.get(SVG_ATTR_RADIUS_X, None)).value()
self.ry = Length(values.get(SVG_ATTR_RADIUS_Y, None)).value()
def property_by_args(self, *args):
arg_length = len(args)
if arg_length >= 1:
self.x = Length(args[0]).value()
if arg_length >= 2:
self.y = Length(args[1]).value()
if arg_length >= 3:
self.width = Length(args[2]).value()
if arg_length >= 4:
self.height = Length(args[3]).value()
if arg_length >= 5:
self.rx = Length(args[4]).value()
if arg_length >= 6:
self.ry = Length(args[5]).value()
if arg_length >= 7:
self._init_shape(*args[6:])
def _validate_rect(self):
"""None is 'auto' for values."""
rx = self.rx
ry = self.ry
if rx is None and ry is None:
rx = ry = 0
if rx is not None and ry is None:
rx = Length(rx).value(relative_length=self.width)
ry = rx
elif ry is not None and rx is None:
ry = Length(ry).value(relative_length=self.height)
rx = ry
elif rx is not None and ry is not None:
rx = Length(rx).value(relative_length=self.width)
ry = Length(ry).value(relative_length=self.height)
if rx == 0 or ry == 0:
rx = ry = 0
else:
rx = min(rx, self.width / 2.0)
ry = min(ry, self.height / 2.0)
self.rx = rx
self.ry = ry
def __repr__(self):
values = []
if self.x != 0:
values.append('x=%s' % Length.str(self.x))
if self.y != 0:
values.append('y=%s' % Length.str(self.y))
if self.width != 0:
values.append('width=%s' % Length.str(self.width))
if self.height != 0:
values.append('height=%s' % Length.str(self.height))
if self.rx != 0:
values.append('rx=%s' % Length.str(self.rx))
if self.ry != 0:
values.append('ry=%s' % Length.str(self.ry))
self._repr_shape(values)
params = ", ".join(values)
return "Rect(%s)" % params
def __copy__(self):
return Rect(self)
@property
def implicit_position(self):
if not self.apply:
return Point(self.x, self.y)
point = Point(self.x, self.y)
point *= self.transform
return point
@property
def implicit_x(self):
if not self.apply:
return self.x
return self.implicit_position[0]
@property
def implicit_y(self):
if not self.apply:
return self.y
return self.implicit_position[1]
@property
def implicit_width(self):
if not self.apply:
return self.width
p = Point(self.width, 0)
p *= self.transform
origin = Point(0, 0)
origin *= self.transform
return origin.distance_to(p)
@property
def implicit_height(self):
if not self.apply:
return self.height
p = Point(0, self.height)
p *= self.transform
origin = Point(0, 0)
origin *= self.transform
return origin.distance_to(p)
@property
def implicit_rx(self):
if not self.apply:
return self.rx
p = Point(self.rx, 0)
p *= self.transform
origin = Point(0, 0)
origin *= self.transform
return origin.distance_to(p)
@property
def implicit_ry(self):
if not self.apply:
return self.ry
p = Point(0, self.ry)
p *= self.transform
origin = Point(0, 0)
origin *= self.transform
return origin.distance_to(p)
def segments(self, transformed=True):
"""
Rect decomposition is given in SVG 2.0 10.2
Rect:
* perform an absolute moveto operation to location (x,y);
* perform an absolute horizontal lineto with parameter x+width;
* perform an absolute vertical lineto parameter y+height;
* perform an absolute horizontal lineto parameter x;
* ( close the path)
Rounded Rect:
rx and ry are used as the equivalent parameters to the elliptical arc command,
the x-axis-rotation and large-arc-flag are set to zero, the sweep-flag is set to one
* perform an absolute moveto operation to location (x+rx,y);
* perform an absolute horizontal lineto with parameter x+width-rx;
* perform an absolute elliptical arc operation to coordinate (x+width,y+ry)
* perform an absolute vertical lineto parameter y+height-ry;
* perform an absolute elliptical arc operation to coordinate (x+width-rx,y+height)
* perform an absolute horizontal lineto parameter x+rx;
* perform an absolute elliptical arc operation to coordinate (x,y+height-ry)
* perform an absolute vertical lineto parameter y+ry
* perform an absolute elliptical arc operation with a segment-completing close path operation
:param transformed: provide the reified version.
:return: path_d of shape.
"""
x = self.x
y = self.y
width = self.width
height = self.height
if width == 0 or height == 0:
return '' # a computed value of zero for either dimension disables rendering.
rx = self.rx
ry = self.ry
if rx == ry == 0:
segments = (Move(None, (x, y)),
Line((x, y), (x + width, y)),
Line((x + width, y), (x + width, y + height)),
Line((x + width, y + height), (x, y + height)),
Close((x, y + height), (x, y)))
else:
segments = (Move(None, (x + rx, y)),
Line((x + rx, y), (x + width - rx, y)),
Arc((x + width - rx, y), (x + width, y + ry), rx=rx, ry=ry),
Line((x + width, y + ry), (x + width, y + height - ry)),
Arc((x + width, y + height - ry), (x + width - rx, y + height), rx=rx, ry=ry),
Line((x + width - rx, y + height), (x + rx, y + height)),
Arc((x + rx, y + height), (x, y + height - ry), rx=rx, ry=ry),
Line((x, y + height - ry), (x, y + ry)),
Arc((x, y + ry), (x + rx, y), rx=rx, ry=ry),
Close((x + rx, y), (x + rx, y)))
if not transformed or self.transform.is_identity():
return segments
else:
return [s * self.transform for s in segments]
def reify(self):
"""
Realizes the transform to the shape properties.
If the realized shape can be properly represented as a rectangle with an identity matrix
it will be, otherwise the properties will approximate the implied values.
Skewed and Rotated rectangles cannot be reified.
"""
Transformable.reify(self)
scale_x = self.transform.value_scale_x()
scale_y = self.transform.value_scale_y()
translate_x = self.transform.value_trans_x()
translate_y = self.transform.value_trans_y()
if self.transform.value_skew_x() == 0 and self.transform.value_skew_y() == 0 \
and scale_x != 0 and scale_y != 0:
self.x *= scale_x
self.y *= scale_y
self.x += translate_x
self.y += translate_y
self.transform *= Matrix.translate(-translate_x, -translate_y)
self.rx = scale_x * self.rx
self.ry = scale_y * self.ry
self.width = scale_x * self.width
self.height = scale_y * self.height
self.transform *= Matrix.scale(1.0 / scale_x, 1.0 / scale_y)
return self
def render(self, width=None, height=None, relative_length=None, **kwargs):
if width is None and relative_length is not None:
width = relative_length
if height is None and relative_length is not None:
height = relative_length
Shape.render(self, width=width, height=height, relative_length=relative_length, **kwargs)
if isinstance(self.x, Length):
self.x = self.x.value(relative_length=width, **kwargs)
if isinstance(self.y, Length):
self.y = self.y.value(relative_length=height, **kwargs)
if isinstance(self.width, Length):
self.width = self.width.value(relative_length=width, **kwargs)
if isinstance(self.height, Length):
self.height = self.height.value(relative_length=height, **kwargs)
if isinstance(self.rx, Length):
self.rx = self.rx.value(relative_length=width, **kwargs)
if isinstance(self.ry, Length):
self.ry = self.ry.value(relative_length=height, **kwargs)
return self
class _RoundShape(Shape):
def __init__(self, *args, **kwargs):
self.cx = None
self.cy = None
self.rx = None
self.ry = None
Shape.__init__(self, *args, **kwargs)
def property_by_object(self, s):
Shape.property_by_object(self, s)
self.cx = s.cx
self.cy = s.cy
self.rx = s.rx
self.ry = s.ry
def property_by_values(self, values):
Shape.property_by_values(self, values)
self.cx = Length(values.get(SVG_ATTR_CENTER_X)).value()
self.cy = Length(values.get(SVG_ATTR_CENTER_Y)).value()
self.rx = Length(values.get(SVG_ATTR_RADIUS_X)).value()
self.ry = Length(values.get(SVG_ATTR_RADIUS_Y)).value()
r = Length(values.get(SVG_ATTR_RADIUS, None)).value()
if r is not None:
self.rx = r
self.ry = r
else:
if self.rx is None:
self.rx = 1
if self.ry is None:
self.ry = 1
center = values.get('center', None)
if center is not None:
self.cx, self.cy = Point(center)
if self.cx is None:
self.cx = 0
if self.cy is None:
self.cy = 0
def property_by_args(self, *args):
arg_length = len(args)
if arg_length >= 1:
self.cx = Length(args[0]).value()
if arg_length >= 2:
self.cy = Length(args[1]).value()
if arg_length >= 3:
self.rx = Length(args[2]).value()
if arg_length >= 4:
self.ry = Length(args[3]).value()
else:
self.ry = self.rx
if arg_length >= 5:
self._init_shape(*args[4:])
def __repr__(self):
values = []
if self.cx is not None:
values.append('cx=%s' % Length.str(self.cx))
if self.cy is not None:
values.append('cy=%s' % Length.str(self.cy))
if self.rx == self.ry or self.ry is None:
values.append('r=%s' % Length.str(self.rx))
else:
values.append('rx=%s' % Length.str(self.rx))
values.append('ry=%s' % Length.str(self.ry))
self._repr_shape(values)
params = ", ".join(values)
name = self._name()
return "%s(%s)" % (name, params)
@property
def implicit_rx(self):
if not self.apply:
return self.rx
prx = Point(self.rx, 0)
prx *= self.transform
origin = Point(0, 0)
origin *= self.transform
return origin.distance_to(prx)
@property
def implicit_ry(self):
if not self.apply:
return self.ry
pry = Point(0, self.ry)
pry *= self.transform
origin = Point(0, 0)
origin *= self.transform
return origin.distance_to(pry)
implicit_r = implicit_rx
@property
def implicit_center(self):
center = Point(self.cx, self.cy)
if not self.apply:
return center
center *= self.transform
return center
def segments(self, transformed=True):
"""
SVG path decomposition is given in SVG 2.0 10.3, 10.4.
A move-to command to the point cx+rx,cy;
arc to cx,cy+ry;
arc to cx-rx,cy;
arc to cx,cy-ry;
arc with a segment-completing close path operation.
Converts the parameters from an ellipse or a circle to a string for a
Path object d-attribute"""
original = self.apply
self.apply = transformed
path = Path()
steps = 4
step_size = tau / steps
t_start = 0
t_end = step_size
path.move((self.point_at_t(0)))
for i in range(steps):
path += Arc(
self.point_at_t(t_start),
self.point_at_t(t_end),
self.implicit_center,
rx=self.implicit_rx, ry=self.implicit_ry, rotation=self.rotation, sweep=step_size)
t_start = t_end
t_end += step_size
path.closed()
self.apply = original
return path.segments(transformed)
def reify(self):
"""
Realizes the transform to the shape properties.
Skewed and Rotated roundshapes cannot be reified.
"""
Transformable.reify(self)
scale_x = self.transform.value_scale_x()
scale_y = self.transform.value_scale_y()
translate_x = self.transform.value_trans_x()
translate_y = self.transform.value_trans_y()
if self.transform.value_skew_x() == 0 and self.transform.value_skew_y() == 0 \
and scale_x != 0 and scale_y != 0:
self.cx *= scale_x
self.cy *= scale_y
self.cx += translate_x
self.cy += translate_y
self.transform *= Matrix.translate(-translate_x, -translate_y)
self.rx = scale_x * self.rx
self.ry = scale_y * self.ry
self.transform *= Matrix.scale(1.0 / scale_x, 1.0 / scale_y)
return self
def render(self, width=None, height=None, relative_length=None, **kwargs):
if width is None and relative_length is not None:
width = relative_length
if height is None and relative_length is not None:
height = relative_length
Shape.render(self, width=width, height=height, relative_length=relative_length, **kwargs)
if isinstance(self.cx, Length):
self.cx = self.cx.value(relative_length=width, **kwargs)
if isinstance(self.cy, Length):
self.cy = self.cy.value(relative_length=height, **kwargs)
if isinstance(self.rx, Length):
self.rx = self.rx.value(relative_length=width, **kwargs)
if isinstance(self.ry, Length):
self.ry = self.ry.value(relative_length=height, **kwargs)
return self
def unit_matrix(self):
"""
return the unit matrix which could would transform the unit circle into this ellipse.
One of the valid parameterizations for ellipses is that they are all affine transforms of the unit circle.
This provides exactly such a matrix.
:return: matrix
"""
m = Matrix()
m.post_scale(self.implicit_rx, self.implicit_ry)
m.post_rotate(self.rotation)
center = self.implicit_center
m.post_translate(center[0], center[1])
return m
def arc_t(self, t0, t1):
"""
return the arc found between the given values of t on the ellipse.
:param t0: t start
:param t1: t end
:return: arc
"""
return Arc(self.point_at_t(t0),
self.point_at_t(t1),
self.implicit_center,
rx=self.implicit_rx, ry=self.implicit_ry, rotation=self.rotation, sweep=t1 - t0)
def arc_angle(self, a0, a1):
"""
return the arc found between the given angles on the ellipse.
:param a0: start angle
:param a1: end angle
:return: arc
"""
return Arc(self.point_at_angle(a0),
self.point_at_angle(a1),
self.implicit_center,
rx=self.implicit_rx, ry=self.implicit_ry,
rotation=self.rotation, ccw=a0 > a1)
def point_at_angle(self, angle):
"""
find the point on the ellipse from the center at the given angle.
Note: For non-circular arcs this is different than point(t).
:param angle: angle from center to find point
:return: point found
"""
a = self.implicit_rx
b = self.implicit_ry
if a == b:
return self.point_at_t(angle)
angle -= self.rotation
t = atan2(a * tan(angle), b)
tau_1_4 = tau / 4.0
tau_3_4 = 3 * tau_1_4
if tau_3_4 >= abs(angle) % tau > tau_1_4:
t += tau / 2
return self.point_at_t(t)
def angle_at_point(self, p):
"""
find the angle to the point.
:param p: point
:return: angle to given point.
"""
if self.apply and not self.transform.is_identity():
return self.implicit_center.angle_to(p)
else:
center = Point(self.cx, self.cy)
return center.angle_to(p)
def t_at_point(self, p):
"""
find the t parameter to at the point.
:param p: point
:return: t parameter to the given point.
"""
angle = self.angle_at_point(p)
angle -= self.rotation
a = self.implicit_rx
b = self.implicit_ry
t = atan2(a * tan(angle), b)
tau_1_4 = tau / 4.0
tau_3_4 = 3 * tau_1_4
if tau_3_4 >= abs(angle) % tau > tau_1_4:
t += tau / 2
return t
def point_at_t(self, t):
"""
find the point that corresponds to given value t.
Where t=0 is the first point and t=tau is the final point.
In the case of a circle: t = angle.
:param t:
:return:
"""
rotation = self.rotation
a = self.implicit_rx
b = self.implicit_ry
center = self.implicit_center
cx = center[0]
cy = center[1]
cosTheta = cos(rotation)
sinTheta = sin(rotation)
cosT = cos(t)
sinT = sin(t)
px = cx + a * cosT * cosTheta - b * sinT * sinTheta
py = cy + a * cosT * sinTheta + b * sinT * cosTheta
return Point(px, py)
def point(self, position):
"""
find the point that corresponds to given value [0,1].
Where t=0 is the first point and t=1 is the final point.
:param position:
:return: point at t
"""
return self.point_at_t(tau * position)
class Ellipse(_RoundShape):
"""
SVG Ellipse shapes are defined in SVG2 10.4
https://www.w3.org/TR/SVG2/shapes.html#EllipseElement
These have geometric properties cx, cy, rx, ry
"""
def __init__(self, *args, **kwargs):
_RoundShape.__init__(self, *args, **kwargs)
def __copy__(self):
return Ellipse(self)
def _name(self):
return self.__class__.__name__
class Circle(_RoundShape):
"""
SVG Circle shapes are defined in SVG2 10.3
https://www.w3.org/TR/SVG2/shapes.html#CircleElement
These have geometric properties cx, cy, r
"""
def __init__(self, *args, **kwargs):
_RoundShape.__init__(self, *args, **kwargs)
def __copy__(self):
return Circle(self)
def _name(self):
return self.__class__.__name__
class SimpleLine(Shape):
"""
SVG Line shapes are defined in SVG2 10.5
https://www.w3.org/TR/SVG2/shapes.html#LineElement
These have geometric properties x1, y1, x2, y2
These are called Line in SVG but that name is already used for Line(PathSegment)
"""
def __init__(self, *args, **kwargs):
self.x1 = None
self.y1 = None
self.x2 = None
self.y2 = None
Shape.__init__(self, *args, **kwargs)
def property_by_object(self, s):
Shape.property_by_object(self, s)
self.x1 = s.x1
self.y1 = s.y1
self.x2 = s.x2
self.y2 = s.y2
def property_by_values(self, values):
Shape.property_by_values(self, values)
self.x1 = Length(values.get(SVG_ATTR_X1, 0)).value()
self.y1 = Length(values.get(SVG_ATTR_Y1, 0)).value()
self.x2 = Length(values.get(SVG_ATTR_X2, 0)).value()
self.y2 = Length(values.get(SVG_ATTR_Y2, 0)).value()
def property_by_args(self, *args):
arg_length = len(args)
if arg_length >= 1:
self.x1 = Length(args[0]).value()
if arg_length >= 2:
self.y1 = Length(args[1]).value()
if arg_length >= 3:
self.x2 = Length(args[2]).value()
if arg_length >= 4:
self.y2 = Length(args[3]).value()
self._init_shape(*args[4:])
def __repr__(self):
values = []
if self.x1 is not None:
values.append('x1=%s' % repr(self.x1))
if self.y1 is not None:
values.append('y1=%s' % repr(self.y1))
if self.x2 is not None:
values.append('x2=%s' % repr(self.x2))
if self.y2 is not None:
values.append('y2=%s' % repr(self.y2))
self._repr_shape(values)
params = ", ".join(values)
return "SimpleLine(%s)" % params
def __copy__(self):
return SimpleLine(self)
@property
def implicit_x1(self):
point = Point(self.x1, self.y1)
point *= self.transform
return point[0]
@property
def implicit_y1(self):
point = Point(self.x1, self.y1)
point *= self.transform
return point[1]
@property
def implicit_x2(self):
point = Point(self.x2, self.y2)
point *= self.transform
return point[0]
@property
def implicit_y2(self):
point = Point(self.x2, self.y2)
point *= self.transform
return point[1]
def segments(self, transformed=True):
"""
SVG path decomposition is given in SVG 2.0 10.5.
perform an absolute moveto operation to absolute location (x1,y1)
perform an absolute lineto operation to absolute location (x2,y2)
:returns Path_d path for line.
"""
start = Point(self.x1, self.y1)
end = Point(self.x2, self.y2)
if transformed:
start *= self.transform
end *= self.transform
return (Move(None, start), Line(start, end))
def reify(self):
"""
Realizes the transform to the shape properties.
SimpleLines are perfectly reified.
"""
Transformable.reify(self)
matrix = self.transform
p = Point(self.x1, self.y1)
p *= matrix
self.x1 = p[0]
self.y1 = p[1]
p = Point(self.x2, self.y2)
p *= matrix
self.x2 = p[0]
self.y2 = p[1]
matrix.reset()
return self
def render(self, width=None, height=None, relative_length=None, **kwargs):
if width is None and relative_length is not None:
width = relative_length
if height is None and relative_length is not None:
height = relative_length
Shape.render(self, width=width, height=height, relative_length=relative_length, **kwargs)
if isinstance(self.x1, Length):
self.x1 = self.x1.value(relative_length=width, **kwargs)
if isinstance(self.y1, Length):
self.y1 = self.y1.value(relative_length=height, **kwargs)
if isinstance(self.x2, Length):
self.x2 = self.x2.value(relative_length=width, **kwargs)
if isinstance(self.y2, Length):
self.y2 = self.y2.value(relative_length=height, **kwargs)
return self
class _Polyshape(Shape):
"""Base form of Polygon and Polyline since the objects are nearly the same."""
def __init__(self, *args, **kwargs):
self.points = list()
Shape.__init__(self, *args, **kwargs)
def property_by_object(self, s):
Shape.property_by_object(self, s)
self._init_points(s.points)
def property_by_values(self, values):
Shape.property_by_values(self, values)
self._init_points(values)
def property_by_args(self, *args):
self._init_points(args)
def _init_points(self, points):
if len(self.points) != 0:
return
if points is None:
self.points = list()
return
if isinstance(points, dict):
if SVG_ATTR_POINTS in points:
points = points[SVG_ATTR_POINTS]
else:
self.points = list()
return
try:
if len(points) == 1:
points = points[0]
except TypeError:
pass
if isinstance(points, str):
findall = REGEX_COORD_PAIR.findall(points)
self.points = [Point(float(j), float(k)) for j, k in findall]
elif isinstance(points, (list, tuple)):
if len(points) == 0:
self.points = list()
else:
first_point = points[0]
if isinstance(first_point, (float, int)):
self.points = list(map(Point, zip(*[iter(points)] * 2)))
elif isinstance(first_point, (list, tuple, complex, str, Point)):
self.points = list(map(Point, points))
else:
self.points = list()
def __repr__(self):
values = []
if self.points is not None:
s = ", ".join(map(str, self.points))
values.append('points=(%s)' % repr(s))
self._repr_shape(values)
params = ", ".join(values)
name = self._name()
return "%s(%s)" % (name, params)
def __len__(self):
return len(self.points)
def __getitem__(self, item):
return self.points[item]
def segments(self, transformed=True):
"""
Polyline and Polygon decomposition is given in SVG2. 10.6 and 10.7
* perform an absolute moveto operation to the first coordinate pair in the list of points
* for each subsequent coordinate pair, perform an absolute lineto operation to that coordinate pair.
* (Polygon-only) perform a closepath command
Note: For a polygon/polyline made from n points, the resulting path will
be composed of n lines (even if some of these lines have length zero).
"""
if self.transform.is_identity() or not transformed:
points = self.points
else:
points = list(map(self.transform.point_in_matrix_space, self.points))
if len(points) == 0:
return []
segments = [Move(None, points[0])]
last = points[0]
for i in range(1, len(points)):
current = points[i]
segments.append(Line(last, current))
last = current
if isinstance(self, Polygon):
segments.append(Close(last, points[0]))
return segments
def reify(self):
"""
Realizes the transform to the shape properties.
Polyshapes are perfectly reified.
"""
Transformable.reify(self)
matrix = self.transform
for p in self:
p *= matrix
matrix.reset()
return self
class Polyline(_Polyshape):
"""
SVG Polyline shapes are defined in SVG2 10.6
https://www.w3.org/TR/SVG2/shapes.html#PolylineElement
These have geometric properties points
"""
def __init__(self, *args, **kwargs):
_Polyshape.__init__(self, *args, **kwargs)
def __copy__(self):
return Polyline(self)
def _name(self):
return self.__class__.__name__
class Polygon(_Polyshape):
"""
SVG Polygon shapes are defined in SVG2 10.7
https://www.w3.org/TR/SVG2/shapes.html#PolygonElement
These have geometric properties points
"""
def __init__(self, *args, **kwargs):
_Polyshape.__init__(self, *args, **kwargs)
def __copy__(self):
return Polygon(self)
def _name(self):
return self.__class__.__name__
class Subpath:
"""
Subpath is a Path-backed window implementation. It does not store a list of segments but rather
stores a Path, start position, end position. When a function is called on a subpath, the result of
those events is performed on the backing Path. When the backing Path is modified the behavior is
undefined."""
def __init__(self, path, start, end):
self._path = path
self._start = start
self._end = end
def __copy__(self):
return Subpath(Path(self._path), self._start, self._end)
def __getitem__(self, index):
return self._path[self.index_to_path_index(index)]
def __setitem__(self, index, value):
self._path[self.index_to_path_index(index)] = value
def __delitem__(self, index):
del self._path[self.index_to_path_index(index)]
self._end -= 1
def __iadd__(self, other):
if isinstance(other, str):
p = Path(other)
self._path[self._end:self._end] = p
elif isinstance(other, Path):
p = copy(other)
self._path[self._end:self._end] = p
elif isinstance(other, PathSegment):
self._path.insert(self._end, other)
else:
return NotImplemented
return self
def __add__(self, other):
n = copy(self)
n += other
return n
def __radd__(self, other):
if isinstance(other, str):
path = Path(other)
path.extend(map(copy, self._path))
return path
elif isinstance(other, PathSegment):
path = Path(self)
path.insert(0, other)
return path
else:
return NotImplemented
def __imul__(self, other):
if isinstance(other, str):
other = Matrix(other)
if isinstance(other, Matrix):
for e in self:
e *= other
return self
def __mul__(self, other):
if isinstance(other, (Matrix, str)):
n = copy(self)
n *= other
return n
__rmul__ = __mul__
def __iter__(self):
class Iterator:
def __init__(self, subpath):
self.n = subpath._start - 1
self.subpath = subpath
def __next__(self):
self.n += 1
try:
if self.n > self.subpath._end:
raise StopIteration
return self.subpath._path[self.n]
except IndexError:
raise StopIteration
next = __next__
return Iterator(self)
def __len__(self):
return self._end - self._start + 1
def __str__(self):
return self.d()
def __repr__(self):
return 'Path(%s)' % (', '.join(repr(x) for x in self))
def __eq__(self, other):
if isinstance(other, str):
return self.__eq__(Path(other))
if not isinstance(other, (Path, Subpath)):
return NotImplemented
if len(self) != len(other):
return False
for s, o in zip(self, other):
if not s == o:
return False
return True
def __ne__(self, other):
if not isinstance(other, (Path, Subpath, str)):
return NotImplemented
return not self == other
def segments(self, transformed=True):
path = self._path
if transformed:
return [s * path.transform for s in path._segments[self._start:self._end + 1]]
return path._segments[self._start:self._end + 1]
def index_to_path_index(self, index):
if index < 0:
return self._end + index + 1
else:
return self._start + index
def bbox(self):
"""returns a bounding box for the input Path"""
segments = self._path._segments[self._start:self._end + 1]
bbs = [seg.bbox() for seg in segments if not isinstance(Close, Move)]
try:
xmins, ymins, xmaxs, ymaxs = list(zip(*bbs))
except ValueError:
return None # No bounding box items existed. So no bounding box.
xmin = min(xmins)
xmax = max(xmaxs)
ymin = min(ymins)
ymax = max(ymaxs)
return xmin, ymin, xmax, ymax
def d(self, relative=False):
segments = self._path._segments[self._start:self._end + 1]
return Path.svg_d(segments, relative)
def _reverse_segments(self, start, end):
"""Reverses segments between the given indexes in the subpath space."""
segments = self._path._segments # must avoid path validation.
s = self.index_to_path_index(start)
e = self.index_to_path_index(end)
while s <= e:
start_segment = segments[s]
end_segment = segments[e]
start_segment.reverse()
if start_segment is not end_segment:
end_segment.reverse()
segments[s] = end_segment
segments[e] = start_segment
s += 1
e -= 1
start = self.index_to_path_index(start)
end = self.index_to_path_index(end)
self._path._validate_connection(start - 1, prefer_second=True)
self._path._validate_connection(end)
def reverse(self):
size = len(self)
if size == 0:
return
start = 0
end = size - 1
if isinstance(self[-1], Close):
end -= 1
if isinstance(self[0], Move): # Move remains in place but references next element.
start += 1
self._reverse_segments(start, end)
if size > 1:
if isinstance(self[0], Move):
self[0].end = Point(self[1].start)
last = self[-1]
if isinstance(last, Close):
last.reverse()
if last.start != self[-2].end:
last.start = Point(self[-2].end)
if last.end != self[0].end:
last.end = Point(self[0].end)
return self
class SVGText(GraphicObject, Transformable):
"""
SVG Text are defined in SVG 2.0 Chapter 11
No methods are implemented to perform a text to path conversion.
However, if such a method exists the assumption is that the results will be
placed in the .path attribute, and functions like bbox() will check if such
a value exists.
"""
def __init__(self, *args, **kwargs):
if len(args) >= 1:
self.text = args[0]
else:
self.text = ''
self.width = 0
self.height = 0
self.x = 0
self.y = 0
self.dx = 0
self.dy = 0
self.anchor = 'start' # start, middle, end.
self.font_family = 'san-serif'
self.font_size = 16.0 # 16 point font 'normal'
self.font_weight = 400.0 # Thin=100, Normal=400, Bold=700
self.font_face = ''
self.path = None
Transformable.__init__(self, *args, **kwargs)
GraphicObject.__init__(self, *args, **kwargs)
def __str__(self):
parts = list()
parts.append("'%s'" % self.text)
parts.append('font_family=%s' % self.font_family)
parts.append('anchor=%s' % self.anchor)
parts.append('font_size=%d' % self.font_size)
parts.append('font_weight=%s' % str(self.font_weight))
return 'Text(%s)' % (', '.join(parts))
def __repr__(self):
parts = list()
parts.append('%s' % self.text)
parts.append('font_family=%s' % self.font_family)
parts.append('anchor=%s' % self.anchor)
parts.append('font_size=%d' % self.font_size)
parts.append('font_weight=%s' % str(self.font_weight))
return 'Text(%s)' % (', '.join(parts))
def property_by_object(self, s):
Transformable.property_by_object(self, s)
GraphicObject.property_by_object(self, s)
self.text = s.text
self.x = s.x
self.y = s.y
self.width = s.width
self.height = s.height
self.dx = s.dx
self.dy = s.dy
self.anchor = s.anchor
self.font_family = s.font_family
self.font_size = s.font_size
self.font_weight = s.font_weight
self.font_face = s.font_face
def parse_font(self, font):
"""
CSS Fonts 3 has a shorthand font property which serves to provide a single location to define:
font-style, font-variant, font-weight, font-stretch, font-size, line-height, and font-family
font-style: normal | italic | oblique
font-variant: normal | small-caps
font-weight: normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
font-stretch: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded
font-size: <absolute-size> | <relative-size> | <length-percentage>
line-height: '/' <line-height>
font-family: [ <family-name> | <generic-family> ] #
generic-family: serif, sans-serif, cursive, fantasy, and monospace
"""
# https://www.w3.org/TR/css-fonts-3/#font-prop
font_elements = list(*re.findall(REGEX_CSS_FONT, font))
font_style = font_elements[0]
font_variant = font_elements[1]
font_weight = font_elements[2]
font_stretch = font_elements[3]
font_size = font_elements[4]
line_height = font_elements[5]
font_face = font_elements[6]
font_family = font_elements[7]
if len(font_weight) > 0:
self.font_weight = self.parse_font_weight(font_weight)
if len(font_size) > 0:
self.font_size = Length(font_size).value()
if len(font_face) > 0:
if font_face.endswith(','):
font_face = font_face[:-1]
self.font_face = font_face
if len(font_family) > 0:
self.font_family = font_family
def parse_font_weight(self, weight):
if weight == 'bold':
return 700
if weight == 'normal':
return 400
try:
return int(weight)
except KeyError:
return 400
def property_by_values(self, values):
Transformable.property_by_values(self, values)
GraphicObject.property_by_values(self, values)
self.anchor = values.get(SVG_ATTR_TEXT_ANCHOR, self.anchor)
self.font_face = values.get(SVG_ATTR_FONT_FACE)
self.font_family = values.get(SVG_ATTR_FONT_FAMILY, self.font_family)
self.font_size = Length(values.get(SVG_ATTR_FONT_SIZE, self.font_size)).value()
self.font_weight = values.get(SVG_ATTR_FONT_WEIGHT, self.font_weight)
font = values.get(SVG_ATTR_FONT, None)
if font is not None:
self.parse_font(font)
self.text = values.get(SVG_TAG_TEXT, self.text)
self.x = Length(values.get(SVG_ATTR_X, self.x)).value()
self.y = Length(values.get(SVG_ATTR_Y, self.y)).value()
self.dx = Length(values.get(SVG_ATTR_DX, self.dx)).value()
self.dy = Length(values.get(SVG_ATTR_DY, self.dy)).value()
def __copy__(self):
return SVGText(self)
def render(self, width=None, height=None, relative_length=None, **kwargs):
if width is None and relative_length is not None:
width = relative_length
if height is None and relative_length is not None:
height = relative_length
self.transform.render(width=width, height=height, relative_length=relative_length, **kwargs)
if isinstance(self.x, Length):
self.x = self.x.value(relative_length=width, **kwargs)
if isinstance(self.y, Length):
self.y = self.y.value(relative_length=height, **kwargs)
if isinstance(self.dx, Length):
self.dx = self.dx.value(relative_length=width, **kwargs)
if isinstance(self.dy, Length):
self.dy = self.dy.value(relative_length=height, **kwargs)
return self
def bbox(self, transformed=True):
"""
Get the bounding box for the given text object.
"""
if self.path is not None:
return (self.path * self.transform).bbox(transformed=True)
width = self.width
height = self.height
xmin = self.x
ymin = self.y - height
xmax = self.x + width
ymax = self.y
if not hasattr(self, 'anchor') or self.anchor == 'start':
pass
elif self.anchor == 'middle':
xmin -= (width / 2)
xmax -= (width / 2)
elif self.anchor == 'end':
xmin -= width
xmax -= width
if transformed:
p0 = self.transform.transform_point([xmin, ymin])
p1 = self.transform.transform_point([xmin, ymax])
p2 = self.transform.transform_point([xmax, ymin])
p3 = self.transform.transform_point([xmax, ymax])
xmin = min(p0[0], p1[0], p2[0], p3[0])
ymin = min(p0[1], p1[1], p2[1], p3[1])
xmax = max(p0[0], p1[0], p2[0], p3[0])
ymax = max(p0[1], p1[1], p2[1], p3[1])
return xmin, ymin, xmax, ymax
class SVGDesc:
"""
SVG Desc are just desc data.
This is a stub element.
"""
def __init__(self, values, desc=None):
if isinstance(values, dict):
self.desc = desc
else:
self.desc = values
class SVGImage(GraphicObject, Transformable):
"""
SVG Images are defined in SVG 2.0 12.3
This class is called SVG Image rather than image as a guard against many Image objects
which are quite useful and would be ideal for reading the linked or contained data.
"""
def __init__(self, *args, **kwargs):
self.url = None
self.data = None
self.viewbox = None
self.image = None
self.image_width = None
self.image_height = None
Transformable.__init__(self, *args, **kwargs)
GraphicObject.__init__(self, *args, **kwargs)
if self.url is not None:
if self.url.startswith("data:image/"):
# Data URL
from base64 import b64decode
if self.url.startswith("data:image/png;base64,"):
self.data = b64decode(self.url[22:])
elif self.url.startswith("data:image/jpg;base64,"):
self.data = b64decode(self.url[22:])
elif self.url.startswith("data:image/jpeg;base64,"):
self.data = b64decode(self.url[23:])
elif self.url.startswith("data:image/svg+xml;base64,"):
self.data = b64decode(self.url[26:])
if SVG_ATTR_WIDTH in kwargs:
self.viewbox.physical_width = Length(kwargs[SVG_ATTR_WIDTH]).value()
if SVG_ATTR_HEIGHT in kwargs:
self.viewbox.physical_height = Length(kwargs[SVG_ATTR_HEIGHT]).value()
def property_by_object(self, s):
Transformable.property_by_object(self, s)
GraphicObject.property_by_object(self, s)
self.url = s.url
self.data = s.data
self.viewbox = s.viewbox
self.image = s.image
self.image_width = s.image_width
self.image_height = s.image_height
def property_by_values(self, values):
Transformable.property_by_values(self, values)
GraphicObject.property_by_values(self, values)
if XLINK_HREF in values:
self.url = values[XLINK_HREF]
elif SVG_HREF in values:
self.url = values[SVG_HREF]
self.viewbox = Viewbox(values)
if 'image' in values:
self.image = values['image']
self.image_width, self.image_height = self.image.size
def __copy__(self):
"""
Copy of SVGImage. This will not copy the .image subobject in a deep manner
since it's optional that that object will exist or not. As such if using PIL it would
be required to either say self.image = self.image.copy() or call .load() again.
"""
return SVGImage(self)
def load(self, directory=None):
try:
from PIL import Image
if self.data is not None:
self.load_data()
elif self.url is not None:
self.load_file(directory)
self.set_values_by_image()
except ImportError:
pass
def load_data(self):
try:
# This code will not activate without PIL/Pillow installed.
from PIL import Image
if self.data is not None:
from io import BytesIO
self.image = Image.open(BytesIO(self.data))
else:
return
except ImportError:
# PIL/Pillow not found, decoding data is most we can do.
pass
def load_file(self, directory):
try:
# This code will not activate without PIL/Pillow installed.
from PIL import Image
if self.url is not None:
try:
self.image = Image.open(self.url)
except IOError:
try:
if directory is not None:
from os.path import join
relpath = join(directory, self.url)
self.image = Image.open(relpath)
except IOError:
return
except ImportError:
# PIL/Pillow not found, decoding data is most we can do.
pass
def set_values_by_image(self):
if self.image is not None:
self.image_width = self.image.width
self.image_height = self.image.height
else:
return
viewbox = "0 0 %d %d" % (self.image_width, self.image_height)
self.viewbox.set_viewbox(viewbox)
self.viewbox.render(width=self.image_width, height=self.image_height)
viewbox_transform = self.viewbox.transform()
self.transform = Matrix(viewbox_transform) * self.transform
def bbox(self, transformed=True):
"""
Get the bounding box for the given image object
"""
if self.image_width is None or self.image_height is None:
p = Point(0, 0)
p *= self.transform
return p[0], p[1], p[0], p[1]
width = self.image_width
height = self.image_height
if transformed:
p = (Point(0, 0) * self.transform,
Point(width, 0) * self.transform,
Point(width, height) * self.transform,
Point(0, height) * self.transform)
else:
p = (Point(0, 0),
Point(width, 0),
Point(width, height),
Point(0, height))
x_vals = list(s[0] for s in p)
y_vals = list(s[1] for s in p)
min_x = min(x_vals)
min_y = min(y_vals)
max_x = max(x_vals)
max_y = max(y_vals)
return min_x, min_y, max_x, max_y
class Viewbox:
def __init__(self, *args, **kwargs):
"""
Viewbox(nodes)
If the viewbox is not available or in the nodes data it doesn't need to be expressly defined.
Viewbox control the scaling between the element size and viewbox.
The given width and height are merely to interpret the meaning of percent values of lengths. Usually this is
the size of the physical space being occupied. And the PPI is used to interpret the meaning of physical units
if the pixel_per_inch conversion isn't 96.
:param args: nodes, must contain node values.
:param kwargs: ppi, width, height, viewbox
"""
self.viewbox_x = None
self.viewbox_y = None
self.viewbox_width = None
self.viewbox_height = None
if len(args) == 1:
if isinstance(args[0], dict):
values = args[0]
elif isinstance(args[0], Viewbox):
viewbox = args[0]
self.viewbox_x = viewbox.viewbox_x
self.viewbox_y = viewbox.viewbox_y
self.viewbox_width = viewbox.viewbox_width
self.viewbox_height = viewbox.viewbox_height
self.viewbox = viewbox.viewbox
self.physical_width = viewbox.physical_width
self.physical_height = viewbox.physical_height
self.element_x = viewbox.element_x
self.element_y = viewbox.element_y
self.element_height = viewbox.element_height
self.element_width = viewbox.element_height
self.preserve_aspect_ratio = viewbox.preserve_aspect_ratio
return
else:
return
else:
return
if SVG_ATTR_VIEWBOX in kwargs:
self.viewbox = kwargs[SVG_ATTR_VIEWBOX]
elif SVG_ATTR_VIEWBOX in values:
self.viewbox = values[SVG_ATTR_VIEWBOX]
else:
self.viewbox = None
if SVG_ATTR_WIDTH in kwargs:
self.physical_width = Length(kwargs[SVG_ATTR_WIDTH]).value()
else:
self.physical_width = None
if SVG_ATTR_HEIGHT in kwargs:
self.physical_height = Length(kwargs[SVG_ATTR_HEIGHT]).value()
else:
self.physical_height = None
if SVG_ATTR_WIDTH in values:
self.element_width = Length(values[SVG_ATTR_WIDTH]).value(relative_length=self.physical_width)
else:
self.element_width = self.physical_width
if SVG_ATTR_HEIGHT in values:
self.element_height = Length(values[SVG_ATTR_HEIGHT]).value(relative_length=self.physical_height)
else:
self.element_height = self.physical_height
if SVG_ATTR_X in values:
self.element_x = Length(values[SVG_ATTR_X]).value(relative_length=self.physical_width)
else:
self.element_x = 0
if SVG_ATTR_Y in values:
self.element_y = Length(values[SVG_ATTR_Y]).value(relative_length=self.physical_height)
else:
self.element_y = 0
self.set_viewbox(self.viewbox)
if SVG_ATTR_PRESERVEASPECTRATIO in values:
self.preserve_aspect_ratio = values[SVG_ATTR_PRESERVEASPECTRATIO]
else:
self.preserve_aspect_ratio = None
def __str__(self):
return '%s %s %s %s -> %s %s %s %s' % (
Length.str(self.element_x),
Length.str(self.element_y),
Length.str(self.element_width),
Length.str(self.element_height),
Length.str(self.viewbox_x),
Length.str(self.viewbox_y),
Length.str(self.viewbox_width),
Length.str(self.viewbox_height),
)
def set_viewbox(self, viewbox):
if viewbox is not None and isinstance(viewbox, str):
dims = list(REGEX_FLOAT.findall(viewbox))
self.viewbox_x = float(dims[0])
self.viewbox_y = float(dims[1])
self.viewbox_width = float(dims[2])
self.viewbox_height = float(dims[3])
def render(self, width=None, height=None, relative_length=None, **kwargs):
if width is None and relative_length is not None:
width = relative_length
if height is None and relative_length is not None:
height = relative_length
if isinstance(self.physical_width, Length):
self.physical_width = self.physical_width.value(relative_length=width, **kwargs)
if isinstance(self.physical_height, Length):
self.physical_height = self.physical_height.value(relative_length=height, **kwargs)
if self.physical_width is not None:
width = self.physical_width
if self.physical_height is not None:
height = self.physical_height
if isinstance(self.element_x, Length):
self.element_x = self.element_x.value(relative_length=width, **kwargs)
if isinstance(self.element_y, Length):
self.element_y = self.element_y.value(relative_length=height, **kwargs)
if isinstance(self.element_width, Length):
self.element_width = self.element_width.value(relative_length=width, **kwargs)
if isinstance(self.element_height, Length):
self.element_height = self.element_height.value(relative_length=height, **kwargs)
return self
def transform(self):
return Viewbox.viewbox_transform(
self.element_x, self.element_y, self.element_width, self.element_height,
self.viewbox_x, self.viewbox_y, self.viewbox_width, self.viewbox_height,
self.preserve_aspect_ratio)
@staticmethod
def viewbox_transform(e_x, e_y, e_width, e_height, vb_x, vb_y, vb_width, vb_height, aspect):
"""
SVG 1.1 7.2, SVG 2.0 8.2 equivalent transform of an SVG viewport.
With regards to https://github.com/w3c/svgwg/issues/215 use 8.2 version.
It creates transform commands equal to that viewport expected.
:param svg_node: dict containing the relevant svg entries.
:return: string of the SVG transform commands to account for the viewbox.
"""
# Let e-x, e-y, e-width, e-height be the position and size of the element respectively.
# Let vb-x, vb-y, vb-width, vb-height be the min-x, min-y,
# width and height values of the viewBox attribute respectively.
# Let align be the align value of preserveAspectRatio, or 'xMidYMid' if preserveAspectRatio is not defined.
# Let meetOrSlice be the meetOrSlice value of preserveAspectRatio, or 'meet' if preserveAspectRatio is not defined
# or if meetOrSlice is missing from this value.
if e_x is None or e_y is None or e_width is None or e_height is None or \
vb_x is None or vb_y is None or vb_width is None or vb_height is None:
return ''
if aspect is not None:
aspect_slice = aspect.split(' ')
try:
align = aspect_slice[0]
except IndexError:
align = 'xMidyMid'
try:
meet_or_slice = aspect_slice[1]
except IndexError:
meet_or_slice = 'meet'
else:
align = 'xMidyMid'
meet_or_slice = 'meet'
# Initialize scale-x to e-width/vb-width.
scale_x = e_width / vb_width
# Initialize scale-y to e-height/vb-height.
scale_y = e_height / vb_height
# If align is not 'none' and meetOrSlice is 'meet', set the larger of scale-x and scale-y to the smaller.
if align != SVG_VALUE_NONE and meet_or_slice == 'meet':
scale_x = scale_y = min(scale_x, scale_y)
# Otherwise, if align is not 'none' and meetOrSlice is 'slice', set the smaller of scale-x and scale-y to the larger
elif align != SVG_VALUE_NONE and meet_or_slice == 'slice':
scale_x = scale_y = max(scale_x, scale_y)
# Initialize translate-x to e-x - (vb-x * scale-x).
translate_x = e_x - (vb_x * scale_x)
# Initialize translate-y to e-y - (vb-y * scale-y)
translate_y = e_y - (vb_y * scale_y)
# If align contains 'xMid', add (e-width - vb-width * scale-x) / 2 to translate-x.
align = align.lower()
if 'xmid' in align:
translate_x += (e_width - vb_width * scale_x) / 2.0
# If align contains 'xMax', add (e-width - vb-width * scale-x) to translate-x.
if 'xmax' in align:
translate_x += e_width - vb_width * scale_x
# If align contains 'yMid', add (e-height - vb-height * scale-y) / 2 to translate-y.
if 'ymid' in align:
translate_y += (e_height - vb_height * scale_y) / 2.0
# If align contains 'yMax', add (e-height - vb-height * scale-y) to translate-y.
if 'ymax' in align:
translate_y += (e_height - vb_height * scale_y)
# The transform applied to content contained by the element is given by:
# translate(translate-x, translate-y) scale(scale-x, scale-y)
if isinstance(scale_x, Length) or isinstance(scale_y, Length):
raise ValueError
if translate_x == 0 and translate_y == 0:
if scale_x == 1 and scale_y == 1:
return "" # Nothing happens.
else:
return "scale(%s, %s)" % (Length.str(scale_x), Length.str(scale_y))
else:
if scale_x == 1 and scale_y == 1:
return "translate(%s, %s)" % (Length.str(translate_x), Length.str(translate_y))
else:
return "translate(%s, %s) scale(%s, %s)" % \
(Length.str(translate_x), Length.str(translate_y),
Length.str(scale_x), Length.str(scale_y))
class SVG(Group):
"""
SVG Document and Parsing.
SVG is the SVG main object and also the embedded SVGs within it. It's a subtype of Group. The SVG has a viewbox,
and parsing methods which can be used if given a stream, path, or svg string.
"""
def __init__(self, *args, **kwargs):
Group.__init__(self, *args, **kwargs)
self.viewbox = None
values = None
if len(args) == 1:
if isinstance(args[0], dict):
values = args[0]
elif isinstance(args[0], SVG):
s = args[0]
self.viewbox = Viewbox(s.viewbox)
return
if values is not None:
self.viewbox = Viewbox(values)
else:
self.viewbox = Viewbox(kwargs)
def render(self, ppi=None, width=None, height=None):
self.viewbox.render(ppi=ppi, width=width, height=height)
def elements(self, conditional=None):
yield self
yield self.viewbox
for q in self.select(conditional):
yield q
@staticmethod
def _shadow_iter(elem, children):
yield 'start', elem
for e, c in children:
for shadow_event, shadow_elem in SVG._shadow_iter(e, c):
yield shadow_event, shadow_elem
yield 'end', elem
@staticmethod
def svg_structure_parse(source):
"""
SVG Structure parsing parses the svg file such that it creates the structure implied by reused objects in a more
generalized context. Objects ids are read, and put into a shadow tree. <defs> objects are omitted from the
structure of the objects. And <use> objects seamlessly replaced with their definitions.
:param source: svg file source.
:generates: iterparse 'start' and 'end' values restructured.
"""
defs = {}
parent = None
children = list()
def_depth = 0
for event, elem in iterparse(source, events=('start', 'end')):
tag = elem.tag
if tag.startswith('{http://www.w3.org/2000/svg'):
tag = tag[28:] # Removing namespace. http://www.w3.org/2000/svg:
elem.tag = tag
attributes = elem.attrib
if event == 'start':
# New node.
siblings = children # Parent's children are now my siblings.
parent = (parent, children) # parent is now previous node context
children = list() # new node has no children.
node = (elem, children) # define this node.
siblings.append(node) # siblings now includes this node.
if SVG_TAG_USE == tag:
url = None
if XLINK_HREF in attributes:
url = attributes[XLINK_HREF]
if SVG_HREF in attributes:
url = attributes[SVG_HREF]
if url is not None:
transform = False
try:
transform = True
x = attributes[SVG_ATTR_X]
del attributes[SVG_ATTR_X]
except KeyError:
x = '0'
try:
transform = True
y = attributes[SVG_ATTR_Y]
del attributes[SVG_ATTR_Y]
except KeyError:
y = '0'
if transform:
try:
attributes[SVG_ATTR_TRANSFORM] = '%s translate(%s, %s)' % \
(attributes[SVG_ATTR_TRANSFORM], x, y)
except KeyError:
attributes[SVG_ATTR_TRANSFORM] = 'translate(%s, %s)' % (x, y)
yield event, elem
try:
s_elem, s_children = defs[url[1:]] # Shadow node.
except KeyError:
continue
for shadow_event, shadow_elem in SVG._shadow_iter(s_elem, s_children):
yield shadow_event, shadow_elem
continue
if SVG_ATTR_ID in attributes: # If we have an ID, save the node.
defs[attributes[SVG_ATTR_ID]] = node # store node value in defs.
if tag == SVG_TAG_DEFS:
def_depth += 1
else:
# event is 'end', pop values.
parent, children = parent # Pop off previous context.
if tag == SVG_TAG_DEFS:
def_depth -= 1
continue
if def_depth == 0:
yield event, elem
@staticmethod
def parse(source,
reify=True,
ppi=DEFAULT_PPI,
width=1,
height=1,
color="black",
transform=None,
context=None):
"""
Parses the SVG file.
Style elements are split into their proper values.
switch elements are not processed.
title elements are not processed.
metadata elements are not processed.
foreignObject elements are not processed.
use elements are not processed.
"""
root = context
styles = {}
stack = []
values = {SVG_ATTR_COLOR: color, SVG_ATTR_FILL: color,
SVG_ATTR_STROKE: color}
if transform is not None:
values[SVG_ATTR_TRANSFORM] = transform
for event, elem in SVG.svg_structure_parse(source):
# print("%d tag: %s is %s" % (len(stack), elem.tag, event))
if event == 'start':
stack.append((context, values))
current_values = values
values = {}
values.update(current_values) # copy of dictionary
tag = elem.tag
# Non-propagating values.
if SVG_ATTR_PRESERVEASPECTRATIO in values:
del values[SVG_ATTR_PRESERVEASPECTRATIO]
if SVG_ATTR_VIEWBOX in values:
del values[SVG_ATTR_VIEWBOX]
if SVG_ATTR_ID in values:
del values[SVG_ATTR_ID]
attributes = elem.attrib # priority; lowest
attributes[SVG_ATTR_TAG] = tag
# Split any Style block elements into parts; priority medium
style = ''
if '*' in styles: # Select all.
style += styles['*']
if tag in styles: # selector type
style += styles[tag]
if SVG_ATTR_ID in attributes: # Selector id #id
svg_id = attributes[SVG_ATTR_ID]
css_tag = '#%s' % svg_id
if css_tag in styles:
style += styles[css_tag]
if SVG_ATTR_CLASS in attributes: # Selector class .class
for svg_class in attributes[SVG_ATTR_CLASS].split(' '):
css_tag = '.%s' % svg_class
if css_tag in styles:
style += styles[css_tag]
css_tag = '%s.%s' % (tag, svg_class) # Selector type/class type.class
if css_tag in styles:
style += styles[css_tag]
# Split style element into parts; priority highest
if SVG_ATTR_STYLE in attributes:
style += attributes[SVG_ATTR_STYLE]
# Process style tag left to right.
for equate in style.split(";"):
equal_item = equate.split(":")
if len(equal_item) == 2:
key = str(equal_item[0]).strip()
value = str(equal_item[1]).strip()
attributes[key] = value
if SVG_ATTR_FILL in attributes and attributes[SVG_ATTR_FILL] == SVG_VALUE_CURRENT_COLOR:
if SVG_ATTR_COLOR in attributes:
attributes[SVG_ATTR_FILL] = attributes[SVG_ATTR_COLOR]
else:
attributes[SVG_ATTR_FILL] = values[SVG_ATTR_COLOR]
if SVG_ATTR_STROKE in attributes and attributes[SVG_ATTR_STROKE] == SVG_VALUE_CURRENT_COLOR:
if SVG_ATTR_COLOR in attributes:
attributes[SVG_ATTR_STROKE] = attributes[SVG_ATTR_COLOR]
else:
attributes[SVG_ATTR_STROKE] = values[SVG_ATTR_COLOR]
if SVG_ATTR_TRANSFORM in attributes:
# If transform is already in values, append the new value.
if SVG_ATTR_TRANSFORM in values:
attributes[SVG_ATTR_TRANSFORM] = values[SVG_ATTR_TRANSFORM] + \
" " + \
attributes[SVG_ATTR_TRANSFORM]
else:
attributes[SVG_ATTR_TRANSFORM] = attributes[SVG_ATTR_TRANSFORM]
values.update(attributes)
if SVG_NAME_TAG == tag:
# The ordering for transformations on the SVG object are:
# explicit transform, parent transforms, attribute transforms, viewport transforms
s = SVG(values)
s.render(ppi=ppi, width=width, height=height)
# viewbox was rendered here.
viewport_transform = s.viewbox.transform()
if SVG_ATTR_TRANSFORM in values:
# transform on SVG element applied as if svg had parent with transform.
values[SVG_ATTR_TRANSFORM] += " " + viewport_transform
else:
values[SVG_ATTR_TRANSFORM] = viewport_transform
width = s.viewbox.viewbox_width
height = s.viewbox.viewbox_height
if context is None:
stack[-1] = (context, values)
if context is not None:
context.append(s)
context = s
if root is None:
root = s
continue
elif SVG_TAG_GROUP == tag:
s = Group(values)
context.append(s)
context = s
continue
elif SVG_TAG_PATH == tag:
try:
s = Path(values)
except ValueError:
continue
elif SVG_TAG_CIRCLE == tag:
s = Circle(values)
elif SVG_TAG_ELLIPSE == tag:
s = Ellipse(values)
elif SVG_TAG_LINE == tag:
s = SimpleLine(values)
elif SVG_TAG_POLYLINE == tag:
s = Polyline(values)
elif SVG_TAG_POLYGON == tag:
s = Polygon(values)
elif SVG_TAG_RECT == tag:
s = Rect(values)
elif SVG_TAG_IMAGE == tag:
s = SVGImage(values)
else:
# <style>, <text>, <desc>
continue
s.render(ppi=ppi, width=width, height=height)
if reify:
s.reify()
context.append(s)
else: # End event.
# The iterparse spec makes it clear that internal text data is undefined except at the end.
tag = elem.tag
if SVG_TAG_TEXT == tag:
s = SVGText(values, text=elem.text)
s.render(ppi=ppi, width=width, height=height)
if reify:
s.reify()
context.append(s)
elif SVG_TAG_TSPAN == tag:
s = SVGText(values, text=elem.text)
context.append(s)
s.render(ppi=ppi, width=width, height=height)
elif SVG_TAG_DESC == tag:
s = SVGDesc(values, desc=elem.text)
context.append(s)
elif SVG_TAG_STYLE == tag:
assignments = list(re.findall(REGEX_CSS_STYLE, elem.text))
for key, value in assignments:
key = key.strip()
value = value.strip()
for selector in key.split(','): # Can comma select subitems.
styles[selector.strip()] = value
context, values = stack.pop()
return root