|
|
# -*- 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
|