# -*- coding: ISO-8859-1 -*-

import re

try:
    from collections.abc import MutableSequence  # noqa
except ImportError:
    from collections import MutableSequence  # noqa

from copy import copy
from math import (
    acos,
    atan,
    atan2,
    ceil,
    cos,
    degrees,
    hypot,
    log,
    radians,
    sin,
    sqrt,
    tan,
)
from xml.etree.ElementTree import iterparse

try:
    from math import tau
except ImportError:
    from math import pi

    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 Image class acquires new functionality if provided with PIL/Pillow as an import
and the Arc can do exact arc calculations if scipy is installed.
"""

SVGELEMENTS_VERSION = "1.9.6"

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_TITLE = "title"
SVG_TAG_METADATA = "metadata"
SVG_TAG_STYLE = "style"
SVG_TAG_DEFS = "defs"
SVG_TAG_USE = "use"
SVG_TAG_CLIPPATH = "clipPath"
SVG_TAG_PATTERN = "pattern"

SVG_STRUCT_ATTRIB = "attributes"
SVG_ATTR_ID = "id"
SVG_ATTR_DATA = "d"
SVG_ATTR_DISPLAY = "display"
SVG_ATTR_COLOR = "color"
SVG_ATTR_FILL = "fill"
SVG_ATTR_FILL_OPACITY = "fill-opacity"
SVG_ATTR_STROKE = "stroke"
SVG_ATTR_STROKE_OPACITY = "stroke-opacity"
SVG_ATTR_STROKE_WIDTH = "stroke-width"
SVG_ATTR_TRANSFORM = "transform"
SVG_ATTR_STYLE = "style"
SVG_ATTR_CLASS = "class"
SVG_ATTR_CLIP_PATH = "clip-path"
SVG_ATTR_CLIP_RULE = "clip-rule"
SVG_ATTR_CLIP_UNIT_TYPE = "clipPathUnits"
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_STYLE = "font-style"
SVG_ATTR_FONT_VARIANT = "font-variant"
SVG_ATTR_FONT_WEIGHT = "font-weight"  # normal, bold, bolder, lighter, 100-900
SVG_ATTR_FONT_STRETCH = "font-stretch"
SVG_ATTR_FONT_SIZE = "font-size"
SVG_ATTR_TEXT_ANCHOR = "text-anchor"
SVG_ATTR_TEXT_ALIGNMENT_BASELINE = "alignment-baseline"
SVG_ATTR_TEXT_DOMINANT_BASELINE = "dominant-baseline"
SVG_ATTR_PATTERN_CONTENT_UNITS = "patternContentUnits"
SVG_ATTR_PATTERN_TRANSFORM = "patternTransform"
SVG_ATTR_PATTERN_UNITS = "patternUnits"

SVG_ATTR_VECTOR_EFFECT = "vector-effect"

SVG_UNIT_TYPE_USERSPACEONUSE = "userSpaceOnUse"
SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = "objectBoundingBox"

SVG_RULE_NONZERO = "nonzero"
SVG_RULE_EVENODD = "evenodd"

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"

SVG_VALUE_NON_SCALING_STROKE = "non-scaling-stroke"

PATTERN_WS = r"[\s\t\n]*"
PATTERN_COMMA = r"(?:\s*,\s*|\s+|(?=-))"
PATTERN_COMMAWSP = r"[ ,\t\n\x09\x0A\x0C\x0D]+"
PATTERN_FLOAT = r"[-+]?[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_IRI = re.compile(r"url\(#?(.*)\)")
REGEX_DATA_URL = re.compile(r"^data:([^,]*),")
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(
    r"(?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(r"(%s)([A-Za-z%%]*)" % PATTERN_FLOAT)
REGEX_CSS_COMMENT = re.compile(r"\/\*[\s\S]*?\*\/|\/\/.*$", re.MULTILINE)
REGEX_CSS_STYLE = re.compile(r"([^{]+)\s*\{\s*([^}]+)\s*\}")
REGEX_CSS_FONT = re.compile(
    r"^"
    r"(?:"
    r"(?:(normal|italic|oblique)\s)?"
    r"(?:(normal|small-caps)\s)?"
    r"(?:(normal|bold|bolder|lighter|[0-9]{3})\s)?"
    r"(?:(normal|(?:ultra-|extra-|semi-)?condensed|(?:semi-|extra-)?expanded)\s)"
    r"?){0,4}"
    r"(?:"
    r"((?:x-|xx-)?small|medium|(?:x-|xx-)?large|larger|smaller|[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?"
    r"(?:em|pt|pc|px|%)?)"
    r"(?:/"
    r"((?:x-|xx-)?small|medium|(?:x-|xx-)?large|larger|smaller|[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?"
    r"(?:em|pt|pc|px|%)?)"
    r")?\s"
    r")?"
    r"([^;]*);?"
    r"$"
)
REGEX_CSS_FONT_FAMILY = re.compile(
    r"""(?:([^\s"';,]+|"[^";,]+"|'[^';,]+'|serif|sans-serif|cursive|fantasy|monospace)),?\s*;?"""
)

svg_parse = [("COMMAND", r"[MmZzLlHhVvCcSsQqTtAa]"), ("SKIP", PATTERN_COMMAWSP)]
svg_re = re.compile("|".join("(?P<%s>%s)" % pair for pair in svg_parse))
num_parse = [("FLOAT", PATTERN_FLOAT), ("CLOSE", r"[Zz]"), ("SKIP", PATTERN_COMMAWSP)]
num_re = re.compile("|".join("(?P<%s>%s)" % pair for pair in num_parse))
flag_parse = [("FLAG", r"[01]"), ("SKIP", PATTERN_COMMAWSP)]
flag_re = re.compile("|".join("(?P<%s>%s)" % pair for pair in flag_parse))


class SVGLexicalParser:
    def __init__(self):
        self.parser = None
        self.pathd = None
        self.pos = 0
        self.limit = 0
        self.inline_close = None

    def _command(self):
        while self.pos < self.limit:
            match = svg_re.match(self.pathd, self.pos)
            if match is None:
                return None  # Did not match at command sequence.
            self.pos = match.end()
            kind = match.lastgroup
            if kind == "SKIP":
                continue
            return match.group()
        return None

    def _more(self):
        while self.pos < self.limit:
            match = num_re.match(self.pathd, self.pos)
            if match is None:
                return False
            kind = match.lastgroup
            if kind == "CLOSE":
                self.inline_close = match.group()
                return False
            if kind == "SKIP":
                # move skipped elements forward.
                self.pos = match.end()
                continue
            return True
        return None

    def _number(self):
        while self.pos < self.limit:
            match = num_re.match(self.pathd, self.pos)
            if match is None:
                break  # No more matches.
            kind = match.lastgroup
            if kind == "CLOSE":
                # Inline Close
                self.inline_close = match.group()
                return None
            self.pos = match.end()
            if kind == "SKIP":
                continue
            return float(match.group())
        return None

    def _flag(self):
        while self.pos < self.limit:
            match = flag_re.match(self.pathd, self.pos)
            if match is None:
                break  # No more matches.
            self.pos = match.end()
            kind = match.lastgroup
            if kind == "SKIP":
                continue
            return bool(int(match.group()))
        return None

    def _coord(self):
        x = self._number()
        if x is None:
            return None
        y = self._number()
        if y is None:
            raise ValueError
        return x, y

    def _rcoord(self):
        position = self._coord()
        if position is None:
            return None
        current_pos = self.parser.current_point
        if current_pos is None:
            return position
        return position[0] + current_pos.x, position[1] + current_pos.y

    def parse(self, parser, pathd):
        self.parser = parser
        self.parser.start()
        self.pathd = pathd
        self.pos = 0
        self.limit = len(pathd)
        while True:
            cmd = self._command()
            if cmd is None:
                return
            elif cmd == "z" or cmd == "Z":
                if self._more():
                    raise ValueError
                self.parser.closed(relative=cmd.islower())
                self.inline_close = None
                continue
            elif cmd == "m":
                if not self._more():
                    raise ValueError
                coord = self._rcoord()
                self.parser.move(coord, relative=True)
                while self._more():
                    coord = self._rcoord()
                    self.parser.line(coord, relative=True)
            elif cmd == "M":
                if not self._more():
                    raise ValueError
                coord = self._coord()
                self.parser.move(coord, relative=False)
                while self._more():
                    coord = self._coord()
                    self.parser.line(coord, relative=False)
            elif cmd == "l":
                while True:
                    coord = self._rcoord()
                    if coord is None:
                        coord = self.inline_close
                        if coord is None:
                            raise ValueError
                    self.parser.line(coord, relative=True)
                    if not self._more():
                        break
            elif cmd == "L":
                while True:
                    coord = self._coord()
                    if coord is None:
                        coord = self.inline_close
                        if coord is None:
                            raise ValueError
                    self.parser.line(coord, relative=False)
                    if not self._more():
                        break
            elif cmd == "t":
                while True:
                    coord = self._rcoord()
                    if coord is None:
                        coord = self.inline_close
                        if coord is None:
                            raise ValueError
                    self.parser.smooth_quad(coord, relative=True)
                    if not self._more():
                        break
            elif cmd == "T":
                while True:
                    coord = self._coord()
                    if coord is None:
                        coord = self.inline_close
                        if coord is None:
                            raise ValueError
                    self.parser.smooth_quad(coord, relative=False)
                    if not self._more():
                        break
            elif cmd == "h":
                while True:
                    value = self._number()
                    self.parser.horizontal(value, relative=True)
                    if not self._more():
                        break
            elif cmd == "H":
                while True:
                    value = self._number()
                    self.parser.horizontal(value, relative=False)
                    if not self._more():
                        break
            elif cmd == "v":
                while True:
                    value = self._number()
                    self.parser.vertical(value, relative=True)
                    if not self._more():
                        break
            elif cmd == "V":
                while self._more():
                    value = self._number()
                    self.parser.vertical(value, relative=False)
            elif cmd == "c":
                while True:
                    coord1, coord2, coord3 = (
                        self._rcoord(),
                        self._rcoord(),
                        self._rcoord(),
                    )
                    if coord1 is None:
                        coord1 = self.inline_close
                        if coord1 is None:
                            raise ValueError
                    if coord2 is None:
                        coord2 = self.inline_close
                        if coord2 is None:
                            raise ValueError
                    if coord3 is None:
                        coord3 = self.inline_close
                        if coord3 is None:
                            raise ValueError
                    self.parser.cubic(coord1, coord2, coord3, relative=True)
                    if not self._more():
                        break
            elif cmd == "C":
                while True:
                    coord1, coord2, coord3 = self._coord(), self._coord(), self._coord()
                    if coord1 is None:
                        coord1 = self.inline_close
                        if coord1 is None:
                            raise ValueError
                    if coord2 is None:
                        coord2 = self.inline_close
                        if coord2 is None:
                            raise ValueError
                    if coord3 is None:
                        coord3 = self.inline_close
                        if coord3 is None:
                            raise ValueError
                    self.parser.cubic(coord1, coord2, coord3, relative=False)
                    if not self._more():
                        break
            elif cmd == "q":
                while True:
                    coord1, coord2 = self._rcoord(), self._rcoord()
                    if coord1 is None:
                        coord1 = self.inline_close
                        if coord1 is None:
                            raise ValueError
                    if coord2 is None:
                        coord2 = self.inline_close
                        if coord2 is None:
                            raise ValueError
                    self.parser.quad(coord1, coord2, relative=True)
                    if not self._more():
                        break
            elif cmd == "Q":
                while True:
                    coord1, coord2 = self._coord(), self._coord()
                    if coord1 is None:
                        coord1 = self.inline_close
                        if coord1 is None:
                            raise ValueError
                    if coord2 is None:
                        coord2 = self.inline_close
                        if coord2 is None:
                            raise ValueError
                    self.parser.quad(coord1, coord2, relative=False)
                    if not self._more():
                        break
            elif cmd == "s":
                while True:
                    coord1, coord2 = self._rcoord(), self._rcoord()
                    if coord1 is None:
                        coord1 = self.inline_close
                        if coord1 is None:
                            raise ValueError
                    if coord2 is None:
                        coord2 = self.inline_close
                        if coord2 is None:
                            raise ValueError
                    self.parser.smooth_cubic(coord1, coord2, relative=True)
                    if not self._more():
                        break
            elif cmd == "S":
                while True:
                    coord1, coord2 = self._coord(), self._coord()
                    if coord1 is None:
                        coord1 = self.inline_close
                        if coord1 is None:
                            raise ValueError
                    if coord2 is None:
                        coord2 = self.inline_close
                        if coord2 is None:
                            raise ValueError
                    self.parser.smooth_cubic(coord1, coord2, relative=False)
                    if not self._more():
                        break
            elif cmd == "a":
                while self._more():
                    rx, ry, rotation, arc, sweep, coord = (
                        self._number(),
                        self._number(),
                        self._number(),
                        self._flag(),
                        self._flag(),
                        self._rcoord(),
                    )
                    if sweep is None:
                        raise ValueError
                    if coord is None:
                        coord = self.inline_close
                        if coord is None:
                            raise ValueError
                    self.parser.arc(rx, ry, rotation, arc, sweep, coord, relative=True)
            elif cmd == "A":
                while self._more():
                    rx, ry, rotation, arc, sweep, coord = (
                        self._number(),
                        self._number(),
                        self._number(),
                        self._flag(),
                        self._flag(),
                        self._coord(),
                    )
                    if coord is None:
                        coord = self.inline_close
                        if coord is None:
                            raise ValueError
                    self.parser.arc(rx, ry, rotation, arc, sweep, coord, relative=False)
        self.parser.end()


class Length(object):
    """
    SVGLength as used in SVG

    Length class is lazy when solving values. Several conversion values are 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 absolute values like 30cm or 20in which 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 is 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 e.g. Length("20in") + Length("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 * 4.0 / 3.0
        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 * 4.0 / 3.0
            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 / 4.0 / 3.0
            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 * 4.0 / 3.0)
            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 * 3.0 / 4.0)
            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 = other.in_pixels()
            if o is not None:
                if abs(s - o) <= ERROR:
                    return True
        s = self.in_inches()
        if s is not None:
            o = other.in_inches()
            if o is not None:
                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 * 3.0 / 4.0
        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,
        **kwargs,
    ):
        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 * 4.0 / 3.0
        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.width / 100.0
        if self.units == "vh":
            if viewbox is None:
                return self
            v = Viewbox(viewbox)
            return self.amount * v.height / 100.0
        if self.units == "vmin":
            if viewbox is None:
                return self
            v = Viewbox(viewbox)
            m = min(v.height, v.height)
            return self.amount * m / 100.0
        if self.units == "vmax":
            if viewbox is None:
                return self
            v = Viewbox(viewbox)
            m = max(v.height, v.height)
            return self.amount * m / 100.0
        try:
            return float(self)
        except ValueError:
            return self

    @staticmethod
    def str(s):
        if s is None:
            return "n/a"
        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)
        try:
            s = "%.12f" % s
        except TypeError:
            return str(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

        arglen = len(args)
        if arglen == 1:
            v = args[0]
            if isinstance(v, Color):
                self.value = v.value
            elif isinstance(v, int):
                self.rgb = v
            else:
                self.value = Color.parse(v)
        elif arglen == 2:
            v = args[0]
            if isinstance(v, Color):
                self.value = v.value
            elif isinstance(v, int):
                self.rgb = v
            else:
                self.value = Color.parse(v)
            self.opacity = float(args[1])
        elif arglen == 3:
            r = args[0]
            g = args[1]
            b = args[2]
            self.value = Color.rgb_to_int(r, g, b)
        elif arglen == 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)
        if "red" in kwargs:
            self.red = kwargs["red"]
        if "green" in kwargs:
            self.green = kwargs["green"]
        if "blue" in kwargs:
            self.blue = kwargs["blue"]
        if "alpha" in kwargs:
            self.alpha = kwargs["alpha"]
        if "opacity" in kwargs:
            self.opacity = kwargs["opacity"]
        if "r" in kwargs:
            self.red = kwargs["r"]
        if "g" in kwargs:
            self.green = kwargs["g"]
        if "b" in kwargs:
            self.blue = kwargs["b"]
        if "rgb" in kwargs:
            self.rgb = kwargs["rgb"]
        if "bgr" in kwargs:
            self.bgr = kwargs["bgr"]
        if "argb" in kwargs:
            self.argb = kwargs["argb"]
        if "rgba" in kwargs:
            self.rgba = kwargs["rgba"]
        if "h" in kwargs:
            self.hue = kwargs["h"]
        if "s" in kwargs:
            self.saturation = kwargs["s"]
        if "l" in kwargs:
            self.lightness = kwargs["l"]
        if "hue" in kwargs:
            self.hue = kwargs["hue"]
        if "saturation" in kwargs:
            self.saturation = kwargs["saturation"]
        if "lightness" in kwargs:
            self.lightness = kwargs["lightness"]

    def __int__(self):
        return self.value

    def __str__(self):
        if self.value is None:
            return str(self.value)
        return self.hex

    def __repr__(self):
        return "Color('%s')" % str(self)

    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
        if first is None:
            return second is None
        if second is None:
            return first is None
        return first & 0xFFFFFFFF == second & 0xFFFFFFFF

    def __ne__(self, other):
        return not self == other

    def __abs__(self):
        # Return opaque color.
        if self.value is None:
            return Color(self.value)
        return Color(self.red, self.green, self.blue)

    @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 = int(round(opacity * 255.0))
        a = Color.crimp(a)
        r <<= 24
        g <<= 16
        b <<= 8
        c = r | g | b | a
        return c

    @staticmethod
    def hsl_to_int(h, s, l, opacity=1.0):
        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))
        value = Color.rgb_to_int(r, g, b, opacity=opacity)
        return 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)
        try:
            return int(v)
        except ValueError:
            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}FF".format(h[:6])
            v = int(s, 16)
            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}FF".format(h[0], h[1], h[2])
            v = int(s, 16)
            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)

    @classmethod
    def distinct(cls, index):
        """
        Produces a deterministic distinct color for the given index.
        """

        def _pattern(pattern: int):
            n = int(pattern ** (1.0 / 3.0))
            pattern -= n * n * n
            p = [n] * 3
            if pattern == 0:
                return p
            pattern -= 1

            v = int(pattern % 3)
            pattern = int(pattern // 3)
            if pattern < n:
                p[v] = pattern % n
                return p
            pattern -= n

            p[v] = pattern // n
            v += 1
            p[v % 3] = pattern % n
            return p

        def _8bit_reverse(r: int):
            value = r - 1
            v = 0
            for i in range(0, 8):
                v = v | (value & 1)
                v <<= 1
                value >>= 1
            v >>= 1
            return v & 0xFF

        p = _pattern(index)
        return Color(
            _8bit_reverse(p[0]),
            _8bit_reverse(p[1]),
            _8bit_reverse(p[2]),
        )

    @property
    def rgb(self):
        if self.value is None:
            return None
        return self.value >> 8

    @rgb.setter
    def rgb(self, rgb):
        rgb <<= 8
        rgb |= 0xFF
        self.value = rgb

    @property
    def bgr(self):
        if self.value is None:
            return None
        return self.blue << 16 | self.green << 8 | self.red

    @bgr.setter
    def bgr(self, bgr):
        self.value = 0
        self.alpha = 0xFF
        self.red = bgr & 0xFF
        self.green = (bgr >> 8) & 0xFF
        self.blue = (bgr >> 16) & 0xFF

    @property
    def rgba(self):
        return self.value

    @rgba.setter
    def rgba(self, rgba):
        self.value = rgba

    @property
    def argb(self):
        if self.value is None:
            return None
        return ((self.value >> 8) & 0xFFFFFF) | (self.alpha << 24)

    @argb.setter
    def argb(self, argb):
        self.value = ((argb << 8) & 0xFFFFFF00) | (argb >> 24 & 0xFF)

    @property
    def opacity(self):
        return self.alpha / 255.0 if self.value is not None else None

    @opacity.setter
    def opacity(self, opacity):
        if self.value is None:
            raise ValueError
        a = int(round(opacity * 255.0))
        a = Color.crimp(a)
        self.alpha = a

    @property
    def alpha(self):
        return self.value & 0xFF if self.value is not None else None

    @alpha.setter
    def alpha(self, a):
        if self.value is None:
            raise ValueError
        a = Color.crimp(a)
        self.value &= ~0xFF
        self.value |= a

    @property
    def red(self):
        return (self.value >> 24) & 0xFF if self.value is not None else None

    @red.setter
    def red(self, r):
        if self.value is None:
            raise ValueError
        r = Color.crimp(r)
        self.value &= ~0xFF000000
        self.value |= r << 24

    @property
    def green(self):
        return (self.value >> 16) & 0xFF if self.value is not None else None

    @green.setter
    def green(self, g):
        if self.value is None:
            raise ValueError
        g = Color.crimp(g)
        self.value &= ~0xFF0000
        self.value |= g << 16

    @property
    def blue(self):
        return (self.value >> 8) & 0xFF if self.value is not None else None

    @blue.setter
    def blue(self, b):
        if self.value is None:
            raise ValueError
        b = Color.crimp(b)
        self.value &= ~0xFF00
        self.value |= b << 8

    @property
    def hexa(self):
        if self.value is None:
            return None
        return "#%02x%02x%02x%02x" % (self.red, self.green, self.blue, self.alpha)

    @property
    def hexrgb(self):
        if self.value is None:
            return None
        return "#%02x%02x%02x" % (self.red, self.green, self.blue)

    @property
    def hex(self):
        if self.value is None:
            return None
        if self.alpha == 0xFF:
            return self.hexrgb
        else:
            return self.hexa

    @property
    def hue(self):
        if self.value is None:
            return None
        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 Angle.turns(h).as_degrees

    @hue.setter
    def hue(self, v):
        if self.value is None:
            raise ValueError
        h, s, l = self.hsl
        self.hsl = v, s, l

    @property
    def saturation(self):
        if self.value is None:
            return None
        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):
        if self.value is None:
            raise ValueError
        h, s, l = self.hsl
        self.hsl = h, v, l

    @property
    def lightness(self):
        if self.value is None:
            return None
        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):
        if self.value is None:
            raise ValueError
        h, s, l = self.hsl
        self.hsl = h, s, v

    @property
    def intensity(self):
        if self.value is None:
            return None
        r = self.red
        g = self.green
        b = self.blue
        return (r + b + g) / 768.0

    @property
    def brightness(self):
        if self.value is None:
            return None
        r = self.red
        g = self.green
        b = self.blue
        cmax = max(r, g, b)
        return cmax / 255.0

    @property
    def blackness(self):
        if self.value is None:
            return None
        return 1.0 - self.brightness

    @property
    def luminance(self):
        if self.value is None:
            return None
        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):
        if self.value is None:
            return None
        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):
        if self.value is None:
            return None
        return self.hue, self.saturation, self.lightness

    @hsl.setter
    def hsl(self, value):
        if not isinstance(value, (tuple, list)):
            return
        h = value[0]
        s = value[1]
        l = value[2]
        self.value = Color.hsl_to_int(h, s, l, 1.0)

    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]
                self.x = float(string_x)
                self.y = float(string_y)
                return
            try:  # Try .x .y
                self.y = x.y
                self.x = x.x
                return
            except AttributeError:
                pass
            try:  # try subscription.
                self.y = x[1]
                self.x = x[0]
                return
            except (TypeError, IndexError):
                pass
            try:  # try .imag .real complex values.
                self.y = x.imag
                self.x = x.real
                return
            except AttributeError:
                # Unknown.
                raise TypeError
        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):
        if other is None:
            return False
        try:
            if not isinstance(other, Point):
                other = Point(other)
        except Exception:
            return NotImplemented
        if (
            isinstance(self.x, Length)
            or isinstance(self.y, Length)
            or isinstance(other.x, Length)
            or isinstance(other.x, Length)
        ):
            return self.x == other.x and self.y == other.y
        return abs(self.x - other.x) <= ERROR and abs(self.y - other.y) <= ERROR

    def __ne__(self, other):
        return not self == other

    def __len__(self):
        return 2

    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):
        try:
            x_str = "%.12G" % self.x
        except TypeError:
            return self.__repr__()
        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.x = v.x
            self.y = v.y
            return self
        try:
            c = complex(self) * complex(other.x, other.y)
            self.x = c.real
            self.y = c.imag
            return self
        except AttributeError:
            pass
        try:
            c = complex(self) * complex(other[0], other[1])
            self.x = c.real
            self.y = c.imag
            return self
        except (TypeError, IndexError):
            pass
        try:
            c = complex(self) * complex(other.real, other.imag)
            self.x = c.real
            self.y = c.imag
            return self
        except AttributeError:
            pass
        try:
            self.x *= other
            self.y *= other
            return self
        except Exception:
            return NotImplemented

    def __mul__(self, other):
        if isinstance(other, str):
            other = Matrix(other)
        if isinstance(other, Matrix):
            return other.point_in_matrix_space(self)
        try:
            return Point(complex(self) * complex(other.x, other.y))
        except AttributeError:
            pass
        try:
            return Point(complex(self) * complex(other[0], other[1]))
        except (TypeError, IndexError):
            pass
        try:
            return Point(complex(self) * complex(other.real, other.imag))
        except AttributeError:
            pass
        try:
            return Point(self.x * other, self.y * other)
        except Exception:
            return NotImplemented

    __rmul__ = __mul__

    def __iadd__(self, other):
        try:
            self.x += other.x
            self.y += other.y
            return self
        except AttributeError:
            pass
        try:
            self.y += other[1]
            self.x += other[0]
            return self
        except (TypeError, IndexError):
            pass
        try:
            self.x += other.real
            self.y += other.imag
            return self
        except AttributeError:
            pass
        try:
            self.x += other
            return self
        except Exception:
            return NotImplemented

    def __add__(self, other):
        try:
            x = self.x + other.x
            y = self.y + other.y
            return Point(x, y)
        except AttributeError:
            pass
        try:
            y = self.y + other[1]
            x = self.x + other[0]
            return Point(x, y)
        except (TypeError, IndexError):
            pass
        try:
            x = self.x + other.real
            y = self.y + other.imag
            return Point(x, y)
        except AttributeError:
            pass
        if isinstance(other, (float, int)):
            x = self.x + other
            return Point(x, self.y)
        return NotImplemented

    __radd__ = __add__

    def __isub__(self, other):
        try:
            self.x -= other.x
            self.y -= other.y
            return self
        except AttributeError:
            pass
        try:
            self.y -= other[1]
            self.x -= other[0]
            return self
        except (TypeError, IndexError):
            pass
        try:
            self.x -= other.real
            self.y -= other.imag
            return self
        except AttributeError:
            pass
        try:
            self.x -= other
            return self
        except Exception:
            return NotImplemented

    def __sub__(self, other):
        try:
            x = self.x - other.x
            y = self.y - other.y
            return Point(x, y)
        except AttributeError:
            pass
        try:
            y = self.y - other[1]
            x = self.x - other[0]
            return Point(x, y)
        except (TypeError, IndexError):
            pass
        try:
            x = self.x - other.real
            y = self.y - other.imag
            return Point(x, y)
        except AttributeError:
            pass
        if isinstance(other, (float, int)):
            x = self.x - other
            return Point(x, self.y)
        return NotImplemented

    def __rsub__(self, other):
        try:
            x = other.x - self.x
            y = other.y - self.y
            return Point(x, y)
        except AttributeError:
            pass
        try:
            y = other[1] - self.y
            x = other[0] - self.x
            return Point(x, y)
        except (TypeError, IndexError):
            pass
        try:
            x = other.real - self.x
            y = other.imag - self.y
            return Point(x, y)
        except AttributeError:
            pass
        if isinstance(other, (float, int)):
            x = other - self.x
            return Point(x, self.y)
        return NotImplemented

    def __complex__(self):
        return self.x + self.y * 1j

    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):
        self *= matrix
        return self

    def move_towards(self, p2, amount=1):
        if not isinstance(p2, Point):
            p2 = Point(p2)
        self += amount * (p2 - self)

    def distance_to(self, p2):
        return abs(self - p2)

    def angle_to(self, p2):
        p = p2 - self
        return Angle.radians(atan2(p.y, p.x))

    def polar_to(self, angle, distance):
        q = Point.polar(self, angle, distance)
        self.x = q.x
        self.y = q.y
        return self

    def reflected_across(self, p):
        return p + (p - self)

    @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 float(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)) == 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):
                if not m:
                    return
                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:

        :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,
        )

    @property
    def rotation(self):
        prx = Point(1, 0)
        prx *= self
        origin = Point(0, 0)
        origin *= self
        return origin.angle_to(prx)

    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 isinstance(self.e, Length):
            if width is None and relative_length is not None:
                width = relative_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):
            if height is None and relative_length is not None:
                height = relative_length
            self.f = self.f.value(
                ppi=ppi,
                relative_length=height,
                font_size=font_size,
                font_height=font_height,
                viewbox=viewbox,
            )
        return self

    @property
    def determinant(self):
        return self.a * self.d - self.c * self.b

    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 affine(cls, p1, p2, p4):
        """
        Create a matrix which transforms these three ordered points to the clockwise points of the unit-square.

        @param p1:
        @param p2:
        @param p4:
        @return:
        """
        x1, y1 = p1
        x2, y2 = p2
        x4, y4 = p4

        a = x4 - x1
        b = x2 - x1
        c = x1
        d = y4 - y1
        e = y2 - y1
        f = y1
        return cls(a, d, b, e, c, f)

    @classmethod
    def perspective(cls, p1, p2, p3, p4):
        """
        Create a matrix which transforms these four ordered points to the clockwise points of the unit-square.

        If G and H are very close to 0, this is an affine transformation. If they are not, then the perspective
        transform requires g and h, but we do not support non-affine transformations.

        @param p1:
        @param p2:
        @param p3:
        @param p4:
        @return:
        """
        x1, y1 = p1
        x2, y2 = p2
        x3, y3 = p3
        x4, y4 = p4

        i = 1
        try:
            j = (y1 - y2 + y3 - y4) / (y2 - y3)
            k = (x1 - x2 + x3 - x4) / (x4 - x3)
            m = (y4 - y3) / (y2 - y3)
            n = (x2 - x3) / (x4 - x3)

            h = i * (j - k * m) / (1 - m * n)
            g = i * (k - j * n) / (1 - m * n)
        except ZeroDivisionError:
            h = 0.0
            g = 0.0

        c = x1 * i
        f = y1 * i
        a = x4 * (g + i) - x1 * i
        b = x2 * (h + i) - x1 * i
        d = y4 * (g + i) - y1 * i
        e = y2 * (h + i) - y1 * i

        return cls(a, d, b, e, c, f)

    @classmethod
    def map(cls, p1, p2, p3, p4, p5, p6, p7, p8):
        """
        Create a matrix which transforms these four ordered points to equivalent points for the other 4 ordered points.

        If G and H are very close to 0, this is an affine transformation. If they are not, then the perspective
        transform requires g and h, but we do not support non-affine transformations.

        @param p1:
        @param p2:
        @param p3:
        @param p4:
        @return:
        """
        m1 = Matrix.perspective(p1, p2, p3, p4)
        m2 = Matrix.perspective(p5, p6, p7, p8)
        return cls(~m1 * m2)

    @classmethod
    def map3(cls, p1, p2, p4, p5, p6, p8):
        """
        Create a matrix which transforms these three ordered points to map to 3 other ordered points. This does not
        include p3 or p7 for the ordered squares.

        @param p1:
        @param p2:
        @param p4:
        @param p5:
        @param p6:
        @param p8:
        @return:
        """
        m1 = Matrix.affine(p1, p2, p4)
        m2 = Matrix.affine(p5, p6, p8)
        return cls(~m1 * m2)

    @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 m: matrix operand
        :param s: matrix operand
        :return: multiplied 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 Viewbox:
    def __init__(self, *args, **kwargs):
        """
        Viewbox controls the scaling between the drawing size view that is observing that drawing.

        :param viewbox: either values or viewbox attribute or a Viewbox object
        :param preserveAspectRatio or preserve_aspect_ratio: preserveAspectRatio
        """
        self.x = None
        self.y = None
        self.width = None
        self.height = None
        self.preserve_aspect_ratio = None
        if args and len(args) <= 2:
            viewbox = args[0]
            if isinstance(viewbox, dict):
                self.property_by_values(viewbox)
            elif isinstance(viewbox, Viewbox):
                self.property_by_object(viewbox)
            else:
                self.set_viewbox(viewbox)
            if len(args) == 2:
                self.preserve_aspect_ratio = args[1]
        elif len(args) == 4:
            self.x = float(args[0])
            self.y = float(args[1])
            self.width = float(args[2])
            self.height = float(args[3])
        if kwargs:
            self.property_by_values(dict(kwargs))

    def __eq__(self, other):
        if not isinstance(other, Viewbox):
            return False
        if self.x != other.x:
            return False
        if self.y != other.y:
            return False
        if self.width != other.width:
            return False
        if self.height != other.height:
            return False
        return self.preserve_aspect_ratio == other.preserve_aspect_ratio

    def __str__(self):
        return "%s %s %s %s" % (
            Length.str(self.x),
            Length.str(self.y),
            Length.str(self.width),
            Length.str(self.height),
        )

    def __repr__(self):
        values = []
        if self.x is not None:
            values.append("%s=%s" % (SVG_ATTR_X, Length.str(self.x)))
        if self.y is not None:
            values.append("%s=%s" % (SVG_ATTR_Y, Length.str(self.y)))
        if self.width is not None:
            values.append("%s=%s" % (SVG_ATTR_WIDTH, Length.str(self.width)))
        if self.height is not None:
            values.append("%s=%s" % (SVG_ATTR_HEIGHT, Length.str(self.height)))
        if self.preserve_aspect_ratio is not None:
            values.append(
                "%s='%s'" % (SVG_ATTR_PRESERVEASPECTRATIO, self.preserve_aspect_ratio)
            )
        params = ", ".join(values)
        return "Viewbox(%s)" % params

    def property_by_object(self, obj):
        self.x = obj.x
        self.y = obj.y
        self.width = obj.width
        self.height = obj.height
        self.preserve_aspect_ratio = obj.preserve_aspect_ratio

    def property_by_values(self, values):
        if SVG_ATTR_VIEWBOX in values:
            self.set_viewbox(values[SVG_ATTR_VIEWBOX])
        if SVG_ATTR_X in values:
            self.x = values[SVG_ATTR_X]
        if SVG_ATTR_Y in values:
            self.y = values[SVG_ATTR_Y]
        if SVG_ATTR_WIDTH in values:
            self.width = values[SVG_ATTR_WIDTH]
        if SVG_ATTR_HEIGHT in values:
            self.height = values[SVG_ATTR_HEIGHT]
        if "preserve_aspect_ratio" in values:
            self.preserve_aspect_ratio = values["preserve_aspect_ratio"]
        if SVG_ATTR_PRESERVEASPECTRATIO in values:
            self.preserve_aspect_ratio = values[SVG_ATTR_PRESERVEASPECTRATIO]

    def set_viewbox(self, viewbox):
        if viewbox is not None:
            dims = list(REGEX_FLOAT.findall(viewbox))
            try:
                self.x = float(dims[0])
                self.y = float(dims[1])
                self.width = float(dims[2])
                self.height = float(dims[3])
            except IndexError:
                pass

    def transform(self, element):
        return Viewbox.viewbox_transform(
            element.x,
            element.y,
            element.width,
            element.height,
            self.x,
            self.y,
            self.width,
            self.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.

        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.

        :param e_x: element_x value
        :param e_y: element_y value
        :param e_width: element_width value
        :param e_height: element_height value
        :param vb_x: viewbox_x value
        :param vb_y: viewbox_y value
        :param vb_width: viewbox_width value
        :param vb_height: viewbox_height value
        :param aspect: preserve aspect ratio value
        :return: string of the SVG transform commands to account for the viewbox.
        """
        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 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)

    def render(self, **kwargs):
        """
        Render changes any length/percent values or attributes into real usable limits if
        given the information required to change such parameters.

        :param kwargs: various other properties to be rendered with.
        :return:
        """
        pass

    def set(self, key, value):
        self.values[key] = value
        return self

    def write_xml(self, f, **kwargs):
        write(self, f, **kwargs)

    def string_xml(self):
        return tostring(self)


class Transformable:
    """Any element that is transformable and has a transform property."""

    def __init__(self, *args, **kwargs):
        self._length = None
        self._lengths = None
        self.transform = None
        self.apply = None

    def property_by_object(self, s):
        self.transform = Matrix(s.transform)
        self.apply = s.apply

    def property_by_values(self, values):
        self.transform = Matrix(values.get(SVG_ATTR_TRANSFORM, ""))
        self.apply = bool(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.
        """
        self._lengths = None
        self._length = None

    def render(self, **kwargs):
        """
        Renders the transformable by performing any required length conversion operations into pixels. The element
        will be the pixel-length form.
        """
        if self.transform is not None:
            self.transform.render(**kwargs)
        return self

    def bbox(self, transformed=True, with_stroke=False):
        """
        Returns the bounding box of the given object.

        :param transformed: whether this is the transformed bounds or default.
        :param with_stroke: should the stroke-width be included in the bounds.
        :return: bounding box of the given element
        """
        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:
    """Any drawn element."""

    def __init__(self, *args, **kwargs):
        self.stroke = None
        self.fill = None
        self.stroke_width = None

    def property_by_object(self, s):
        self.fill = Color(s.fill) if s.fill is not None else None
        self.stroke = Color(s.stroke) if s.stroke is not None else None
        self.stroke_width = (
            Length(s.stroke_width).value() if s.stroke_width is not None else None
        )

    def property_by_values(self, values):
        stroke = values.get(SVG_ATTR_STROKE)
        self.stroke = Color(stroke) if stroke is not None else None
        stroke_opacity = values.get("stroke_opacity")
        stroke_opacity = values.get(SVG_ATTR_STROKE_OPACITY, stroke_opacity)
        if (
            stroke_opacity is not None
            and self.stroke is not None
            and self.stroke.value is not None
        ):
            try:
                self.stroke.opacity = float(stroke_opacity)
            except ValueError:
                pass
        fill = values.get(SVG_ATTR_FILL)
        self.fill = Color(fill) if fill is not None else None
        fill_opacity = values.get("fill_opacity")
        fill_opacity = values.get(SVG_ATTR_FILL_OPACITY, fill_opacity)
        if (
            fill_opacity is not None
            and self.fill is not None
            and self.fill.value is not None
        ):
            try:
                self.fill.opacity = float(fill_opacity)
            except ValueError:
                pass
        self.stroke_width = Length(values.get("stroke_width", 1.0)).value()
        self.stroke_width = Length(
            values.get(SVG_ATTR_STROKE_WIDTH, self.stroke_width)
        ).value()

    def render(self, **kwargs):
        if isinstance(self.stroke_width, Length):
            width = kwargs.get("width", kwargs.get("relative_length"))
            height = kwargs.get("height", kwargs.get("relative_length"))
            try:
                del kwargs["relative_length"]
            except KeyError:
                pass
            self.stroke_width = self.stroke_width.value(
                relative_length=sqrt(width * width + height * height), **kwargs
            )
            # A percentage stroke_width is always computed as a percentage of the normalized viewBox diagonal length.

    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.
        """
        self.stroke_width = self.implicit_stroke_width
        return self

    @property
    def implicit_stroke_width(self):
        try:
            if not self.apply:
                return self.stroke_width
            if self.stroke_width is not None:
                transform = self.transform
                if (
                    hasattr(self, "values")
                    and SVG_ATTR_VECTOR_EFFECT in self.values
                    and SVG_VALUE_NON_SCALING_STROKE
                    in self.values[SVG_ATTR_VECTOR_EFFECT]
                ):
                    # If we do not apply scaling stroke we still apply viewport transform.
                    transform = Matrix(self.values.get("viewport_transform", ""))
                width = self.stroke_width
                det = transform.determinant
                return width * sqrt(abs(det))
        except AttributeError:
            return self.stroke_width

    def is_degenerate(self):
        return False


class Shape(SVGElement, 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)
    stroke_width: Stroke width of the stroke. (GraphicObject)
    """

    def __init__(self, *args, **kwargs):
        self._strict = True
        Transformable.__init__(self, *args, **kwargs)
        GraphicObject.__init__(self, *args, **kwargs)
        SVGElement.__init__(
            self, *args, **kwargs
        )  # Must go last, triggers, by_object, by_value, by_arg functions.

    def property_by_object(self, s):
        SVGElement.property_by_object(self, s)
        Transformable.property_by_object(self, s)
        GraphicObject.property_by_object(self, s)

    def property_by_values(self, values):
        SVGElement.property_by_values(self, values)
        Transformable.property_by_values(self, values)
        GraphicObject.property_by_values(self, values)

    def render(self, **kwargs):
        SVGElement.render(self, **kwargs)
        Transformable.render(self, **kwargs)
        GraphicObject.render(self, **kwargs)

    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 _calc_lengths(self, error=ERROR, min_depth=MIN_DEPTH, segments=None):
        """
        Calculate the length values for the segments of the Shape.

        :param error: error permitted for length calculations.
        :param min_depth: minimum depth for the length calculation.
        :param segments: optional segments to use.
        :return:
        """
        if segments is None:
            segments = self.segments(False)
        if self._length is not None:
            return
        lengths = [each.length(error=error, min_depth=min_depth) for each in segments]
        self._length = sum(lengths)
        if self._length == 0:
            self._lengths = lengths
        else:
            self._lengths = [each / self._length for each in lengths]

    def npoint(self, positions, error=ERROR):
        """
        Find a points between 0 and 1 within the shape. Numpy acceleration allows points to be an array of floats.
        """
        try:
            import numpy as np
        except ImportError:
            return [self.point(pos) for pos in positions]

        segments = self.segments(False)
        if len(segments) == 0:
            return None
        # Shortcuts
        if self._length is None:
            self._calc_lengths(error=error, segments=segments)
        xy = np.empty((len(positions), 2), dtype=float)
        if self._length == 0:
            # all segments have 0 length
            seg_points = np.empty((len(segments), 2), dtype=float)
            for i, seg in enumerate(segments):
                seg_points[i] = seg.point(0)
            indexes = np.round(positions * (len(segments) - 1)).astype(int)
            xy[:] = seg_points[indexes]
            return xy

        # Find which segment the point we search for is located on:
        segment_start = 0
        for index, segment in enumerate(segments):
            segment_end = segment_start + self._lengths[index]
            position_subset = (segment_start <= positions) & (positions < segment_end)
            v0 = positions[position_subset]
            if not len(v0):
                segment_start = segment_end
                continue  # Nothing matched.
            d = segment_end - segment_start
            if d == 0:  # This segment is 0 length.
                segment_pos = 0.0
            else:
                segment_pos = (v0 - segment_start) / d
            c = segment.npoint(segment_pos)
            xy[position_subset] = c[:]
            segment_start = segment_end

        # the loop above will miss position == 1
        xy[positions == 1] = np.array(list(segments[-1].end))
        return xy

    def point(self, position, error=ERROR):
        """
        Find a point between 0 and 1 within the Shape, going through the shape with regard to position.

        :param position: value between 0 and 1 within the shape.
        :param error: Length error permitted.
        :return: Point at the given location.
        """
        segments = self.segments(False)
        if len(segments) == 0:
            return None
        # Shortcuts
        try:
            if position <= 0.0:
                return segments[0].point(position)
            if position >= 1.0:
                return segments[-1].point(position)
        except ValueError:
            return self.npoint([position], error=error)[0]

        if self._length is None:
            self._calc_lengths(error=error, segments=segments)

        if self._length == 0:
            i = int(round(position * (len(segments) - 1)))
            return segments[i].point(0.0)
        # Find which segment the point we search for is located on:
        segment_start = 0
        segment_pos = 0
        segment = segments[0]
        for index, segment in enumerate(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 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, with_stroke=False):
        """
        Get the bounding box for the given shape.

        :param transformed: whether this is the transformed bounds or default.
        :param with_stroke: should the stroke-width be included in the bounds.
        :return: bounding box of the given element
        """
        bbs = [
            seg.bbox()
            for seg in self.segments(transformed=transformed)
            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.

        if (
            with_stroke
            and self.stroke_width is not None
            and not (self.stroke is None or self.stroke.value is None)
        ):
            if transformed:
                delta = float(self.implicit_stroke_width) / 2.0
            else:
                delta = float(self.stroke_width) / 2.0
        else:
            delta = 0.0

        return (
            min(xmins) - delta,
            min(ymins) - delta,
            max(xmaxs) + delta,
            max(ymaxs) + delta,
        )

    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.
        """
        # Cannot use SVG_ATTR_* for some attributes in repr because they contain hyphens
        if self.stroke is not None and self.stroke.rgb is not None:
            values.append("%s='%s'" % (SVG_ATTR_STROKE, self.stroke.hexrgb))
            if self.stroke.opacity != 1.0:
                values.append("%s=%s" % ("stroke_opacity", str(self.stroke.opacity)))
        if self.fill is not None and self.fill.rgb is not None:
            values.append("%s='%s'" % (SVG_ATTR_FILL, self.fill.hexrgb))
            if self.fill.opacity != 1.0:
                values.append("%s=%s" % ("fill_opacity", str(self.fill.opacity)))
        if self.stroke_width is not None and self.stroke_width != 1.0:
            values.append("stroke_width=%s" % str(self.stroke_width))
        if not self.transform.is_identity():
            values.append("%s=%s" % (SVG_ATTR_TRANSFORM, repr(self.transform)))
        if self.apply is not None and not self.apply:
            values.append("apply=%s" % self.apply)
        if self.id is not None:
            values.append("%s='%s'" % (SVG_ATTR_ID, self.id))

    def _str_shape(self, values):
        """
        Generic pieces of str shape.
        """
        if self.stroke is not None and self.stroke.rgb is not None:
            values.append("%s='%s'" % (SVG_ATTR_STROKE, self.stroke.hexrgb))
            if self.stroke.opacity != 1.0:
                values.append(
                    "%s=%s" % (SVG_ATTR_STROKE_OPACITY, str(self.stroke.opacity))
                )
        if self.fill is not None and self.fill.rgb is not None:
            values.append("%s='%s'" % (SVG_ATTR_FILL, self.fill.hexrgb))
            if self.fill.opacity != 1.0:
                values.append("%s=%s" % (SVG_ATTR_FILL_OPACITY, str(self.fill.opacity)))
        if self.stroke_width is not None and self.stroke_width != 1.0:
            values.append("%s=%s" % (SVG_ATTR_STROKE_WIDTH, str(self.stroke_width)))
        if not self.transform.is_identity():
            values.append("%s=%s" % (SVG_ATTR_TRANSFORM, repr(self.transform)))
        if self.apply is not None and not self.apply:
            values.append("apply=%s" % self.apply)
        if self.id is not None:
            values.append("%s='%s'" % (SVG_ATTR_ID, 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
    Bézier curves, quadratic Bézier 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, **kwargs):
        try:
            self.relative = bool(kwargs["relative"])
        except (KeyError, ValueError):
            self.relative = False
        try:
            self.smooth = bool(kwargs["smooth"])
        except (KeyError, ValueError):
            self.smooth = True
        self.start = None
        self.end = None

    def __repr__(self):
        values = []
        s = self.start
        if s is not None:
            values.append("start=%s" % repr(s))
        e = self.end
        if e is not None:
            values.append("end=%s" % repr(e))
        return "%s(%s)" % (self.__class__.__name__, ", ".join(values))

    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):
        """
        This defines an individual path segment string. Since this isn't part of a Path it appends a pseudo-Move
        command to correctly provide the starting position.
        :return: string representation of the object.
        """
        d = self.d()
        if self.start is not None:
            if self.relative:
                return "m %s %s" % (self.start, d)
            else:
                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.0
        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.x for p in self if p is not None]
        ys = [p.y 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):
        """
        Reverses the current path segment.
        """
        end = self.end
        self.end = self.start
        self.start = end

    def point(self, position):
        """
        Returns the point at a given amount through the path segment.
        :param position:  t value between 0 and 1
        :return: Point instance
        """
        return Point(self.npoint([position])[0])

    def npoint(self, positions):
        """
        Returns the points at given positions along the path segment
        :param positions: N-sized sequence of t value between 0 and 1
        :return: N-sized sequence of 2-sized sequence of float
        """
        return [self.end] * len(positions)

    def length(self, error=ERROR, min_depth=MIN_DEPTH):
        """
        Returns the length of this path segment.

        :param error:
        :param min_depth:
        :return:
        """
        return 0

    def d(self, current_point=None, relative=None, smooth=None):
        """Returns the fragment path_d value for the current path segment.

        For a relative segment the current_point must be provided. If it is omitted then only an absolute segment
        can be returned."""
        raise NotImplementedError

    def intersect(self, segment):
        return list(PathSegment.find_intersections(self, segment))

    @staticmethod
    def find_intersections(
        segment1,
        segment2,
        samples=50,
        ta=(0.0, 1.0, None),
        tb=(0.0, 1.0, None),
        depth=0,
        enhancements=2,
        enhance_samples=50,
    ):
        """
        Calculate intersections by linearized polyline intersections with enhancements.
        We calculate probable intersections by linearizing our segment into `sample` polylines
        we then find those intersecting segments and the range of t where those intersections
        could have occurred and then subdivide those segments in a series of enhancements to
        find their intersections with increased precision.

        This code is fast, but it could fail by both finding a rare phantom intersection (if there
        is a low or no enhancements) or by failing to find a real intersection. Because the polylines
        approximation did not intersect in the base case.

        At a resolution of about 1e-15 the intersection calculations become unstable and intersection
        candidates can duplicate or become lost. We terminate at that point and give the last best
        guess.

        :param segment1:
        :param segment2:
        :param samples:
        :param ta:
        :param tb:
        :param depth:
        :param enhancements:
        :param enhance_samples:
        :return:
        """
        assert samples >= 2

        import numpy as np

        a = np.linspace(ta[0], ta[1], num=samples)
        b = np.linspace(tb[0], tb[1], num=samples)
        step_a = a[1] - a[0]
        step_b = b[1] - b[0]
        j = segment1.npoint(a)
        k = segment2.npoint(b)
        j = j[:, 0] + j[:, 1] * 1j
        k = k[:, 0] + k[:, 1] * 1j

        ax1, bx1 = np.meshgrid(np.real(j[:-1]), np.real(k[:-1]))
        ax2, bx2 = np.meshgrid(np.real(j[1:]), np.real(k[1:]))
        ay1, by1 = np.meshgrid(np.imag(j[:-1]), np.imag(k[:-1]))
        ay2, by2 = np.meshgrid(np.imag(j[1:]), np.imag(k[1:]))

        denom = (by2 - by1) * (ax2 - ax1) - (bx2 - bx1) * (ay2 - ay1)
        qa = (bx2 - bx1) * (ay1 - by1) - (by2 - by1) * (ax1 - bx1)
        qb = (ax2 - ax1) * (ay1 - by1) - (ay2 - ay1) * (ax1 - bx1)
        hits = np.dstack(
            (
                denom != 0,  # Cannot be parallel.
                np.sign(denom) == np.sign(qa),  # D and Qa must have same sign.
                np.sign(denom) == np.sign(qb),  # D and Qb must have same sign.
                abs(denom) >= abs(qa),  # D >= Qa (else not between 0 - 1)
                abs(denom) >= abs(qb),  # D >= Qb (else not between 0 - 1)
            )
        ).all(axis=2)

        where_hit = np.argwhere(hits)
        if len(where_hit) != 1 and step_a < 1e-10:
            # We're hits are becoming unstable give last best value.
            if ta[2] is not None and tb[2] is not None:
                yield ta[2], tb[2]
            return

        # Calculate the t values for the intersections
        ta_hit = qa[hits] / denom[hits]
        tb_hit = qb[hits] / denom[hits]

        for i, hit in enumerate(where_hit):

            at = ta[0] + float(hit[1]) * step_a  # Zoomed min+segment intersected.
            bt = tb[0] + float(hit[0]) * step_b
            a_fractional = (
                ta_hit[i] * step_a
            )  # Fractional guess within intersected segment
            b_fractional = tb_hit[i] * step_b
            if depth == enhancements:
                # We've enhanced as best as we can, yield the current + segment t-value to our answer
                yield at + a_fractional, bt + b_fractional
            else:
                yield from PathSegment.find_intersections(
                    segment1,
                    segment2,
                    ta=(at, at + step_a, at + a_fractional),
                    tb=(bt, bt + step_b, bt + b_fractional),
                    samples=enhance_samples,
                    depth=depth + 1,
                    enhancements=enhancements,
                    enhance_samples=enhance_samples,
                )


class Move(PathSegment):
    """Represents move commands. Moves to a new location without any path distance.
    Paths that consist of only move commands, are valid.

    Move serve to make discontinuous paths into continuous linked paths segments
    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.

        For many Move commands it is not necessary to have an original start location. The start point provides a
        linked locations for some elements that may require it. If known it can be provided.

        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, **kwargs)
        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 __copy__(self):
        return Move(self.start, self.end, relative=self.relative)

    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 bbox(self):
        """Return the bounding box for a Move which is the end point."""
        return self.end.x, self.end.y, self.end.x, self.end.y

    def d(self, current_point=None, relative=None, smooth=None):
        if (
            current_point is None
            or (relative is None and self.relative)
            or (relative is not None and not relative)
        ):
            return "M %s" % self.end
        return "m %s" % (self.end - current_point)


class Curve(PathSegment):
    """Represents curve commands"""

    def __init__(self, start=None, end=None, **kwargs):
        PathSegment.__init__(self, **kwargs)
        self.start = Point(start) if start is not None else None
        self.end = Point(end) if end is not None else None

    def as_circular_arcs(self, error=ERROR, start=0.0, end=1.0):
        mid = 0.5 * (end - start) + start
        start_point = self.point(start)
        mid_point = self.point(mid)
        end_point = self.point(end)
        sample_point0 = self.point(0.25 * (end - start) + start)
        sample_point1 = self.point(0.75 * (end - start) + start)
        arc = Arc(start=start_point, end=end_point, control=mid_point)
        radius = Point.distance(start_point, arc.center)
        r0 = Point.distance(sample_point0, arc.center)
        r1 = Point.distance(sample_point1, arc.center)
        current_error = (abs(r0 - radius) + abs(r1 - radius)) / radius
        if current_error < error:
            yield arc
        else:
            yield from self.as_circular_arcs(error=error, start=start, end=mid)
            yield from self.as_circular_arcs(error=error, start=mid, end=end)


class Linear(PathSegment):
    """Represents line commands."""

    def __init__(self, start=None, end=None, **kwargs):
        PathSegment.__init__(self, **kwargs)
        self.start = Point(start) if start is not None else None
        self.end = Point(end) if end is not None else None

    def __copy__(self):
        return self.__class__(self.start, self.end, relative=self.relative)

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return NotImplemented
        return self.start == other.start and self.end == other.end

    def __ne__(self, other):
        if not isinstance(other, self.__class__):
            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 npoint(self, positions):
        try:
            import numpy as np

            xy = np.empty(shape=(len(positions), 2), dtype=float)
            xy[:, 0] = np.interp(positions, [0, 1], [self.start.x, self.end.x])
            xy[:, 1] = np.interp(positions, [0, 1], [self.start.y, self.end.y])
            return xy
        except ImportError:
            return [Point.towards(self.start, self.end, pos) for pos in positions]

    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 closest_segment_point(self, p, respect_bounds=True):
        """Gives the point on the line closest to the given point."""
        a = self.start
        b = self.end
        v_ap_x = p[0] - a.x
        v_ap_y = p[1] - a.y
        v_ab_x = b.x - a.x
        v_ab_y = b.y - a.y
        sq_distance_ab = v_ab_x * v_ab_x + v_ab_y * v_ab_y
        ab_ap_product = v_ab_x * v_ap_x + v_ab_y * v_ap_y
        if sq_distance_ab == 0:
            return 0  # Line is point.
        amount = ab_ap_product / float(sq_distance_ab)
        if respect_bounds:
            if amount > 1:
                amount = 1
            if amount < 0:
                amount = 0
        return self.point(amount)

    def d(self, current_point=None, relative=None, smooth=None):
        raise NotImplementedError


class Close(Linear):
    """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 multipart shapes
    which can close or not close several times.
    """

    def d(self, current_point=None, relative=None, smooth=None):
        if (
            current_point is None
            or (relative is None and self.relative)
            or (relative is not None and not relative)
        ):
            return "Z"
        else:
            return "z"


class Line(Linear):
    """Represents line commands."""

    def d(self, current_point=None, relative=None, smooth=None):
        if (
            current_point is None
            or (relative is None and self.relative)
            or (relative is not None and not relative)
        ):
            return "L %s" % self.end
        else:
            return "l %s" % (self.end - current_point)


class QuadraticBezier(Curve):
    """Represents Quadratic Bézier commands."""

    def __init__(self, start, control, end, **kwargs):
        Curve.__init__(self, start, end, **kwargs)
        self.control = Point(control) if control is not None else None

    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,
            relative=self.relative,
            smooth=self.smooth,
        )

    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 npoint(self, positions):
        """Calculate the x,y position at a certain position of the path. `pos` may be a
        float or a NumPy array."""
        x0, y0 = self.start
        x1, y1 = self.control
        x2, y2 = self.end

        def _compute_point(position):
            # compute factors
            n_pos = 1 - position
            pos_2 = position * position
            n_pos_2 = n_pos * n_pos
            n_pos_pos = n_pos * position

            return (
                n_pos_2 * x0 + 2 * n_pos_pos * x1 + pos_2 * x2,
                n_pos_2 * y0 + 2 * n_pos_pos * y1 + pos_2 * y2,
            )

        try:
            import numpy as np

            xy = np.empty(shape=(len(positions), 2))
            xy[:, 0], xy[:, 1] = _compute_point(np.array(positions))
            return xy
        except ImportError:
            return [Point(*_compute_point(position)) for position in positions]

    def bbox(self):
        """
        Returns the bounding box for the quadratic Bézier curve.
        """
        n = self.start.x - self.control.x
        d = self.start.x - 2 * self.control.x + self.end.x
        if d != 0:
            t = n / float(d)
        else:
            t = 0.5
        if 0 < t < 1:
            x_values = [self.start.x, self.end.x, self.point(t).x]
        else:
            x_values = [self.start.x, self.end.x]
        n = self.start.y - self.control.y
        d = self.start.y - 2 * self.control.y + self.end.y
        if d != 0:
            t = n / float(d)
        else:
            t = 0.5
        if 0 < t < 1:
            y_values = [self.start.y, self.end.y, self.point(t).y]
        else:
            y_values = [self.start.y, self.end.y]
        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 * a.real + a.imag * a.imag)
            B = 4 * (a.real * b.real + a.imag * b.imag)
            C = b.real * b.real + b.imag * b.imag

            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 * B) * 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 * k / 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, relative=None, smooth=None):
        if (smooth is None and self.smooth) or (smooth is not None and smooth):
            if (
                current_point is None
                or (relative is None and self.relative)
                or (relative is not None and not relative)
            ):
                return "T %s" % self.end
            else:
                return "t %s" % (self.end - current_point)
        else:
            if (
                current_point is None
                or (relative is None and self.relative)
                or (relative is not None and not relative)
            ):
                return "Q %s %s" % (self.control, self.end)
            else:
                return "q %s %s" % (
                    self.control - current_point,
                    self.end - current_point,
                )


class CubicBezier(Curve):
    """Represents Cubic Bézier commands."""

    def __init__(self, start, control1, control2, end, **kwargs):
        Curve.__init__(self, start, end, **kwargs)
        self.control1 = Point(control1) if control1 is not None else None
        self.control2 = Point(control2) if control1 is not None else None

    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,
            relative=self.relative,
            smooth=self.smooth,
        )

    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 npoint(self, positions):
        """Calculate the x,y position at a certain position of the path. `pos` may be a
        float or a NumPy array."""
        x0, y0 = self.start
        x1, y1 = self.control1
        x2, y2 = self.control2
        x3, y3 = self.end

        def _compute_point(position):
            # compute factors
            pos_3 = position * position * position
            n_pos = 1 - position
            n_pos_3 = n_pos * n_pos * n_pos
            pos_2_n_pos = position * position * n_pos
            n_pos_2_pos = n_pos * n_pos * position
            return (
                n_pos_3 * x0 + 3 * (n_pos_2_pos * x1 + pos_2_n_pos * x2) + pos_3 * x3,
                n_pos_3 * y0 + 3 * (n_pos_2_pos * y1 + pos_2_n_pos * y2) + pos_3 * y3,
            )

        try:
            import numpy as np

            xy = np.empty(shape=(len(positions), 2))
            xy[:, 0], xy[:, 1] = _compute_point(np.array(positions))
            return xy
        except ImportError:
            return [Point(*_compute_point(position)) for position in positions]

    def bbox(self):
        """returns the tight-fitting bounding box of the Bézier 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-8:
            delta = (
                a[1] * a[1] - (a[0] + a[1]) * a[2] + a[2] * a[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:
            c = a[1] - a[0]
            b = 2 * (a[0] - 2 * a[1] + a[2])
            if b != 0:
                r0 = -c / b
                if 0 < r0 < 1:
                    local_extremizers.append(r0)
        local_extrema = [self.point(t)[v] for t in local_extremizers]
        return min(local_extrema), max(local_extrema)

    def _length_scipy(self, error=ERROR):
        from scipy.integrate import quad

        p0 = complex(*self.start)
        p1 = complex(*self.control1)
        p2 = complex(*self.control2)
        p3 = complex(*self.end)

        def _abs_derivative(t):
            return abs(
                3 * (p1 - p0) * (1 - t) ** 2
                + 6 * (p2 - p1) * (1 - t) * t
                + 3 * (p3 - p2) * t**2
            )

        return quad(_abs_derivative, 0.0, 1.0, epsabs=error, limit=1000)[0]

    def _length_default(self, error=ERROR, min_depth=MIN_DEPTH):
        return self._line_length(0, 1, error, min_depth)

    def length(self, error=ERROR, min_depth=MIN_DEPTH):
        """Calculate the length of the path up to a certain position"""
        try:
            return self._length_scipy(error)
        except:  # Fallback on any failure
            return self._length_default(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, relative=None, smooth=None):
        if (smooth is None and self.smooth) or (smooth is not None and smooth):
            if (
                current_point is None
                or (relative is None and self.relative)
                or (relative is not None and not relative)
            ):
                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
                or (relative is None and self.relative)
                or (relative is not None and not relative)
            ):
                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(Curve):
    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 elliptical 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.

        start_t + sweep = end_t
        """
        Curve.__init__(self, **kwargs)
        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.x + rx, self.center.y)
            self.pry = Point(self.center.x, self.center.y + 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.0, 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
                ab_mid = Point.towards(self.start, control, 0.5)
                bc_mid = Point.towards(control, self.end, 0.5)
                if self.start == self.end:
                    cx = ab_mid.x
                    cy = ab_mid.y
                    self.sweep = tau
                else:
                    if abs(delta_a.x) > 1e-12:
                        slope_a = delta_a.y / delta_a.x
                    else:
                        slope_a = float("inf")
                    if abs(delta_b.x) > 1e-12:
                        slope_b = delta_b.y / delta_b.x
                    else:
                        slope_b = float("inf")
                    if abs(delta_a.y) < 1e-12:  # slope_a == 0
                        cx = ab_mid.x
                        if abs(delta_b.x) < 1e-12:  # slope_b == inf
                            cy = bc_mid.y
                        else:
                            if abs(slope_b) > 1e-12:
                                cy = bc_mid.y + (bc_mid.x - cx) / slope_b
                            else:
                                cy = float("inf")
                    elif abs(delta_b.y) < 1e-12:  # slope_b == 0
                        cx = bc_mid.x
                        if abs(delta_a.y) < 1e-12:  # slope_a == inf
                            cy = ab_mid.y
                        else:
                            cy = ab_mid.y + (ab_mid.x - cx) / slope_a
                    elif abs(delta_a.x) < 1e-12:  # slope_a == inf
                        cy = ab_mid.y
                        cx = slope_b * (bc_mid.y - cy) + bc_mid.x
                    elif abs(delta_b.x) < 1e-12:  # slope_b == inf
                        cy = bc_mid.y
                        cx = slope_a * (ab_mid.y - cy) + ab_mid.x
                    elif abs(slope_a - slope_b) < 1e-12:
                        cx = ab_mid.x
                        cy = ab_mid.y
                    else:
                        cx = (
                            slope_a * slope_b * (ab_mid.y - bc_mid.y)
                            - slope_a * bc_mid.x
                            + slope_b * ab_mid.x
                        ) / (slope_b - slope_a)
                        cy = ab_mid.y - (cx - ab_mid.x) / 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.x + self.end.x) / 2.0, (self.start.y + self.end.y) / 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.x + sqrt(r * r - hq * hq) * (self.start.y - self.end.y) / q,
                    mid.y + sqrt(r * r - hq * hq) * (self.end.x - self.start.x) / 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.x - sqrt(r * r - hq * hq) * (self.start.y - self.end.y) / q,
                        mid.y - sqrt(r * r - hq * hq) * (self.end.x - self.start.x) / 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.x - self.end.x)
                self.sweep = tau / 4.0
                self.center = Point(self.start.x, self.end.y)
                cw = bool(Point.orientation(self.start, self.center, self.end) == 1)
                if "scooped" in kwargs and kwargs["scooped"]:
                    self.sweep = -self.sweep
                    cw = not cw
                if ("ccw" in kwargs and kwargs["ccw"] and cw) or not cw:
                    self.center = Point(self.end.x, self.start.y)

        if self.center is None:
            raise ValueError("Not enough values to solve for center.")
        if "r" in kwargs:
            r = kwargs["r"]
            if self.prx is None:
                self.prx = Point(self.center.x + r, self.center.y)
            if self.pry is None:
                self.pry = Point(self.center.x, self.center.y + 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.x + rx, self.center.y)
        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.x, self.center.y + 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.x + radius_s, self.center.y)
            self.pry = Point(self.center.x, self.center.y + 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.x + radius_e, self.center.y)
            self.pry = Point(self.center.x, self.center.y + 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(start=%s, end=%s, center=%s, prx=%s, pry=%s, sweep=%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,
            relative=self.relative,
        )

    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.determinant < 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 npoint(self, positions):
        try:
            import numpy as np

            return self._points_numpy(np.array(positions))
        except ImportError:
            if self.start == self.end and self.sweep == 0:
                # This is equivalent of omitting the segment
                return [self.start] * len(positions)

            start_t = self.get_start_t()
            return [
                self.start
                if pos == 0
                else self.end
                if pos == 1
                else self.point_at_t(start_t + self.sweep * pos)
                for pos in positions
            ]

    def _points_numpy(self, positions):
        """Vectorized version of `point()`.

        :param positions: 1D numpy array of float in [0, 1]
        :return: 1D numpy array of complex
        """
        import numpy as np

        xy = np.empty((len(positions), 2), dtype=float)

        if self.start == self.end and self.sweep == 0:
            xy[:, 0], xy[:, 1] = self.start
        else:
            t = self.get_start_t() + self.sweep * positions

            rotation = self.get_rotation()
            a = self.rx
            b = self.ry
            cx = self.center.x
            cy = self.center.y
            cos_rot = cos(rotation)
            sin_rot = sin(rotation)
            cos_t = np.cos(t)
            sin_t = np.sin(t)
            xy[:, 0] = cx + a * cos_t * cos_rot - b * sin_t * sin_rot
            xy[:, 1] = cy + a * cos_t * sin_rot + b * sin_t * cos_rot

            # ensure clean endings
            xy[positions == 0, :] = list(self.start)
            xy[positions == 1, :] = list(self.end)

        return xy

    def _integral_length(self):
        def ellipse_part_integral(t1, t2, a, b, n=100000):
            # function to integrate
            def f(t):
                sint = sin(t)
                return sqrt(1 - (1 - (a * a) / (b * b)) * sint * sint)

            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. .length() delegates to here and on failure uses the
        fallback method."""
        from scipy.special import ellipeinc

        a = self.rx
        b = self.ry
        adb = a / b
        m = 1 - adb * adb
        phi = self.get_start_t()
        d1 = ellipeinc(phi, m)
        phi = phi + self.sweep
        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 Bézier 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:  # Fallback on any failure
            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.x, center.y
        )

        self.center = center
        self.prx = Point(center.x + rx, center.y)
        self.pry = Point(center.x, center.y + 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, arc_required=None):
        if arc_required is None:
            sweep_limit = tau / 12.0
            arc_required = int(ceil(abs(self.sweep) / sweep_limit))
        if arc_required == 0:
            return
        t_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.x
        cy = self.center.y

        for i in range(0, arc_required):
            next_t = current_t + 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(t_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, arc_required=None):
        if arc_required is None:
            sweep_limit = tau / 12.0
            arc_required = int(ceil(abs(self.sweep) / sweep_limit))
        if arc_required == 0:
            return
        t_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.x
        y0 = self.center.y
        cos_theta = cos(theta)
        sin_theta = sin(theta)

        for i in range(0, arc_required):
            next_t = current_t + t_slice

            alpha = sin(t_slice) * (sqrt(4 + 3 * pow(tan(t_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 from 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.0
        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.0
        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.x
        cy = self.center.y
        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 an arc.
        Code from: https://github.com/mathandy/svgpathtools
        """
        if self.sweep == 0:
            return self.start.x, self.start.y, self.end.x, self.end.y
        phi = self.get_rotation().as_radians
        if cos(phi) == 0:
            atan_x = tau / 4.0
            atan_y = 0
        elif sin(phi) == 0:
            atan_x = 0
            atan_y = tau / 4.0
        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 + (tau / 2.0) * k) * (360 / tau) - self.theta) / self.delta

        xtrema = [self.start.x, self.end.x]
        ytrema = [self.start.y, self.end.y]

        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).x)
            if 0 <= ty <= 1:
                ytrema.append(self.point(ty).y)

        return min(xtrema), min(ytrema), max(xtrema), max(ytrema)

    def d(self, current_point=None, relative=None, smooth=None):
        if (
            current_point is None
            or (relative is None and self.relative)
            or (relative is not None and not relative)
        ):
            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.

    Instantiation formats:

    Path("d-string", keywords)
    Path(pathsegment1,...)
    Path(d="d-string", other keywords)
    """

    def __init__(self, *args, **kwargs):
        Shape.__init__(self, *args, **kwargs)
        self._length = None
        self._lengths = None
        self._segments = list()
        if len(args) != 1:
            for segment in args:
                if not isinstance(segment, PathSegment):
                    raise ValueError(
                        "Object not PathSegment when instantiating a Path: %s"
                        % segment.__class__.__name__
                    )
            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:
            # Not sure what the purpose of pathd_loaded is.
            # It is only set and checked here, and you cannot have "d" attribute more than once anyway
            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."""
        if index < 0 or index + 1 >= len(self._segments):
            return  # This connection doesn't exist.
        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 self._segments[0].end is not None else None
        )
        # If move is never found, just the end point of the first element. Unless that's not a thing.

    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
        an 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.segments()
            if isinstance(index, int):
                if len(new_element) > 1:
                    raise ValueError  # Cannot insert multiple items into a single space. Requires slice.
                new_element = new_element[0]
        self._segments[index] = new_element
        self._length = None
        self._lengths = None
        if isinstance(index, slice):
            self.validate_connections()
        else:
            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
        if isinstance(index, slice):
            self.validate_connections()
        else:
            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):
        if isinstance(other, (str, Path, Subpath, Shape, PathSegment)):
            n = copy(self)
            n += other
            return n
        return NotImplemented

    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)
        return "%s(%s)" % (self.__class__.__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
        if p.stroke_width != q.stroke_width:
            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 = SVGLexicalParser()
        tokens.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

    def _is_valid(self):
        """
        Checks validation of all connections.

        Paths are valid if all end points match the start of the next point and all close
        commands return to the last valid move command.

        This does not check for incongruent path validity. Path fragments without initial moves
        double closed paths, may all pass this check.
        """
        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:
                    return False
                elif last_segment.end is None:
                    return False
                elif last_segment.end != segment.start:
                    return False
            if (
                isinstance(segment, Close)
                and zpoint is not None
                and segment.end != zpoint
            ):
                return False
            last_segment = segment
        return True

    @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) if self._segments[0].end is not None else None
        )

    @property
    def current_point(self):
        if len(self._segments) == 0:
            return None
        return (
            Point(self._segments[-1].end)
            if self._segments[-1].end is not None
            else None
        )

    @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.
        In regard 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, relative=False, **kwargs):
        start_pos = self.current_point
        end_pos = points[0]
        if end_pos in ("z", "Z"):
            end_pos = self.z_point
        self.append(Move(start_pos, end_pos, relative=relative))
        if len(points) > 1:
            self.line(*points[1:], relative=relative)
        return self

    def line(self, *points, relative=False, **kwargs):
        for index in range(len(points)):
            start_pos = self.current_point
            end_pos = points[index]
            if end_pos in ("z", "Z"):
                end_pos = self.z_point
            self.append(Line(start_pos, end_pos, relative=relative))
        return self

    def vertical(self, *y_points, relative=False, **kwargs):
        for index in range(len(y_points)):
            start_pos = self.current_point
            if relative:
                self.append(
                    Line(
                        start_pos,
                        Point(start_pos.x, start_pos.y + y_points[index]),
                        relative=relative,
                    )
                )
            else:
                self.append(
                    Line(
                        start_pos,
                        Point(start_pos.x, y_points[index]),
                        relative=relative,
                    )
                )
        return self

    def horizontal(self, *x_points, relative=False, **kwargs):
        for index in range(len(x_points)):
            start_pos = self.current_point
            if relative:
                self.append(
                    Line(
                        start_pos,
                        Point(start_pos.x + x_points[index], start_pos.y),
                        relative=relative,
                    )
                )
            else:
                self.append(
                    Line(
                        start_pos,
                        Point(x_points[index], start_pos.y),
                        relative=relative,
                    )
                )
        return self

    def smooth_quad(self, *points, relative=False, **kwargs):
        """Smooth curve. First control point is the "reflection" of
        the second control point in the previous path."""
        for index in range(len(points)):
            start_pos = self.current_point
            control1 = self.smooth_point
            end_pos = points[index]
            if end_pos in ("z", "Z"):
                end_pos = self.z_point
            self.append(
                QuadraticBezier(
                    start_pos, control1, end_pos, relative=relative, smooth=True
                )
            )
        return self

    def quad(self, *points, relative=False, **kwargs):
        for index in range(0, len(points), 2):
            start_pos = self.current_point
            control = points[index]
            if control in ("z", "Z"):
                control = self.z_point
                self.append(
                    QuadraticBezier(
                        start_pos, control, control, relative=relative, smooth=False
                    )
                )
                return self
            end_pos = points[index + 1]
            if end_pos in ("z", "Z"):
                end_pos = self.z_point
            self.append(
                QuadraticBezier(
                    start_pos, control, end_pos, relative=relative, smooth=False
                )
            )
        return self

    def smooth_cubic(self, *points, relative=False, **kwargs):
        """Smooth curve. First control point is the "reflection" of
        the second control point in the previous path."""
        for index in range(0, len(points), 2):
            start_pos = self.current_point
            control1 = self.smooth_point
            control2 = points[index]

            if control2 in ("z", "Z"):
                control2 = self.z_point
                self.append(
                    CubicBezier(
                        start_pos,
                        control1,
                        control2,
                        control2,
                        relative=relative,
                        smooth=True,
                    )
                )
                return self
            end_pos = points[index + 1]
            if end_pos in ("z", "Z"):
                end_pos = self.z_point
            self.append(
                CubicBezier(
                    start_pos,
                    control1,
                    control2,
                    end_pos,
                    relative=relative,
                    smooth=True,
                )
            )
        return self

    def cubic(self, *points, relative=False, **kwargs):
        for index in range(0, len(points), 3):
            start_pos = self.current_point
            control1 = points[index]
            if control1 in ("z", "Z"):
                control1 = self.z_point
                self.append(
                    CubicBezier(
                        start_pos,
                        control1,
                        control1,
                        control1,
                        relative=relative,
                        smooth=True,
                    )
                )
                return self
            control2 = points[index + 1]
            if control2 in ("z", "Z"):
                control2 = self.z_point
                self.append(
                    CubicBezier(
                        start_pos,
                        control1,
                        control2,
                        control2,
                        relative=relative,
                        smooth=True,
                    )
                )
                return self
            end_pos = points[index + 2]
            if end_pos in ("z", "Z"):
                end_pos = self.z_point
            self.append(
                CubicBezier(
                    start_pos,
                    control1,
                    control2,
                    end_pos,
                    relative=relative,
                    smooth=False,
                )
            )
        return self

    def arc(self, *arc_args, relative=False, **kwargs):
        for index in range(0, len(arc_args), 6):
            start_pos = self.current_point
            rx = arc_args[index]
            ry = arc_args[index + 1]
            if rx < 0:
                rx = abs(rx)
            if ry < 0:
                ry = abs(ry)
            rotation = arc_args[index + 2]
            arc = arc_args[index + 3]
            sweep = arc_args[index + 4]
            end_pos = arc_args[index + 5]
            if end_pos in ("z", "Z"):
                end_pos = self.z_point
            self.append(
                Arc(start_pos, rx, ry, rotation, arc, sweep, end_pos, relative=relative)
            )
        return self

    def closed(self, relative=False):
        start_pos = self.current_point
        end_pos = self.z_point
        segment = Close(start_pos, end_pos)
        segment.relative = relative
        self.append(segment)
        return self

    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 direct_close(self):
        """Forces close operations to be zero length by introducing a direct
        line to operation just before any non-zero length close.

        This is helpful because for some operations like reverse() because the
        close must be located at the very end of the path sequence. But, if it's
        in effect a line-to and close, the line-to would need to start the sequence.

        But, for some operations this won't matter since it will still result in
        a closed shape with reversed ordering. But, if the final point in the
        sequence must exactly switch with the first point in the sequence. The
        close segments must be direct and zero length.
        """
        if len(self._segments) == 0:
            return
        for i in range(len(self._segments) - 1, -1, -1):
            segment = self._segments[i]
            if isinstance(segment, Close):
                if segment.length() != 0:
                    line = Line(segment.start, segment.end)
                    segment.start = Point(segment.end)
                    self.insert(i, line)
        return self

    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):
        start = 0
        for current, seg in enumerate(self):
            if isinstance(seg, Move):
                if current != start:
                    yield Subpath(self, start, current - 1)
                    start = current
            if isinstance(seg, Close):
                yield Subpath(self, start, current)
                start = current + 1
        if start != len(self):
            yield Subpath(self, start, 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.
        """
        GraphicObject.reify(self)
        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=None, smooth=None):
        if len(segments) == 0:
            return ""
        parts = []
        previous_segment = None
        p = Point(0)
        if smooth is None:
            override_smooth = False
            smooth_set_value = True
        else:
            override_smooth = True
            smooth_set_value = bool(smooth)
        if relative is not None:
            for segment in segments:
                if isinstance(segment, (Move, Line, Arc, Close)):
                    parts.append(segment.d(p, relative=relative))
                elif isinstance(segment, (CubicBezier, QuadraticBezier)):
                    if (override_smooth and smooth_set_value) or (
                        not override_smooth and segment.smooth
                    ):
                        parts.append(
                            segment.d(
                                p,
                                relative=relative,
                                smooth=segment.is_smooth_from(previous_segment),
                            )
                        )
                    else:
                        parts.append(segment.d(p, relative=relative, smooth=False))
                previous_segment = segment
                p = previous_segment.end
        else:
            for segment in segments:
                if isinstance(segment, (Move, Line, Arc, Close)):
                    parts.append(segment.d(p, relative=segment.relative))
                elif isinstance(segment, (CubicBezier, QuadraticBezier)):
                    if (override_smooth and smooth_set_value) or (
                        not override_smooth and segment.smooth
                    ):
                        parts.append(
                            segment.d(
                                p,
                                relative=segment.relative,
                                smooth=segment.is_smooth_from(previous_segment),
                            )
                        )
                    else:
                        parts.append(
                            segment.d(p, relative=segment.relative, smooth=False)
                        )
                previous_segment = segment
                p = previous_segment.end
        return " ".join(parts)

    def d(self, relative=None, transformed=True, smooth=None):
        path = self
        if transformed:
            path = abs(path)
        return Path.svg_d(path._segments, relative=relative, smooth=smooth)

    def segments(self, transformed=True):
        if transformed and not self.transform.is_identity():
            return [s * self.transform for s in self._segments]
        return self._segments

    def approximate_arcs_with_cubics(self, error=0.1):
        """
        Iterates through this path and replaces any Arcs with cubic Bézier curves.
        """
        sweep_limit = tau * error
        for s in range(len(self) - 1, -1, -1):
            segment = self[s]
            if isinstance(segment, Arc):
                arc_required = int(ceil(abs(segment.sweep) / sweep_limit))
                self[s : s + 1] = list(segment.as_cubic_curves(arc_required))

    def approximate_arcs_with_quads(self, error=0.1):
        """
        Iterates through this path and replaces any Arcs with quadratic Bézier curves.
        """
        sweep_limit = tau * error
        for s in range(len(self) - 1, -1, -1):
            segment = self[s]
            if isinstance(segment, Arc):
                arc_required = int(ceil(abs(segment.sweep) / sweep_limit))
                self[s : s + 1] = list(segment.as_quad_curves(arc_required))

    def approximate_bezier_with_circular_arcs(self, error=0.01):
        """
        Iterates through this path and replaces any Bézier curves with circular arcs.
        """
        for s in range(len(self) - 1, -1, -1):
            segment = self[s]
            if isinstance(segment, (QuadraticBezier, CubicBezier)):
                self[s : s + 1] = list(segment.as_circular_arcs(error=error))


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:
            try:
                rx = min(rx, self.width / 2.0)
                ry = min(ry, self.height / 2.0)
            except ValueError:
                pass  # If width is in inches and rx is in units, this is unsolvable without knowing the ppi
        self.rx = rx
        self.ry = ry

    def _attrs(self, values):
        if self.x != 0:
            values.append("%s=%s" % (SVG_ATTR_X, Length.str(self.x)))
        if self.y != 0:
            values.append("%s=%s" % (SVG_ATTR_Y, Length.str(self.y)))
        if self.width != 0:
            values.append("%s=%s" % (SVG_ATTR_WIDTH, Length.str(self.width)))
        if self.height != 0:
            values.append("%s=%s" % (SVG_ATTR_HEIGHT, Length.str(self.height)))
        if self.rx != 0:
            values.append("%s=%s" % (SVG_ATTR_RADIUS_X, Length.str(self.rx)))
        if self.ry != 0:
            values.append("%s=%s" % (SVG_ATTR_RADIUS_Y, Length.str(self.ry)))

    def __repr__(self):
        values = []
        self._attrs(values)
        self._repr_shape(values)
        params = ", ".join(values)
        return "Rect(%s)" % params

    def __str__(self):
        values = []
        self._attrs(values)
        self._str_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.
        """
        scooped = False
        x = self.x
        y = self.y
        width = self.width
        height = self.height
        if self.is_degenerate():
            return ()  # a computed value of zero for either dimension disables rendering.
        rx = self.rx
        ry = self.ry
        if not self._strict:
            if rx < 0 < width and ry < 0 < height:
                scooped = True
                rx = abs(rx)
                ry = abs(ry)
        if rx < 0 < width or ry < 0 < height:
            rx = 0
            ry = 0
        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,
                    scooped=scooped,
                ),
                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,
                    scooped=scooped,
                ),
                Line((x + width - rx, y + height), (x + rx, y + height)),
                Arc(
                    (x + rx, y + height),
                    (x, y + height - ry),
                    rx=rx,
                    ry=ry,
                    scooped=scooped,
                ),
                Line((x, y + height - ry), (x, y + ry)),
                Arc((x, y + ry), (x + rx, y), rx=rx, ry=ry, scooped=scooped),
                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.
        """
        scale_x = self.transform.value_scale_x()
        scale_y = self.transform.value_scale_y()
        if scale_x * scale_y < 0:
            return self  # No reification of negative values, gives negative dims.
        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
        ):
            GraphicObject.reify(self)
            Transformable.reify(self)
            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, **kwargs):
        Shape.render(self, **kwargs)
        width = kwargs.get("width", kwargs.get("relative_length"))
        height = kwargs.get("height", kwargs.get("relative_length"))
        try:
            del kwargs["relative_length"]
        except KeyError:
            pass
        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

    def is_degenerate(self):
        return (
            self.width == 0
            or self.height == 0
            or self.width is None
            or self.height is None
        )


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 _attrs(self, values):
        if self.cx is not None:
            values.append("%s=%s" % (SVG_ATTR_CENTER_X, Length.str(self.cx)))
        if self.cy is not None:
            values.append("%s=%s" % (SVG_ATTR_CENTER_Y, Length.str(self.cy)))
        if self.rx == self.ry or self.ry is None:
            values.append("%s=%s" % (SVG_ATTR_RADIUS, Length.str(self.rx)))
        else:
            values.append("%s=%s" % (SVG_ATTR_RADIUS_X, Length.str(self.rx)))
            values.append("%s=%s" % (SVG_ATTR_RADIUS_Y, Length.str(self.ry)))

    def __repr__(self):
        values = []
        self._attrs(values)
        self._repr_shape(values)
        params = ", ".join(values)
        return "%s(%s)" % (self.__class__.__name__, params)

    def __str__(self):
        values = []
        self._attrs(values)
        self._str_shape(values)
        params = ", ".join(values)
        return "%s(%s)" % (self.__class__.__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
        if (
            transformed
            and self.transform.value_scale_x() * self.transform.value_scale_y() < 0
        ):
            step_size = -step_size
        t_start = 0
        t_end = step_size
        # zero for either dimension, or a computed value of auto for both dimensions, disables rendering of the element.
        rx = self.implicit_rx
        ry = self.implicit_ry
        if self.is_degenerate():
            return ()
        center = self.implicit_center
        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),
                center,
                rx=rx,
                ry=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.
        """
        scale_x = self.transform.value_scale_x()
        scale_y = self.transform.value_scale_y()
        if scale_y * scale_x < 0:
            return self  # No reification of flipped values.
        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
        ):
            GraphicObject.reify(self)
            Transformable.reify(self)
            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, **kwargs):
        Shape.render(self, **kwargs)
        width = kwargs.get("width", kwargs.get("relative_length"))
        height = kwargs.get("height", kwargs.get("relative_length"))
        try:
            del kwargs["relative_length"]
        except KeyError:
            pass
        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 is_degenerate(self):
        rx = self.implicit_rx
        ry = self.implicit_ry
        return rx == 0 or ry == 0

    def unit_matrix(self):
        """
        return the unit matrix which 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.x, center.y)
        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, ccw=None):
        """
        return the arc found between the given angles on the ellipse.

        :param a0: start angle
        :param a1: end angle
        :param ccw: optional flag to force clockwise or counter-clockwise arc-angles, default is smaller angle
        :return: arc
        """
        if ccw is None:
            ccw = a0 > a1
        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=ccw,
        )

    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 from 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.0
        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.0
        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.x
        cy = center.y
        cos_theta = cos(rotation)
        sin_theta = sin(rotation)
        cos_t = cos(t)
        sin_t = sin(t)
        px = cx + a * cos_t * cos_theta - b * sin_t * sin_theta
        py = cy + a * cos_t * sin_theta + b * sin_t * cos_theta
        return Point(px, py)

    def point(self, position, error=ERROR):
        """
        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: position value between 0,1 where value equals the amount through the shape
        :param error: error permitted in determining point value (unused for this shape)
        :return: point at t
        """
        return self.point_at_t(tau * position)

    def _ramanujan_length(self):
        a = self.implicit_rx
        b = self.implicit_ry
        if b > a:
            a, b = b, a
        h = ((a - b) * (a - b)) / ((a + b) * (a + b))
        return tau / 2 * (a + b) * (1 + (3 * h / (10 + sqrt(4 - 3 * h))))


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 _attrs(self, values):
        if self.x1 is not None:
            values.append("%s=%s" % (SVG_ATTR_X1, str(self.x1)))
        if self.y1 is not None:
            values.append("%s=%s" % (SVG_ATTR_Y1, str(self.y1)))
        if self.x2 is not None:
            values.append("%s=%s" % (SVG_ATTR_X2, str(self.x2)))
        if self.y2 is not None:
            values.append("%s=%s" % (SVG_ATTR_Y2, str(self.y2)))

    def __repr__(self):
        values = []
        self._attrs(values)
        self._repr_shape(values)
        params = ", ".join(values)
        return "SimpleLine(%s)" % params

    def __str__(self):
        values = []
        self._attrs(values)
        self._str_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.x

    @property
    def implicit_y1(self):
        point = Point(self.x1, self.y1)
        point *= self.transform
        return point.y

    @property
    def implicit_x2(self):
        point = Point(self.x2, self.y2)
        point *= self.transform
        return point.x

    @property
    def implicit_y2(self):
        point = Point(self.x2, self.y2)
        point *= self.transform
        return point.y

    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.
        """
        GraphicObject.reify(self)
        Transformable.reify(self)
        matrix = self.transform
        p = Point(self.x1, self.y1)
        p *= matrix
        self.x1 = p.x
        self.y1 = p.y

        p = Point(self.x2, self.y2)
        p *= matrix
        self.x2 = p.x
        self.y2 = p.y

        matrix.reset()
        return self

    def render(self, **kwargs):
        Shape.render(self, **kwargs)
        width = kwargs.get("width", kwargs.get("relative_length"))
        height = kwargs.get("height", kwargs.get("relative_length"))
        try:
            del kwargs["relative_length"]
        except KeyError:
            pass
        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 and isinstance(points[0], (list, tuple, str)):
                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("%s='%s'" % (SVG_ATTR_POINTS, s))
        self._repr_shape(values)
        params = ", ".join(values)
        return "%s(%s)" % (self.__class__.__name__, params)

    def __str__(self):
        values = []
        if self.points is not None:
            s = " ".join(map(str, self.points))
            values.append("%s='%s'" % (SVG_ATTR_POINTS, s))
        self._str_shape(values)
        params = ", ".join(values)
        return "%s(%s)" % (self.__class__.__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 self.is_degenerate():
            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.
        """
        GraphicObject.reify(self)
        Transformable.reify(self)
        matrix = self.transform
        for p in self:
            p *= matrix
        matrix.reset()
        return self

    def is_degenerate(self):
        return len(self.points) == 0


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):
        if isinstance(other, (str, Path, PathSegment)):
            n = copy(self)
            n += other
            return n
        return NotImplemented

    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
        return NotImplemented

    __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 _numeric_index(self, index):
        if index < 0:
            return self._end + index + 1
        else:
            return self._start + index

    def index_to_path_index(self, index):
        if isinstance(index, slice):
            start = index.start
            stop = index.stop
            step = index.step
            if start is None:
                start = 0
            start = self._numeric_index(start)
            if stop is None:
                stop = len(self)
            stop = self._numeric_index(stop)
            return slice(start, stop, step)
        return self._numeric_index(index)

    def bbox(self, transformed=True, with_stroke=False):
        """returns a bounding box for the subpath"""
        if transformed:
            return Path(self).bbox(transformed=transformed, with_stroke=with_stroke)

        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.

        if (
            with_stroke
            and self._path.stroke_width is not None
            and not (self._path.stroke is None or self._path.stroke.value is None)
        ):
            delta = float(self._path.stroke_width) / 2.0
        else:
            delta = 0.0

        return (
            min(xmins) - delta,
            min(ymins) - delta,
            max(xmaxs) + delta,
            max(ymaxs) + delta,
        )

    def d(self, relative=None, smooth=None):
        segments = self._path._segments[self._start : self._end + 1]
        return Path.svg_d(segments, relative=relative, smooth=smooth)

    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 Group(SVGElement, Transformable, list):
    """
    Group Container element can have children.
    SVG 2.0 <g> are defined in:
    5.2. Grouping: the g element
    """

    def __init__(self, *args, **kwargs):
        Transformable.__init__(self, *args, **kwargs)
        list.__init__(self)
        if len(args) >= 1:
            s = args[0]
            if isinstance(s, Group):
                self.extend(list(map(copy, s)))
        SVGElement.__init__(self, *args, **kwargs)

    def __imul__(self, other):
        if isinstance(other, str):
            other = Matrix(other)
        if isinstance(other, Matrix):
            self.transform *= other
            for e in self:
                e *= other
        return self

    def render(self, **kwargs):
        Transformable.render(self, **kwargs)

    def __copy__(self):
        return Group(self)

    def property_by_object(self, s):
        Transformable.property_by_object(self, s)
        SVGElement.property_by_object(self, s)

    def property_by_values(self, values):
        Transformable.property_by_values(self, values)
        SVGElement.property_by_values(self, values)

    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 to include or False if exclude
        """
        if conditional is None:
            for subitem in self:
                yield subitem
                if isinstance(subitem, (Group, Use)):
                    for s in subitem.select(conditional):
                        yield s
        else:
            for subitem in self:
                if conditional(subitem):
                    yield subitem
                if isinstance(subitem, (Group, Use)):
                    for s in subitem.select(conditional):
                        yield s

    def reify(self):
        Transformable.reify(self)

    @staticmethod
    def union_bbox(elements, transformed=True, with_stroke=False):
        """
        Returns the union of the bounding boxes for the elements within the iterator.

        :param transformed: Should the children of this object be properly transformed.
        :param with_stroke: should the stroke-width be included in the bounds of the elements
        :return: union of all bounding boxes of elements within the iterable.
        """
        boxes = []
        for e in elements:
            if not hasattr(e, "bbox") or isinstance(e, (Group, Use)):
                continue
            box = e.bbox(transformed=transformed, with_stroke=with_stroke)
            if box is None:
                continue
            boxes.append(box)
        if len(boxes) == 0:
            return None
        (xmins, ymins, xmaxs, ymaxs) = zip(*boxes)
        return min(xmins), min(ymins), max(xmaxs), max(ymaxs)

    def bbox(self, transformed=True, with_stroke=False):
        """
        Returns the bounding box of the given object.

        In the case of groups this is the union of all the bounding boxes of all bound children.

        Setting transformed to false, may yield unexpected results if subitems are transformed in non-uniform
        ways.

        :param transformed: bounding box of the properly transformed children.
        :param with_stroke: should the stroke-width be included in the bounds.
        :return: bounding box of the given element
        """
        return Group.union_bbox(
            self.select(),
            transformed=transformed,
            with_stroke=with_stroke,
        )


class Use(SVGElement, Transformable, list):
    """
    Use elements are defined in svg 5.6
    https://www.w3.org/TR/SVG11/struct.html#UseElement

    Use is a generic container object group-like that use reference objects,
    """

    def __init__(self, *args, **kwargs):
        list.__init__(self)
        self.x = None
        self.y = None
        self.width = None
        self.height = None
        Transformable.__init__(self, *args, **kwargs)
        SVGElement.__init__(
            self, *args, **kwargs
        )  # Must go last, triggers, by_object, by_value, by_arg functions.

    def property_by_object(self, s):
        SVGElement.property_by_object(self, s)
        Transformable.property_by_object(self, s)
        self.x = s.x
        self.y = s.y
        self.width = s.width
        self.height = s.height

    def 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()
        if self.x != 0 or self.y != 0:
            # If x or y is set, apply this to transform
            try:
                values[SVG_ATTR_TRANSFORM] = "%s translate(%s, %s)" % (
                    values[SVG_ATTR_TRANSFORM],
                    self.x,
                    self.y,
                )
            except KeyError:
                values[SVG_ATTR_TRANSFORM] = "translate(%s, %s)" % (
                    self.x,
                    self.y,
                )
        SVGElement.property_by_values(self, values)
        Transformable.property_by_values(self, values)

    def render(self, **kwargs):
        SVGElement.render(self, **kwargs)
        Transformable.render(self, **kwargs)

    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 to include or False if exclude
        """
        if conditional is None:
            for subitem in self:
                yield subitem
                if isinstance(subitem, (Group, Use)):
                    for s in subitem.select(conditional):
                        yield s
        else:
            for subitem in self:
                if conditional(subitem):
                    yield subitem
                if isinstance(subitem, (Group, Use)):
                    for s in subitem.select(conditional):
                        yield s

    @staticmethod
    def union_bbox(elements, transformed=True, with_stroke=False):
        """
        Returns the union of the bounding boxes for the elements within the iterator.

        :param transformed: Should the children of this object be properly transformed.
        :param with_stroke: should the stroke-width be included in the bounds of the elements
        :return: union of all bounding boxes of elements within the iterable.
        """
        boxes = []
        for e in elements:
            if not hasattr(e, "bbox") or isinstance(e, (Group, Use)):
                continue
            box = e.bbox(transformed=transformed, with_stroke=with_stroke)
            if box is None:
                continue
            boxes.append(box)
        if len(boxes) == 0:
            return None
        (xmins, ymins, xmaxs, ymaxs) = zip(*boxes)
        return min(xmins), min(ymins), max(xmaxs), max(ymaxs)

    def bbox(self, transformed=True, with_stroke=False):
        """
        Returns the bounding box of the given object.

        In the case of groups this is the union of all the bounding boxes of all bound children.

        Setting transformed to false, may yield unexpected results if subitems are transformed in non-uniform
        ways.

        :param transformed: bounding box of the properly transformed children.
        :param with_stroke: should the stroke-width be included in the bounds.
        :return: bounding box of the given element
        """
        return Use.union_bbox(
            self.select(),
            transformed=transformed,
            with_stroke=with_stroke,
        )


class ClipPath(SVGElement, list):
    """
    clipPath elements are defined in svg 14.3.5
    https://www.w3.org/TR/SVG11/masking.html#ClipPathElement

    Clip paths conceptually define a 1 bit mask for images these are usually defined within
    def blocks and do not render themselves but rather are attached by IRI references to the
    """

    def __init__(self, *args, **kwargs):
        list.__init__(self)
        self.unit_type = SVG_UNIT_TYPE_USERSPACEONUSE
        SVGElement.__init__(self, *args, **kwargs)

    def property_by_object(self, s):
        SVGElement.property_by_object(self, s)
        self.unit_type = s.unit_type

    def property_by_values(self, values):
        SVGElement.property_by_values(self, values)
        self.unit_type = self.values.get(
            SVG_ATTR_CLIP_UNIT_TYPE, SVG_UNIT_TYPE_USERSPACEONUSE
        )


class Pattern(SVGElement, list):
    def __init__(self, *args, **kwargs):
        self.viewbox = None
        self.preserve_aspect_ratio = None
        self.x = None
        self.y = None
        self.width = None
        self.height = None
        self.href = None
        self.pattern_content_units = None  # UserSpaceOnUse default
        self.pattern_transform = None
        self.pattern_units = None
        SVGElement.__init__(self, *args, **kwargs)

    def __int__(self):
        return 0

    @property
    def viewbox_transform(self):
        if self.viewbox is None:
            return ""
        return self.viewbox.transform(self)

    def property_by_object(self, s):
        SVGElement.property_by_object(self, s)
        self.viewbox = s.viewbox
        self.preserve_aspect_ratio = s.preserve_aspect_ratio

        self.x = s.x
        self.y = s.y
        self.width = s.width
        self.height = s.height
        self.href = s.href
        self.pattern_content_units = s.pattern_contents_units
        self.pattern_transform = (
            Matrix(s.pattern_transform) if s.pattern_transform is not None else None
        )
        self.pattern_units = s.pattern_units

    def property_by_values(self, values):
        SVGElement.property_by_values(self, values)
        if XLINK_HREF in values:
            self.href = values[XLINK_HREF]
        elif SVG_HREF in values:
            self.href = values[SVG_HREF]
        viewbox = values.get(SVG_ATTR_VIEWBOX)
        if viewbox is not None:
            self.viewbox = Viewbox(viewbox)
        if SVG_ATTR_PRESERVEASPECTRATIO in values:
            self.preserve_aspect_ratio = values[SVG_ATTR_PRESERVEASPECTRATIO]
        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, "100%")).value()
        self.height = Length(values.get(SVG_ATTR_HEIGHT, "100%")).value()
        if SVG_ATTR_PATTERN_CONTENT_UNITS in values:
            self.pattern_content_units = values[SVG_ATTR_PATTERN_CONTENT_UNITS]
        if SVG_ATTR_PATTERN_TRANSFORM in values:
            self.pattern_transform = Matrix(values[SVG_ATTR_PATTERN_TRANSFORM])
        if SVG_ATTR_PATTERN_UNITS in values:
            self.pattern_units = values[SVG_ATTR_PATTERN_UNITS]

    def render(self, **kwargs):
        if self.pattern_transform is not None:
            self.pattern_transform.render(**kwargs)
        width = kwargs.get("width", kwargs.get("relative_length"))
        height = kwargs.get("height", kwargs.get("relative_length"))
        try:
            del kwargs["relative_length"]
        except KeyError:
            pass
        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)
        return self


class Text(SVGElement, 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_style = "normal"
        self.font_variant = "normal"
        self.font_weight = 400
        self.font_stretch = "normal"
        self.font_size = 16.0  # 16px font 'normal' 12pt font
        self.line_height = 16.0
        self.font_family = "sans-serif"
        self.path = None

        Transformable.__init__(self, *args, **kwargs)
        GraphicObject.__init__(self, *args, **kwargs)
        SVGElement.__init__(self, *args, **kwargs)

    def __str__(self):
        values = list()
        values.append("'%s'" % self.text)
        values.append("%s='%s'" % (SVG_ATTR_FONT_FAMILY, self.font_family))
        values.append("%s='%s'" % (SVG_ATTR_FONT_STYLE, self.font_style))
        values.append("%s='%s'" % (SVG_ATTR_FONT_VARIANT, self.font_variant))
        values.append("%s='%s'" % (SVG_ATTR_FONT_WEIGHT, str(self.font_weight)))
        values.append("%s='%s'" % (SVG_ATTR_FONT_STRETCH, self.font_stretch))
        values.append("%s=%d" % (SVG_ATTR_FONT_SIZE, self.font_size))
        values.append("%s='%s'" % (SVG_ATTR_TEXT_ANCHOR, self.anchor))
        if self.x != 0 or self.y != 0:
            values.append("%s=%s" % (SVG_ATTR_X, self.x))
            values.append("%s=%s" % (SVG_ATTR_Y, self.y))
        if self.dx != 0 or self.dy != 0:
            values.append("%s=%s" % (SVG_ATTR_DX, self.dx))
            values.append("%s=%s" % (SVG_ATTR_DY, self.dy))
        if self.stroke is not None:
            values.append("%s='%s'" % (SVG_ATTR_STROKE, self.stroke))
        if self.fill is not None:
            values.append("%s='%s'" % (SVG_ATTR_FILL, self.fill))
        if self.stroke_width is not None and self.stroke_width != 1.0:
            values.append("%s=%s" % (SVG_ATTR_STROKE_WIDTH, str(self.stroke_width)))
        if not self.transform.is_identity():
            values.append("%s=%s" % (SVG_ATTR_TRANSFORM, repr(self.transform)))
        if self.id is not None:
            values.append("%s='%s'" % (SVG_ATTR_ID, self.id))
        return "Text(%s)" % (", ".join(values))

    def __repr__(self):
        # Cannot use SVG_ATTR_FONT_* or SVG_ATTR_TEXT_ANCHOR for repr because they contain hyphens
        values = list()
        values.append("'%s'" % self.text)
        values.append("font_family='%s'" % self.font_family)
        if self.font_style != "normal":
            values.append("font_style='%s'" % self.font_style)
        if self.font_variant != "normal":
            values.append("font_variant='%s'" % self.font_variant)
        values.append("font_weight='%s'" % str(self.font_weight))
        if self.font_stretch != "normal":
            values.append("font_stretch='%s'" % self.font_stretch)
        values.append("font_size=%d" % self.font_size)
        values.append("text_anchor='%s'" % self.anchor)
        if self.x != 0 or self.y != 0:
            values.append("%s=%s" % (SVG_ATTR_X, self.x))
            values.append("%s=%s" % (SVG_ATTR_Y, self.y))
        if self.dx != 0 or self.dy != 0:
            values.append("%s=%s" % (SVG_ATTR_DX, self.dx))
            values.append("%s=%s" % (SVG_ATTR_DY, self.dy))
        if self.stroke is not None:
            values.append("%s='%s'" % (SVG_ATTR_STROKE, self.stroke))
        if self.fill is not None:
            values.append("%s='%s'" % (SVG_ATTR_FILL, self.fill))
        if self.stroke_width is not None and self.stroke_width != 1.0:
            values.append(
                "stroke_width=%s" % str(self.stroke_width)
            )  # Cannot use SVG_ATTR_STROKE_WIDTH for repr because it contains a hyphen
        if not self.transform.is_identity():
            values.append("%s=%s" % (SVG_ATTR_TRANSFORM, repr(self.transform)))
        if self.id is not None:
            values.append("%s='%s'" % (SVG_ATTR_ID, self.id))
        return "Text(%s)" % (", ".join(values))

    def __eq__(self, other):
        if not isinstance(other, Text):
            return NotImplemented
        if self.text != other.text:
            return False
        if self.width != other.width:
            return False
        if self.height != other.height:
            return False
        if self.x != other.x:
            return False
        if self.y != other.y:
            return False
        if self.dx != other.dx:
            return False
        if self.dy != other.dy:
            return False
        if self.anchor != other.anchor:
            return False
        if self.font_style != other.font_style:
            return False
        if self.font_variant != other.font_variant:
            return False
        if self.font_weight != other.font_weight:
            return False
        if self.font_stretch != other.font_stretch:
            return False
        if self.font_size != other.font_size:
            return False
        if self.line_height != other.line_height:
            return False
        if self.font_family != other.font_family:
            return False
        if self.font_size != other.font_size:
            return False
        return True

    def __ne__(self, other):
        if not isinstance(other, Text):
            return NotImplemented
        return not self == other

    def property_by_object(self, s):
        Transformable.property_by_object(self, s)
        GraphicObject.property_by_object(self, s)
        SVGElement.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_style = s.font_style
        self.font_variant = s.font_variant
        self.font_weight = s.font_weight
        self.font_stretch = s.font_stretch
        self.font_size = s.font_size
        self.line_height = s.line_height

    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
        match = REGEX_CSS_FONT.match(font)
        if not match:
            # This is not a qualified shorthand font.
            return
        self.font_style = match.group(1)
        if self.font_style is None:
            self.font_style = "normal"

        self.font_variant = match.group(2)
        if self.font_variant is None:
            self.font_variant = "normal"

        self.font_weight = match.group(3)
        if self.font_weight is None:
            self.font_weight = "normal"

        self.font_stretch = match.group(4)
        if self.font_stretch is None:
            self.font_stretch = "normal"

        self.font_size = match.group(5)
        if self.font_size is None:
            self.font_size = "12pt"
        if self.font_size:
            size = self.font_size
            self.font_size = Length(self.font_size)
            try:
                self.font_size = Length(self.font_size).value()
                if self.font_size == 0:
                    self.font_size = size
            except ValueError:
                self.font_size = size

        self.line_height = match.group(6)
        if self.line_height is None:
            self.line_height = "12pt"
        if self.line_height:
            height = self.line_height
            self.line_height = Length(self.line_height)
            try:
                self.line_height = Length(
                    self.line_height, relative_length=self.font_size
                ).value()
                if self.line_height == 0:
                    self.line_height = height
            except ValueError:
                self.line_height = height

        self.font_family = match.group(7)

    @property
    def font_list(self):
        return [
            family[1:-1] if family.startswith('"') or family.startswith("'") else family
            for family in REGEX_CSS_FONT_FAMILY.findall(self.font_family)
        ]

    @property
    def weight(self):
        """
        This does not correctly parse weights for bolder or lighter. Those are relative to the previous set
        font-weight and that is generally unknown in this context.
        """
        if self.font_weight == "bold":
            return 700
        if self.font_weight == "normal":
            return 400
        try:
            return int(self.font_weight)
        except ValueError:
            return 400

    @property
    def font_face(self):
        """
        Deprecated Fontface.
        """
        return ""

    def property_by_values(self, values):
        Transformable.property_by_values(self, values)
        GraphicObject.property_by_values(self, values)
        SVGElement.property_by_values(self, values)

        self.font_family = values.get("font_family", self.font_family)
        self.font_family = values.get(SVG_ATTR_FONT_FAMILY, self.font_family)

        self.font_style = values.get("font_style", self.font_style)
        self.font_style = values.get(SVG_ATTR_FONT_STYLE, self.font_family)

        self.font_variant = values.get("font_variant", self.font_variant)
        self.font_variant = values.get(SVG_ATTR_FONT_VARIANT, self.font_variant)

        self.font_weight = values.get("font_weight", self.font_weight)
        self.font_weight = values.get(SVG_ATTR_FONT_WEIGHT, self.font_weight)
        try:
            self.font_weight = int(self.font_weight)
        except ValueError:
            pass

        self.font_stretch = values.get("font_stretch", self.font_stretch)
        self.font_stretch = values.get(SVG_ATTR_FONT_STRETCH, self.font_stretch)

        self.font_size = Length(values.get("font_size", self.font_size)).value()
        self.font_size = Length(values.get(SVG_ATTR_FONT_SIZE, self.font_size)).value()

        self.anchor = values.get("text_anchor", self.anchor)
        self.anchor = values.get(SVG_ATTR_TEXT_ANCHOR, self.anchor)

        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 reify(self):
        GraphicObject.reify(self)
        Transformable.reify(self)

    def render(self, **kwargs):
        GraphicObject.render(self, **kwargs)
        Transformable.render(self, **kwargs)
        width = kwargs.get("width", kwargs.get("relative_length"))
        height = kwargs.get("height", kwargs.get("relative_length"))
        try:
            del kwargs["relative_length"]
        except KeyError:
            pass
        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 __copy__(self):
        return Text(self)

    def bbox(self, transformed=True, with_stroke=False):
        """
        Get the bounding box for the given text object.

        :param transformed: whether this is the transformed bounds or default.
        :param with_stroke: should the stroke-width be included in the bounds.
        :return: bounding box of the given element
        """
        if self.path is not None:
            return (self.path * self.transform).bbox(
                transformed=True,
                with_stroke=with_stroke,
            )

        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])

        if (
            with_stroke
            and self.stroke_width is not None
            and not (self.stroke is None or self.stroke.value is None)
        ):
            if transformed:
                delta = float(self.implicit_stroke_width) / 2.0
            else:
                delta = float(self.stroke_width) / 2.0
        else:
            delta = 0.0

        return (
            xmin - delta,
            ymin - delta,
            xmax + delta,
            ymax + delta,
        )


SVGText = Text


class Image(SVGElement, 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.media_type = None
        self.viewbox = None
        self.preserve_aspect_ratio = None
        self.x = None
        self.y = None
        self.width = None
        self.height = None

        self.image = None
        self.image_width = None
        self.image_height = None
        Transformable.__init__(self, *args, **kwargs)
        GraphicObject.__init__(self, *args, **kwargs)
        SVGElement.__init__(
            self, *args, **kwargs
        )  # Dataurl requires this be processed first.

        if self.url is not None:
            match = REGEX_DATA_URL.match(self.url)
            if match:
                # Data URL
                self.media_type = match.group(1).split(";")
                self.data = self.url[match.end(1) + 1 :]
                if "base64" in self.media_type:
                    from base64 import b64decode

                    self.data = b64decode(self.data)
                else:
                    from urllib.parse import unquote_to_bytes

                    self.data = unquote_to_bytes(self.data)

    def __repr__(self):
        values = []
        if self.x != 0:
            values.append("%s=%s" % (SVG_ATTR_X, Length.str(self.x)))
        if self.y != 0:
            values.append("%s=%s" % (SVG_ATTR_Y, Length.str(self.y)))
        if self.width != "100%":
            values.append("%s=%s" % (SVG_ATTR_WIDTH, Length.str(self.width)))
        if self.height != "100%":
            values.append("%s=%s" % (SVG_ATTR_HEIGHT, Length.str(self.height)))
        if self.preserve_aspect_ratio is not None:
            values.append(
                "%s=%s" % (SVG_ATTR_PRESERVEASPECTRATIO, self.preserve_aspect_ratio)
            )
        if self.viewbox is not None:
            values.append("%s='%s'" % (SVG_ATTR_VIEWBOX, str(self.viewbox)))
        if self.url is not None:
            values.append("%s='%s'" % (SVG_HREF, self.url))
        if not self.transform.is_identity():
            values.append("transform=%s" % repr(self.transform))
        params = ", ".join(values)
        return "Image(%s)" % params

    def __eq__(self, other):
        if not isinstance(other, Image):
            return NotImplemented
        if self.url != other.url:
            return False
        if self.data != other.data:
            return False
        if self.width != other.width:
            return False
        if self.height != other.height:
            return False
        if self.x != other.x:
            return False
        if self.y != other.y:
            return False
        if self.image != other.image:
            return False
        if self.viewbox != other.viewbox:
            return False
        return self.preserve_aspect_ratio == other.preserve_aspect_ratio

    def __ne__(self, other):
        if not isinstance(other, Image):
            return NotImplemented
        return not self == other

    def property_by_object(self, s):
        SVGElement.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.preserve_aspect_ratio = s.preserve_aspect_ratio

        self.x = s.x
        self.y = s.y
        self.width = s.width
        self.height = s.height

        self.image = s.image
        self.image_width = s.image_width
        self.image_height = s.image_height

    def property_by_values(self, values):
        SVGElement.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]
        viewbox = values.get(SVG_ATTR_VIEWBOX)
        if viewbox is not None:
            self.viewbox = Viewbox(viewbox)
        if SVG_ATTR_PRESERVEASPECTRATIO in values:
            self.preserve_aspect_ratio = values[SVG_ATTR_PRESERVEASPECTRATIO]
        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, "100%")).value()
        self.height = Length(values.get(SVG_ATTR_HEIGHT, "100%")).value()
        if "image" in values:
            self.image = values["image"]
            self.image_width, self.image_height = self.image.size

    def render(self, **kwargs):
        GraphicObject.render(self, **kwargs)
        Transformable.render(self, **kwargs)
        SVGElement.render(self, **kwargs)
        width = kwargs.get("width", kwargs.get("relative_length"))
        height = kwargs.get("height", kwargs.get("relative_length"))
        try:
            del kwargs["relative_length"]
        except KeyError:
            pass
        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)
        return self

    def __copy__(self):
        """
        Copy of Image. 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 Image(self)

    @property
    def viewbox_transform(self):
        if self.viewbox is None:
            return ""
        return self.viewbox.transform(self)

    def load(self, directory=None):
        try:
            from PIL import Image as PILImage

            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 as PILImage

            if self.data is not None:
                from io import BytesIO

                self.image = PILImage.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 as PILImage

            if self.url is not None:
                try:
                    self.image = PILImage.open(self.url)
                except IOError:
                    try:
                        if directory is not None:
                            from os.path import join

                            relpath = join(directory, self.url)
                            self.image = PILImage.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 None:
            return  # No image to set values by.
        self.image_width = self.image.width
        self.image_height = self.image.height
        self.viewbox = Viewbox(
            "0 0 %d %d" % (self.image_width, self.image_height),
            self.preserve_aspect_ratio,
        )
        self.render(width=self.image_width, height=self.image_height)
        self.transform = Matrix(self.viewbox_transform) * self.transform

    def bbox(self, transformed=True, with_stroke=False):
        """
        Get the bounding box for the given image object

        :param transformed: whether this is the transformed bounds or default.
        :param with_stroke: There is no stroke for an image so with_stroke is ignored
        :return: bounding box of the given element
        """
        if self.image_width is None or self.image_height is None:
            p = Point(0, 0)
            p *= self.transform
            return p.x, p.y, p.x, p.y
        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.x for s in p)
        y_vals = list(s.y 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


SVGImage = Image


class Desc(SVGElement):
    def __init__(self, *args, **values):
        self.desc = None
        if values is None:
            values = dict()
        SVGElement.__init__(self, *args, **values)

    def __eq__(self, other):
        if not isinstance(other, Desc):
            return False
        return self.desc == other.desc

    def __repr__(self):
        return "%s('%s')" % (self.__class__.__name__, self.desc)

    def property_by_args(self, *args):
        if len(args) == 1:
            self.desc = args[0]

    def property_by_object(self, obj):
        SVGElement.property_by_object(self, obj)
        self.desc = obj.desc

    def property_by_values(self, values):
        SVGElement.property_by_values(self, values)
        if SVG_TAG_DESC in values:
            self.desc = values[SVG_TAG_DESC]


SVGDesc = Desc


class Title(SVGElement):
    def __init__(self, *args, **values):
        self.title = None
        if values is None:
            values = dict()
        SVGElement.__init__(self, *args, **values)

    def __eq__(self, other):
        if not isinstance(other, Title):
            return False
        return self.title == other.title

    def __repr__(self):
        return "%s('%s')" % (self.__class__.__name__, self.title)

    def property_by_args(self, *args):
        if len(args) == 1:
            self.title = args[0]

    def property_by_object(self, obj):
        SVGElement.property_by_object(self, obj)
        self.title = obj.title

    def property_by_values(self, values):
        SVGElement.property_by_values(self, values)
        if SVG_TAG_TITLE in values:
            self.title = values[SVG_TAG_TITLE]


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):
        self.objects = {}
        self.x = None
        self.y = None
        self.width = None
        self.height = None
        self.viewbox = None
        Group.__init__(self, *args, **kwargs)

    @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)

    def property_by_object(self, s):
        Group.property_by_object(self, s)
        self.x = s.x
        self.y = s.y
        self.width = s.width
        self.height = s.height
        self.viewbox = Viewbox(s.viewbox) if s.viewbox is not None else None

    def property_by_values(self, values):
        Group.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, "100%")).value()
        self.height = Length(values.get(SVG_ATTR_HEIGHT, "100%")).value()
        viewbox = values.get(SVG_ATTR_VIEWBOX)
        par = values.get(SVG_ATTR_PRESERVEASPECTRATIO)
        self.viewbox = Viewbox(viewbox, par) if viewbox is not None else None

    def get_element_by_id(self, id):
        return self.objects.get(id)

    def get_element_by_url(self, url):
        for _id in REGEX_IRI.findall(url):
            return self.get_element_by_id(_id)

    def render(self, **kwargs):
        Group.render(self, **kwargs)
        width = kwargs.get("width", kwargs.get("relative_length"))
        height = kwargs.get("height", kwargs.get("relative_length"))
        try:
            del kwargs["relative_length"]
        except KeyError:
            pass
        self.width = Length(self.width).value(relative_length=width, **kwargs)
        self.height = Length(self.height).value(relative_length=height, **kwargs)
        self.x = Length(self.x).value(relative_length=width, **kwargs)
        self.y = Length(self.y).value(relative_length=height, **kwargs)

    def elements(self, conditional=None):
        if conditional is None or conditional(self):
            yield self
        for q in self.select(conditional):
            yield q

    @property
    def viewbox_transform(self):
        if self.viewbox is None:
            return ""
        return self.viewbox.transform(self)

    @staticmethod
    def _use_structure_parse(source):
        """
        SVG structure pass: parses the svg file such that it creates the structure implied by reused objects in a
        generalized context. Objects ids are read and put into an unparsed shadow tree. <use> objects seamlessly contain
        their definitions.
        """
        event_defs = {}
        parent = None  # Define Root Node.
        children = list()

        # Preprocess iterparse tree. Store all events. Reference all tags.
        for event, elem in iterparse(source, events=("start", "end", "start-ns")):
            if event == "start":
                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.
                attributes = elem.attrib
                if SVG_ATTR_ID in attributes:  # If we have an ID, we save the node.
                    event_defs[
                        attributes[SVG_ATTR_ID]
                    ] = node  # store node value in defs.
            elif event == "end":
                parent, children = parent
            else:
                children.append((elem, None))
        nodes = children

        # End preprocess

        # Semiparse the nodes. All nodes are given in iterparse ordering with start-ns, start, and end.
        # Use values are inlined.
        def semiparse(nodes):
            for elem, children in nodes:
                if children is None:
                    yield None, "start-ns", elem
                    continue
                tag = elem.tag
                if tag.startswith("{http://www.w3.org/2000/svg"):
                    tag = tag[28:]  # Removing namespace. http://www.w3.org/2000/svg:
                yield tag, "start", elem
                yield from semiparse(children)
                if SVG_TAG_USE == tag:
                    url = None
                    semiattr = elem.attrib
                    if XLINK_HREF in semiattr:
                        url = semiattr[XLINK_HREF]
                    if SVG_HREF in semiattr:
                        url = semiattr[SVG_HREF]
                    if url is not None:
                        try:
                            yield from semiparse([event_defs[url[1:]]])
                        except KeyError:
                            pass  # Failed to find link.
                yield tag, "end", elem

        yield from semiparse(nodes)

    @staticmethod
    def parse(
        source,
        reify=True,
        ppi=DEFAULT_PPI,
        width=None,
        height=None,
        color="black",
        transform=None,
        context=None,
        parse_display_none=False,
        on_error="ignore",
    ):
        """
        Parses the SVG file. All attributes are things which the SVG document itself could not be aware of, such as
        the real size of pixels and the size of the viewport (as opposed to the viewbox).

        :param source: Source svg file or stream.
        :param reify: Should the Geometry sized or have lazy matrices.
        :param ppi: How many physical pixels per inch are there in this view.
        :param width: The physical width of the viewport
        :param height: The physical height of the viewport
        :param color: the `currentColor` value from outside the current scope.
        :param transform: Any required transformations to be pre-applied to this document
        :param context: Any existing document context.
        :param parse_display_none: Parse display_none values anyway.
        :param on_error: Error mode, "ignore", "raise", "stop"
        :return:
        """
        use = 0
        clip = 0
        root = context
        styles = {}
        stack = []

        values = {
            SVG_ATTR_COLOR: color,
            SVG_ATTR_FILL: "black",
            SVG_ATTR_STROKE: "none",
        }

        if transform is not None:
            values[SVG_ATTR_TRANSFORM] = transform

        for tag, event, elem in SVG._use_structure_parse(source):
            """
            SVG element parsing parses the job compiling any parsed elements into their compiled object forms.
            """
            if event == "start":
                stack.append((context, values))
                if (
                    not parse_display_none
                    and SVG_ATTR_DISPLAY in values
                    and values[SVG_ATTR_DISPLAY].lower() == SVG_VALUE_NONE
                ):
                    continue  # Values has a display=none. Do not render anything. No Shadow Dom.
                current_values = values
                values = {}
                values.update(current_values)  # copy of dictionary

                # 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]
                if SVG_ATTR_CLASS in values:
                    del values[SVG_ATTR_CLASS]
                if SVG_ATTR_CLIP_PATH in values:
                    del values[SVG_ATTR_CLIP_PATH]

                attributes = dict(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:
                        if len(style) != 0:
                            style += ";"
                        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:
                            if len(style) != 0:
                                style += ";"
                            style += styles[css_tag]
                        css_tag = "%s.%s" % (
                            tag,
                            svg_class,
                        )  # Selector type/class type.class
                        if css_tag in styles:
                            if len(style) != 0:
                                style += ";"
                            style += styles[css_tag]
                # Split style element into parts; priority highest
                if SVG_ATTR_STYLE in attributes:
                    if len(style) != 0:
                        style += ";"
                    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]

                # All class and attribute properties are compiled.
                values.update(attributes)
                values[SVG_STRUCT_ATTRIB] = attributes
                if (
                    not parse_display_none
                    and SVG_ATTR_DISPLAY in values
                    and values[SVG_ATTR_DISPLAY].lower() == SVG_VALUE_NONE
                ):
                    continue  # If the attributes flag our values to display=none, stop rendering.
                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)

                    if width is None:
                        # If a dim was not provided but a viewbox was, use the viewbox dim as physical size, else 1000
                        width = (
                            s.viewbox.width if s.viewbox is not None else 1000
                        )  # 1000 default no information.
                    if height is None:
                        height = s.viewbox.height if s.viewbox is not None else 1000

                    s.render(ppi=ppi, width=width, height=height, viewbox=s.viewbox)
                    height, width = s.width, s.height
                    if s.viewbox is not None:
                        try:
                            if s.height == 0 or s.width == 0:
                                return s
                            viewport_transform = s.viewbox_transform
                        except ZeroDivisionError:
                            # The width or height was zero.
                            # https://www.w3.org/TR/SVG11/struct.html#SVGElementWidthAttribute
                            # "A value of zero disables rendering of the element."
                            return s  # No more parsing will be done.

                        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
                        values["viewport_transform"] = values[SVG_ATTR_TRANSFORM]
                        width, height = s.viewbox.width, s.viewbox.height
                    if context is None:
                        stack[-1] = (context, values)
                    if context is not None:
                        context.append(s)
                    context = s
                elif SVG_TAG_GROUP == tag:
                    s = Group(values)
                    if context is not None:
                        context.append(s)
                    context = s
                    s.render(ppi=ppi, width=width, height=height)
                elif SVG_TAG_DEFS == tag:
                    s = Group(values)
                    context = s  # Non-Rendered
                    s.render(ppi=ppi, width=width, height=height)
                elif SVG_TAG_CLIPPATH == tag:
                    s = ClipPath(values)
                    context = s  # Non-Rendered
                    s.render(ppi=ppi, width=width, height=height)
                    clip += 1
                elif SVG_TAG_USE == tag:
                    s = Use(values)
                    if SVG_ATTR_TRANSFORM in s.values:
                        # Update value in case x or y applied.
                        values[SVG_ATTR_TRANSFORM] = s.values[SVG_ATTR_TRANSFORM]
                    if SVG_ATTR_X in values:
                        del values[SVG_ATTR_X]
                    if SVG_ATTR_Y in values:
                        del values[SVG_ATTR_Y]
                    if SVG_ATTR_WIDTH in values:
                        del values[SVG_ATTR_WIDTH]
                    if SVG_ATTR_HEIGHT in values:
                        del values[SVG_ATTR_HEIGHT]
                    if context is not None:
                        context.append(s)
                    context = s
                    use += 1
                    if SVG_ATTR_ID in attributes and root is not None and use == 1:
                        root.objects[attributes[SVG_ATTR_ID]] = s
                elif SVG_TAG_PATTERN == tag:
                    s = Pattern(values)
                    context = s  # Non-rendered
                    s.render(ppi=ppi, width=width, height=height)
                elif tag in (
                    SVG_TAG_PATH,
                    SVG_TAG_CIRCLE,
                    SVG_TAG_ELLIPSE,
                    SVG_TAG_LINE,  # Shapes
                    SVG_TAG_POLYLINE,
                    SVG_TAG_POLYGON,
                    SVG_TAG_RECT,
                    SVG_TAG_IMAGE,
                ):
                    parse_error = None
                    s = None
                    try:
                        if SVG_TAG_PATH == tag:
                            # Delayed path parsing, for partial paths.
                            s = Path(values, pathd_loaded=True)
                            s.parse(values.get(SVG_ATTR_DATA))
                        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)
                        else:  # SVG_TAG_IMAGE == tag:
                            s = Image(values)
                    except ValueError as e:
                        parse_error = e
                        if s is None:
                            # s was not established we continue without it.
                            continue
                    s.render(ppi=ppi, width=width, height=height)
                    if reify:
                        s.reify()
                    if s.is_degenerate():
                        continue
                    if context is not None:
                        context.append(s)
                    if parse_error:
                        # Error was encountered, but s was established and processed.
                        if on_error == "ignore":
                            continue
                        elif on_error == "raise":
                            raise parse_error
                        else:  # "stop"
                            return root
                elif tag in (
                    SVG_TAG_STYLE,
                    SVG_TAG_TEXT,
                    SVG_TAG_DESC,
                    SVG_TAG_TITLE,
                    SVG_TAG_TSPAN,
                ):
                    # <style>, <text>, <desc>, <title>
                    continue
                else:
                    s = SVGElement(values)  # SVG Unknown object return as element.
                    if context is not None:
                        context.append(s)
                # If no root was established, s is root.
                if root is None:
                    root = s
                if isinstance(root, SVG):
                    # Assign optional linked properties.
                    try:
                        clip_path_url = s.values.get(SVG_ATTR_CLIP_PATH, None)
                        if clip_path_url is not None:
                            clip_path = root.get_element_by_url(clip_path_url)
                            s.clip_path = clip_path
                    except AttributeError:
                        pass
                    if clip != 0:
                        try:
                            clip_rule = s.values.get(
                                SVG_ATTR_CLIP_RULE, SVG_RULE_NONZERO
                            )
                            if clip_rule is not None:
                                s.clip_rule = clip_rule
                        except AttributeError:
                            pass
                    if SVG_ATTR_ID in attributes and use == 0:
                        root.objects[attributes[SVG_ATTR_ID]] = s
            elif event == "end":  # End event.
                # The iterparse spec makes it clear that internal text data is undefined except at the end.
                if (
                    SVG_ATTR_DISPLAY in values
                    and values[SVG_ATTR_DISPLAY].lower() == SVG_VALUE_NONE
                ):
                    # We are in a display=none, do not render this. Pop values and continue.
                    context, values = stack.pop()
                    continue
                s = None
                if tag in (
                    SVG_TAG_TEXT,
                    SVG_TAG_TSPAN,
                    SVG_TAG_DESC,
                    SVG_TAG_TITLE,
                    SVG_TAG_STYLE,
                ):
                    attributes = elem.attrib
                    if SVG_ATTR_ID in attributes and root is not None and use == 0:
                        root.objects[attributes[SVG_ATTR_ID]] = s
                if tag in (SVG_TAG_TEXT, SVG_TAG_TSPAN):
                    s = Text(values, text=elem.text)
                    s.render(ppi=ppi, width=width, height=height)
                    if reify:
                        s.reify()
                    if context is not None:
                        context.append(s)
                elif SVG_TAG_DESC == tag:
                    s = Desc(values, desc=elem.text)
                    if context is not None:
                        context.append(s)
                elif SVG_TAG_TITLE == tag:
                    s = Title(values, title=elem.text)
                    if context is not None:
                        context.append(s)
                elif SVG_TAG_STYLE == tag:
                    textstyle = elem.text
                    if textstyle is None:
                        textstyle = ""
                    textstyle = re.sub(REGEX_CSS_COMMENT, "", textstyle)
                    assignments = list(re.findall(REGEX_CSS_STYLE, textstyle.strip()))
                    for key, value in assignments:
                        key = key.strip()
                        value = value.strip()
                        for selector in key.split(","):  # Can comma select subitems.
                            sel = selector.strip()
                            if sel not in styles:
                                styles[sel] = value
                            else:
                                if not styles[sel].endswith(";"):
                                    styles[sel] += ";"
                                styles[sel] += value
                elif SVG_TAG_CLIPPATH == tag:
                    clip -= 1
                elif SVG_TAG_USE == tag:
                    use -= 1
                if s is not None:
                    if root is None:
                        root = s
                    # Assign optional linked properties.
                    try:
                        clip_path_url = s.values.get(SVG_ATTR_CLIP_PATH, None)
                        if clip_path_url is not None:
                            clip_path = root.get_element_by_url(clip_path_url)
                            s.clip_path = clip_path
                    except AttributeError:
                        pass
                    if clip != 0:
                        try:
                            clip_rule = s.values.get(
                                SVG_ATTR_CLIP_RULE, SVG_RULE_NONZERO
                            )
                            if clip_rule is not None:
                                s.clip_rule = clip_rule
                        except AttributeError:
                            pass

                context, values = stack.pop()
            elif event == "start-ns":
                if elem[0] != SVG_ATTR_DATA:
                    # Rare wc3 test uses a 'd' namespace.
                    values[elem[0]] = elem[1]
        return root


def tostring(node):
    from xml.etree.ElementTree import tostring

    return tostring(_write_node(node), encoding="unicode")


def write(node, f, pretty=True, **kwargs):
    """
    Gets the write node and writes to the ElementTree.write() function, all options in Element.write() are passed
    along via this method. encoding, xml_declaration, short_empty_elements, etc.
    """
    from xml.etree.ElementTree import ElementTree

    root = _write_node(node)
    if pretty:
        _pretty_print(root)
    tree = ElementTree(root)
    try:
        if f.lower().endswith("svgz"):
            import gzip

            f = gzip.open(f, "wb")
    except AttributeError:
        # might be a pathlib.Path()
        pass
    tree.write(f, **kwargs)


def _write_node(node, xml_tree=None, viewport_transform=None):
    if hasattr(node, "values"):
        values = node.values
        values = node.values.get(SVG_STRUCT_ATTRIB, values)
    else:
        values = None

    def subxml(xml_tree, tag):
        from xml.etree.ElementTree import Element, SubElement

        if xml_tree is None:
            xml_tree = Element(tag)
        else:
            xml_tree = SubElement(xml_tree, tag)
        for key, value in values.items():
            if key in (
                "tag",
                SVG_STRUCT_ATTRIB,
                SVG_ATTR_TRANSFORM,
                SVG_ATTR_FILL,
                SVG_ATTR_STROKE,
                SVG_TAG_STYLE,
            ):
                continue
            xml_tree.set(key, str(value))
        return xml_tree

    if isinstance(node, SVG):
        if xml_tree is None:
            xml_tree = subxml(xml_tree, SVG_NAME_TAG)
            xml_tree.set(SVG_ATTR_VERSION, SVG_VALUE_VERSION)
            xml_tree.set(SVG_ATTR_XMLNS, SVG_VALUE_XMLNS)
            xml_tree.set(SVG_ATTR_XMLNS_LINK, SVG_VALUE_XLINK)
            xml_tree.set(SVG_ATTR_XMLNS_EV, SVG_VALUE_XMLNS_EV)
        else:
            xml_tree = subxml(xml_tree, SVG_NAME_TAG)
        if node.x:
            xml_tree.set(SVG_ATTR_X, str(node.x))
        if node.y:
            xml_tree.set(SVG_ATTR_Y, str(node.y))
        if node.width:
            xml_tree.set(SVG_ATTR_WIDTH, str(node.width))
        if node.height:
            xml_tree.set(SVG_ATTR_HEIGHT, str(node.height))
        if node.viewbox:
            xml_tree.set(SVG_ATTR_VIEWBOX, str(node.viewbox))
        vt = None
        try:
            vt = node.viewbox_transform
            if vt:
                m = Matrix(vt)
                m.inverse()
                vt = m
        except ValueError:
            pass
        for child in node:
            _write_node(child, xml_tree, vt)
    elif isinstance(node, Ellipse):
        xml_tree = subxml(xml_tree, SVG_TAG_ELLIPSE)
        if node.cx:
            xml_tree.set(SVG_ATTR_CENTER_X, str(node.cx))
        if node.cy:
            xml_tree.set(SVG_ATTR_CENTER_Y, str(node.cy))
        if node.rx:
            xml_tree.set(SVG_ATTR_RADIUS_X, str(node.rx))
        if node.ry:
            xml_tree.set(SVG_ATTR_RADIUS_Y, str(node.ry))
    elif isinstance(node, Circle):
        xml_tree = subxml(xml_tree, SVG_TAG_CIRCLE)
        if node.cx:
            xml_tree.set(SVG_ATTR_CENTER_X, str(node.cx))
        if node.cy:
            xml_tree.set(SVG_ATTR_CENTER_Y, str(node.cy))
        if node.rx:
            xml_tree.set(SVG_ATTR_RADIUS, str(node.rx))
    elif isinstance(node, Image):
        xml_tree = subxml(xml_tree, SVG_TAG_IMAGE)
        from base64 import b64encode
        from io import BytesIO

        if node.image is not None:
            stream = BytesIO()
            node.image.save(stream, format="PNG")
            xml_tree.set(
                "xlink:href",
                f"data:image/png;base64,{b64encode(stream.getvalue()).decode('utf8')}",
            )
        if node.x:
            xml_tree.set(SVG_ATTR_X, str(node.x))
        if node.y:
            xml_tree.set(SVG_ATTR_Y, str(node.y))
        if node.width:
            xml_tree.set(SVG_ATTR_WIDTH, str(node.width))
        if node.height:
            xml_tree.set(SVG_ATTR_HEIGHT, str(node.height))
    elif isinstance(node, SimpleLine):
        xml_tree = subxml(xml_tree, SVG_TAG_LINE)
        if node.x1:
            xml_tree.set(SVG_ATTR_X1, str(node.x1))
        if node.y1:
            xml_tree.set(SVG_ATTR_Y1, str(node.y1))
        if node.x2:
            xml_tree.set(SVG_ATTR_X2, str(node.x2))
        if node.y2:
            xml_tree.set(SVG_ATTR_Y2, str(node.y2))
    elif isinstance(node, Path):
        xml_tree = subxml(xml_tree, SVG_TAG_PATH)
        xml_tree.set(SVG_ATTR_DATA, node.d(transformed=False))
    elif isinstance(node, Polyline):
        xml_tree = subxml(xml_tree, SVG_TAG_POLYLINE)
        xml_tree.set(
            SVG_ATTR_POINTS,
            " ".join([f"{e[0]} {e[1]}" for e in node.points]),
        )
    elif isinstance(node, Polygon):
        xml_tree = subxml(xml_tree, SVG_TAG_POLYGON)
        xml_tree.set(
            SVG_ATTR_POINTS,
            " ".join([f"{e[0]} {e[1]}" for e in node.points]),
        )
    elif isinstance(node, Rect):
        xml_tree = subxml(xml_tree, SVG_TAG_RECT)
        if node.x:
            xml_tree.set(SVG_ATTR_X, str(node.x))
        if node.y:
            xml_tree.set(SVG_ATTR_Y, str(node.y))
        if node.rx:
            xml_tree.set(SVG_ATTR_RADIUS_X, str(node.rx))
        if node.ry:
            xml_tree.set(SVG_ATTR_RADIUS_Y, str(node.ry))
        if node.width:
            xml_tree.set(SVG_ATTR_WIDTH, str(node.width))
        if node.height:
            xml_tree.set(SVG_ATTR_HEIGHT, str(node.height))
    elif isinstance(node, Text):
        xml_tree = subxml(xml_tree, SVG_TAG_TEXT)
        xml_tree.text = node.text
        if node.font_family:
            xml_tree.set(SVG_ATTR_FONT_FAMILY, str(node.font_family))
        if node.font_style:
            xml_tree.set(SVG_ATTR_FONT_STYLE, str(node.font_style))
        if node.font_variant:
            xml_tree.set(SVG_ATTR_FONT_VARIANT, str(node.font_variant))
        if node.font_stretch:
            xml_tree.set(SVG_ATTR_FONT_STRETCH, node.font_stretch)
        if node.font_size:
            xml_tree.set(SVG_ATTR_FONT_SIZE, str(node.font_size))
        if node.line_height:
            xml_tree.set("line_height", str(node.line_height))
        if node.anchor:
            xml_tree.set(SVG_ATTR_TEXT_ANCHOR, node.anchor)
    elif isinstance(node, Desc):
        xml_tree = subxml(xml_tree, SVG_TAG_DESC)
        if node.desc:
            xml_tree.set(SVG_TAG_DESC, str(node.desc))
    elif isinstance(node, Title):
        xml_tree = subxml(xml_tree, SVG_TAG_TITLE)
        if node.title:
            xml_tree.set(SVG_TAG_TITLE, str(node.title))
    elif isinstance(node, Pattern):
        xml_tree = subxml(xml_tree, SVG_TAG_PATTERN)
        if node.pattern_transform:
            xml_tree.set(SVG_ATTR_PATTERN_TRANSFORM, str(node.pattern_transform))
        if node.pattern_content_units:
            xml_tree.set(
                SVG_ATTR_PATTERN_CONTENT_UNITS, str(node.pattern_content_units)
            )
        if node.pattern_units:
            xml_tree.set(SVG_ATTR_PATTERN_UNITS, str(node.pattern_units))
        if node.preserve_aspect_ratio:
            xml_tree.set(SVG_ATTR_PRESERVEASPECTRATIO, str(node.preserve_aspect_ratio))
    elif isinstance(node, Use):
        # While Use elements are originally their own thing it can't be restored.
        xml_tree = subxml(xml_tree, SVG_TAG_GROUP)
        for child in node:
            _write_node(child, xml_tree, viewport_transform)
    elif isinstance(node, Group):
        # This is a structural group node of elements. Recurse call to write values.
        xml_tree = subxml(xml_tree, SVG_TAG_GROUP)
        for child in node:
            _write_node(child, xml_tree, viewport_transform)
    elif isinstance(node, SVGElement):
        if SVG_ATTR_TAG in values:
            xml_tree = subxml(xml_tree, values.get(SVG_ATTR_TAG))
        else:
            # Cannot write generic svgelement form
            return
    # Write Transform
    if hasattr(node, "transform") and not isinstance(node, Group):
        t = node.transform
        if viewport_transform:
            t = t * viewport_transform
        if not t.is_identity():
            xml_tree.set(
                SVG_ATTR_TRANSFORM,
                "matrix(%f, %f, %f, %f, %f, %f)" % (t.a, t.b, t.c, t.d, t.e, t.f),
            )

    # Write Stroke
    if hasattr(node, "stroke"):
        stroke = node.stroke
        if stroke is not None:
            stroke_opacity = stroke.opacity
            stroke = (
                str(abs(stroke))
                if stroke is not None and stroke.value is not None
                else SVG_VALUE_NONE
            )
            xml_tree.set(SVG_ATTR_STROKE, str(stroke))
            if stroke_opacity != 1.0 and stroke_opacity is not None:
                xml_tree.set(SVG_ATTR_STROKE_OPACITY, str(stroke_opacity))

            try:
                stroke_width = str(node.stroke_width)
                xml_tree.set(SVG_ATTR_STROKE_WIDTH, str(stroke_width))
            except AttributeError:
                pass

    # Write Fill
    if hasattr(node, "fill"):
        fill = node.fill
        if fill is not None:
            fill_opacity = fill.opacity
            fill = (
                str(abs(fill))
                if fill is not None and fill.value is not None
                else SVG_VALUE_NONE
            )
            xml_tree.set(SVG_ATTR_FILL, str(fill))
            if fill_opacity != 1.0 and fill_opacity is not None:
                xml_tree.set(SVG_ATTR_FILL_OPACITY, str(fill_opacity))

    # Write id
    if hasattr(node, "id"):
        if node.id is not None:
            xml_tree.set(SVG_ATTR_ID, str(node.id))

    return xml_tree


def _pretty_print(current, parent=None, index=-1, depth=0):
    for i, node in enumerate(current):
        _pretty_print(node, current, i, depth + 1)
    if parent is not None:
        if index == 0:
            parent.text = "\n" + ("\t" * depth)
        else:
            parent[index - 1].tail = "\n" + ("\t" * depth)
        if index == len(parent) - 1:
            current.tail = "\n" + ("\t" * (depth - 1))
