#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
svg2rlg is a tool to convert from SVG to reportlab graphics.

License: BSD

version 0.3
"""
import os
import re
from gzip import GzipFile

from xml.etree import ElementTree as etree

from reportlab.graphics import renderPDF

from reportlab.graphics.shapes import Drawing, Group, String, Line, Rect, Image
from reportlab.graphics.shapes import Circle, Ellipse, Polygon, PolyLine, Path
from reportlab.graphics.shapes import mmult

from reportlab.lib.units import toLength
import reportlab.lib.colors as colors


class SVGError(Exception):
    pass


EOF = object()


class Lexer:
    """
    This style of implementation was inspired by this article:

        http://www.gooli.org/blog/a-simple-lexer-in-python/
    """
    Float = r'[-\+]?(?:(?:\d*\.\d+)|(?:\d+\.)|(?:\d+))(?:[Ee][-\+]?\d+)?'
    Int = r'[-\+]?\d+'

    lexicon = None
    ignore = None
    callbacks = None

    def __init__(self):
        lexicon = self.lexicon
        # create internal names for group matches
        groupnames = dict(('lexer_%d' % idx, item[0])
                          for idx, item in enumerate(lexicon))
        self.groupnames = groupnames
        # assemble regex parts to one regex
        igroupnames = dict((value, name) for name, value in groupnames.items())
        regex_parts = ('(?P<%s>%s)' % (igroupnames[cls], regs)
                       for cls, regs in lexicon)
        self.regex_string = '|'.join(regex_parts)
        self.regex = re.compile(self.regex_string)

    def lex(self, text):
        """
        Yield (token_type, data) tokens.

        The last token will be (EOF, None) where EOF
        """
        regex = self.regex
        groupnames = self.groupnames
        ignore = self.ignore or set()
        callbacks = self.callbacks or dict()
        position = 0
        size = len(text)

        while position < size:
            match = regex.match(text, position)
            if match is None:
                raise SVGError('Unknown token at position %d' % position)

            position = match.end()
            cls = groupnames[match.lastgroup]
            value = match.group(match.lastgroup)

            if cls in ignore:
                continue

            if cls in callbacks:
                value = callbacks[cls](self, value)

            yield (cls, value)

        yield (EOF, None)


# Parse SVG length units
length_pattern = r"""
    ^                           # match start of line
    \s*                         # ignore whitespace
    (?P<value>[-\+]?\d*\.?\d*([eE][-\+]?\d+)?)  # match float or int value
    (?P<unit>.+)?               # match any chars
    \s*                         # ignore whitespace
    $                           # match end of line
"""

length_match = re.compile(length_pattern, re.X).match

def parseLength(length):
    """
    Convert length to reportlab points.
    """
    match = length_match(length)
    if match is None:
        raise SVGError("Not a valid length unit: '%s'" % length)

    value = match.group('value')
    unit = match.group('unit') or ''

    if not value:
        raise SVGError("Not a valid length unit: '%s'" % length)

    if not unit:
        if value[0] == 'e' or value[0] == 'E':
            return float('1' + value)
        else:
            return float(value)

    elif unit in ('em', 'ex', 'px', '%'):
        # ignoring relative units
        return float(value)

    elif unit == 'pc':
        return toLength(value + 'pica')

    elif unit in ('mm', 'cm', 'in', 'i', 'pt', 'pica'):
        return toLength(length)

    else:
        raise SVGError("Unknown unit '%s'" % unit)

# Parse SVG color definitions
color_pattern = r"""
^(
    (?P<named>
        \w+
    ) |

    (?P<hex>
        [#]
        (?P<hex_r>[a-zA-Z0-9]{2})
        (?P<hex_rg>[a-zA-Z0-9]{2})
        (?P<hex_rb>[a-zA-Z0-9]{2})
    ) |

    (?P<hexshort>
        [#]
        (?P<hexshort_r>[a-zA-Z0-9])
        (?P<hexshort_g>[a-zA-Z0-9])
        (?P<hexshort_b>[a-zA-Z0-9])
    ) |

    (?P<rgbint>
        [rR][gG][bB]
        \(\s*
        (?P<rgbint_r>\d{1,3})
        \s*,\s*
        (?P<rgbint_g>\d{1,3})
        \s*,\s*
        (?P<rgbint_b>\d{1,3})
        \s*\)
    ) |

    (?P<rgb>
        [rR][gG][bB]
        \(
        (?P<rgb_r>\d+\.?\d*)
        \%
        ,\s?
        (?P<rgb_g>\d+\.?\d*)
        \%
        ,\s?
        (?P<rgb_b>\d+\.?\d*)
        \%
        \)
    )
)$
"""

color_match = re.compile(color_pattern, re.X).match

NAMEDCOLOURS = {
    # expanded named colors from SVG spc
    'aliceblue': colors.HexColor('#f0f8ff'),
    'antiquewhite': colors.HexColor('#faebd7'),
    'aqua': colors.HexColor('#00ffff'),
    'aquamarine': colors.HexColor('#7fffd4'),
    'azure': colors.HexColor('#f0ffff'),
    'beige': colors.HexColor('#f5f5dc'),
    'bisque': colors.HexColor('#ffe4c4'),
    'black': colors.HexColor('#000000'),
    'blanchedalmond': colors.HexColor('#ffebcd'),
    'blue': colors.HexColor('#0000ff'),
    'blueviolet': colors.HexColor('#8a2be2'),
    'brown': colors.HexColor('#a52a2a'),
    'burlywood': colors.HexColor('#deb887'),
    'cadetblue': colors.HexColor('#5f9ea0'),
    'chartreuse': colors.HexColor('#7fff00'),
    'chocolate': colors.HexColor('#d2691e'),
    'coral': colors.HexColor('#ff7f50'),
    'cornflowerblue': colors.HexColor('#6495ed'),
    'cornsilk': colors.HexColor('#fff8dc'),
    'crimson': colors.HexColor('#dc143c'),
    'cyan': colors.HexColor('#00ffff'),
    'darkblue': colors.HexColor('#00008b'),
    'darkcyan': colors.HexColor('#008b8b'),
    'darkgoldenrod': colors.HexColor('#b8860b'),
    'darkgray': colors.HexColor('#a9a9a9'),
    'darkgreen': colors.HexColor('#006400'),
    'darkgrey': colors.HexColor('#a9a9a9'),
    'darkkhaki': colors.HexColor('#bdb76b'),
    'darkmagenta': colors.HexColor('#8b008b'),
    'darkolivegreen': colors.HexColor('#556b2f'),
    'darkorange': colors.HexColor('#ff8c00'),
    'darkorchid': colors.HexColor('#9932cc'),
    'darkred': colors.HexColor('#8b0000'),
    'darksalmon': colors.HexColor('#e9967a'),
    'darkseagreen': colors.HexColor('#8fbc8b'),
    'darkslateblue': colors.HexColor('#483d8b'),
    'darkslategray': colors.HexColor('#2f4f4f'),
    'darkslategrey': colors.HexColor('#2f4f4f'),
    'darkturquoise': colors.HexColor('#00ced1'),
    'darkviolet': colors.HexColor('#9400d3'),
    'deeppink': colors.HexColor('#ff1493'),
    'deepskyblue': colors.HexColor('#00bfff'),
    'dimgray': colors.HexColor('#696969'),
    'dimgrey': colors.HexColor('#696969'),
    'dodgerblue': colors.HexColor('#1e90ff'),
    'firebrick': colors.HexColor('#b22222'),
    'floralwhite': colors.HexColor('#fffaf0'),
    'forestgreen': colors.HexColor('#228b22'),
    'fuchsia': colors.HexColor('#ff00ff'),
    'gainsboro': colors.HexColor('#dcdcdc'),
    'ghostwhite': colors.HexColor('#f8f8ff'),
    'gold': colors.HexColor('#ffd700'),
    'goldenrod': colors.HexColor('#daa520'),
    'gray': colors.HexColor('#808080'),
    'green': colors.HexColor('#008000'),
    'greenyellow': colors.HexColor('#adff2f'),
    'grey': colors.HexColor('#808080'),
    'honeydew': colors.HexColor('#f0fff0'),
    'hotpink': colors.HexColor('#ff69b4'),
    'indianred': colors.HexColor('#cd5c5c'),
    'indigo': colors.HexColor('#4b0082'),
    'ivory': colors.HexColor('#fffff0'),
    'khaki': colors.HexColor('#f0e68c'),
    'lavender': colors.HexColor('#e6e6fa'),
    'lavenderblush': colors.HexColor('#fff0f5'),
    'lawngreen': colors.HexColor('#7cfc00'),
    'lemonchiffon': colors.HexColor('#fffacd'),
    'lightblue': colors.HexColor('#add8e6'),
    'lightcoral': colors.HexColor('#f08080'),
    'lightcyan': colors.HexColor('#e0ffff'),
    'lightgoldenrodyellow': colors.HexColor('#fafad2'),
    'lightgray': colors.HexColor('#d3d3d3'),
    'lightgreen': colors.HexColor('#90ee90'),
    'lightgrey': colors.HexColor('#d3d3d3'),
    'lightpink': colors.HexColor('#ffb6c1'),
    'lightsalmon': colors.HexColor('#ffa07a'),
    'lightseagreen': colors.HexColor('#20b2aa'),
    'lightskyblue': colors.HexColor('#87cefa'),
    'lightslategray': colors.HexColor('#778899'),
    'lightslategrey': colors.HexColor('#778899'),
    'lightsteelblue': colors.HexColor('#b0c4de'),
    'lightyellow': colors.HexColor('#ffffe0'),
    'lime': colors.HexColor('#00ff00'),
    'limegreen': colors.HexColor('#32cd32'),
    'linen': colors.HexColor('#faf0e6'),
    'magenta': colors.HexColor('#ff00ff'),
    'maroon': colors.HexColor('#800000'),
    'mediumaquamarine': colors.HexColor('#66cdaa'),
    'mediumblue': colors.HexColor('#0000cd'),
    'mediumorchid': colors.HexColor('#ba55d3'),
    'mediumpurple': colors.HexColor('#9370db'),
    'mediumseagreen': colors.HexColor('#3cb371'),
    'mediumslateblue': colors.HexColor('#7b68ee'),
    'mediumspringgreen': colors.HexColor('#00fa9a'),
    'mediumturquoise': colors.HexColor('#48d1cc'),
    'mediumvioletred': colors.HexColor('#c71585'),
    'midnightblue': colors.HexColor('#191970'),
    'mintcream': colors.HexColor('#f5fffa'),
    'mistyrose': colors.HexColor('#ffe4e1'),
    'moccasin': colors.HexColor('#ffe4b5'),
    'navajowhite': colors.HexColor('#ffdead'),
    'navy': colors.HexColor('#000080'),
    'oldlace': colors.HexColor('#fdf5e6'),
    'olive': colors.HexColor('#808000'),
    'olivedrab': colors.HexColor('#6b8e23'),
    'orange': colors.HexColor('#ffa500'),
    'orangered': colors.HexColor('#ff4500'),
    'orchid': colors.HexColor('#da70d6'),
    'palegoldenrod': colors.HexColor('#eee8aa'),
    'palegreen': colors.HexColor('#98fb98'),
    'paleturquoise': colors.HexColor('#afeeee'),
    'palevioletred': colors.HexColor('#db7093'),
    'papayawhip': colors.HexColor('#ffefd5'),
    'peachpuff': colors.HexColor('#ffdab9'),
    'peru': colors.HexColor('#cd853f'),
    'pink': colors.HexColor('#ffc0cb'),
    'plum': colors.HexColor('#dda0dd'),
    'powderblue': colors.HexColor('#b0e0e6'),
    'purple': colors.HexColor('#800080'),
    'red': colors.HexColor('#ff0000'),
    'rosybrown': colors.HexColor('#bc8f8f'),
    'royalblue': colors.HexColor('#4169e1'),
    'saddlebrown': colors.HexColor('#8b4513'),
    'salmon': colors.HexColor('#fa8072'),
    'sandybrown': colors.HexColor('#f4a460'),
    'seagreen': colors.HexColor('#2e8b57'),
    'seashell': colors.HexColor('#fff5ee'),
    'sienna': colors.HexColor('#a0522d'),
    'silver': colors.HexColor('#c0c0c0'),
    'skyblue': colors.HexColor('#87ceeb'),
    'slateblue': colors.HexColor('#6a5acd'),
    'slategray': colors.HexColor('#708090'),
    'slategrey': colors.HexColor('#708090'),
    'snow': colors.HexColor('#fffafa'),
    'springgreen': colors.HexColor('#00ff7f'),
    'steelblue': colors.HexColor('#4682b4'),
    'tan': colors.HexColor('#d2b48c'),
    'teal': colors.HexColor('#008080'),
    'thistle': colors.HexColor('#d8bfd8'),
    'tomato': colors.HexColor('#ff6347'),
    'turquoise': colors.HexColor('#40e0d0'),
    'violet': colors.HexColor('#ee82ee'),
    'wheat': colors.HexColor('#f5deb3'),
    'white': colors.HexColor('#ffffff'),
    'whitesmoke': colors.HexColor('#f5f5f5'),
    'yellow': colors.HexColor('#ffff00'),
    'yellowgreen': colors.HexColor('#9acd32'),
}


def parseColor(color):
    """
    Convert SVG color to reportlab color
    """
    if color == 'none':
        return None

    elif color == 'currentColor':
        return "currentColor"

    elif color == 'transparent':
        return colors.Color(0., 0., 0., 0.)

    match = color_match(color)
    if match is None:
        raise SVGError("Not a valid color definition: '%s'" % color)

    info = match.groupdict()

    if info['named'] is not None:
        if color not in NAMEDCOLOURS:
            raise SVGError("Not a valid named color: '%s'" % color)

        return NAMEDCOLOURS[color]

    elif info['hex'] is not None:
        return colors.HexColor(color)

    elif info['hexshort'] is not None:
        r = 2*info['hexshort_r']
        g = 2*info['hexshort_g']
        b = 2*info['hexshort_b']

        return colors.HexColor('#%s%s%s' % (r,g,b))

    elif info['rgbint'] is not None:
        r = int(info['rgbint_r'])
        g = int(info['rgbint_g'])
        b = int(info['rgbint_b'])

        if r > 255 or g > 255 or b > 255:
            raise SVGError("RGB value outside range: '%s'" % color)

        return colors.Color(r/255., g/255., b/255.)

    elif info['rgb'] is not None:
        r = float(info['rgb_r'])
        g = float(info['rgb_g'])
        b = float(info['rgb_b'])

        if r > 100 or g > 100 or b > 100:
            raise SVGError("RGB value outside range: '%s'" % color)

        return colors.Color(r/100., g/100., b/100.)


class SVGStyle(Lexer):
    """
    Break SVG inline style into tokens
    """
    name = object()
    value = object()
    delimiter = object()
    comment = object()

    lexicon = (
        (delimiter, r'[ :;\n]'),
        (comment, r'/\*.+\*/'),
        (name, r'[\w\-#]+?(?=:)'),
        (value, r'[\w\-#\.\(\)%,][\w \-#\.\(\)%,]*?(?=[;])'),
    )

    ignore = frozenset((delimiter, comment))

    def __init__(self):
        Lexer.__init__(self)

    def parse(self, text):
        """
        Parse a string of SVG <path> data.
        """
        toks = self.lex(text + ';')
        styles = {}

        while True:
            token, value = next(toks)
            if token == EOF:
                break
            stylename = value.rstrip(':')
            token, value = next(toks)
            if token == EOF:
                raise SVGError('expected value in style definition')
            if stylename in styles:
                raise SVGError('style redefined')
            styles[stylename] = value

        return styles


parseStyle = SVGStyle()


class SVGTransform(Lexer):
    """
    Break SVG transform into tokens.
    """
    numfloat = object()
    numint = object()
    string = object()
    skip = object()

    numbers = frozenset((numfloat, numint))

    lexicon = (
        (numfloat, Lexer.Float),
        (numint, Lexer.Int),
        (string, r'\w+'),
        (skip, r'[\(\), \n]'),
    )

    ignore = frozenset((skip,))

    callbacks = {
        numfloat: lambda self, value: float(value),
        numint: lambda self, value: float(value)
    }

    def __init__(self):
        Lexer.__init__(self)

    def assertion(self, condition, msg = ''):
        if not condition:
            raise SVGError(msg)

    def iterparse(self, text):
        """
        Parse a string of SVG transform data.
        """
        assertion = self.assertion
        toks = self.lex(text)
        numbers = self.numbers
        string = self.string

        token, value = next(toks)
        while token != EOF:
            assertion(token is string, 'Expected string')
            transform = value

            if transform == 'matrix':
                token, a = next(toks)
                assertion(token in numbers, 'Expected number')
                token, b = next(toks)
                assertion(token in numbers, 'Expected number')
                token, c = next(toks)
                assertion(token in numbers, 'Expected number')
                token, d = next(toks)
                assertion(token in numbers, 'Expected number')
                token, e = next(toks)
                assertion(token in numbers, 'Expected number')
                token, f = next(toks)
                assertion(token in numbers, 'Expected number')
                yield (transform, (a, b, c, d, e, f))

            elif transform == 'translate':
                token, tx = next(toks)
                assertion(token in numbers, 'Expected number')
                token, value = next(toks)
                ty = value
                if token not in numbers:
                    ty = 0.

                yield (transform, (tx, ty))

                if token not in numbers:
                    continue

            elif transform == 'scale':
                token, sx = next(toks)
                assertion(token in numbers, 'Expected number')

                token, value = next(toks)
                sy = value
                if token not in numbers:
                    sy = sx

                yield (transform, (sx, sy))

                if token not in numbers:
                    continue

            elif transform == 'rotate':
                token, angle = next(toks)
                assertion(token in numbers, 'Expected number')

                token, value = next(toks)
                cx = value

                if token in numbers:
                    token, value = next(toks)
                    assertion(token in numbers, 'Expected number')

                    cy = value

                    yield (transform, (angle, (cx, cy)))

                else:
                    yield (transform, (angle, None))
                    continue

            elif transform == 'skewX' or transform == 'skewY':
                token, value = next(toks)
                angle = value
                assertion(token in numbers, 'Expected number')

                yield (transform, (angle,))

            else:
                raise SVGError("unknown transform '%s'" % transform)

            # fetch next token
            token, value = next(toks)

parseTransform = SVGTransform()


class SVGPath(Lexer):
    """
    Break SVG path data into tokens.

    The SVG spec requires that tokens are greedy.
    This lexer relies on Python's regexes defaulting to greediness.
    """
    numfloat = object()
    numint = object()
    numexp = object()
    string = object()
    skip = object

    lexicon = (
        (numfloat, Lexer.Float),
        (numint, Lexer.Int),
        (numexp, r'(?:[Ee][-\+]?\d+)'),
        (string, r'[AaCcHhLlMmQqSsTtVvZz]'),
        (skip, r'[, \n]'),
    )

    ignore = frozenset((skip,))

    callbacks = {
        numfloat: lambda self,value: float(value),
        numint: lambda self,value: float(value),
        numexp: lambda self,value: float('1.'+value)
    }

    numbers = frozenset((numfloat, numint, numexp))

    def __init__(self):
        Lexer.__init__(self)

    def assertion(self, condition, msg=''):
        if not condition:
            raise SVGError(msg)

    def iterparse(self, text):
        """
        Parse a string of SVG <path> data.
        """
        assertion = self.assertion
        numbers = self.numbers
        string = self.string

        toks = self.lex(text)

        token, value = next(toks)
        while token != EOF:
            assertion(token is string, 'Expected string in path data')
            cmd = value
            CMD = value.upper()

            # closePath
            if CMD in 'Z':
                token, value = next(toks)
                yield (cmd, (None,))

            # moveTo, lineTo, curve, smoothQuadraticBezier, quadraticBezier, smoothCurve
            elif CMD in 'CMLTQS':
                coords = []
                token, value = next(toks)
                while token in numbers:
                    last = value
                    token, value = next(toks)
                    assertion(token in numbers, 'Expected number in path data')
                    coords.append((last, value))
                    token, value = next(toks)

                if CMD == 'C':
                    assertion(len(coords) % 3 == 0, 'Expected coordinate triplets in path data')

                yield (cmd, tuple(coords))

            # horizontalLine or verticalLine
            elif CMD in 'HVhv':
                coords = []
                token, value = next(toks)
                assertion(token in numbers, 'Expected number')
                while token in numbers:
                    coords.append(value)
                    token, value = next(toks)

                yield (cmd, tuple(coords))

            # ellipticalArc
            elif CMD == 'A':
                token, rx = next(toks)
                #assertion(token in numbers and value > 0, 'expected positive number in path data')
                rx = value
                token, ry = next(toks)
                #assertion(token in numbers and ry > 0, 'expected positive number in path data')
                token, rotation = next(toks)
                #assertion(token in numbers, 'expected number in path data')
                token, largearc = next(toks)
                #assertion(token is int and largearc in (0,1), 'expected 0 or 1 in path data')
                token, sweeparc = next(toks)
                #assertion(token is int and sweeparc in (0,1), 'expected 0 or 1 in path data')
                token, x = next(toks)
                #assertion(token in numbers, 'expected number in path data')
                token, y = next(toks)
                #assertion(token in numbers, 'expected number in path data')
                yield (cmd, ((rx,ry), rotation, largearc, sweeparc, (x,y)))
                token, value = next(toks)

            else:
                raise SVGError("cmd '%s' in path data not supported" % cmd)

parsePath = SVGPath()


def parseDashArray(array):
    if array == 'none':
        return None

    return list(map(parseLength, re.split('[ ,]+', array)))


def parseOpacity(value):
    try:
        opacity = float(value)
    except ValueError:
        raise SVGError('expected float value')

    # clamp value
    opacity = min(max(opacity, 0.), 1.)

    return opacity


def parseAnchor(value):
    if not value or value == 'none':
        return 'start'

    if value not in ('start', 'middle', 'end'):
        raise SVGError("unknown '%s' alignment" % value)

    return value

# map svg font names to reportlab names
FONTMAPPING = {
    "sans-serif": "Helvetica",
    "serif": "Times-Roman",
    "monospace": "Courier"
}

# map svg style to repotlab attributes and converters
STYLES = {
    "fill": ("fillColor", parseColor),
    "stroke": ("strokeColor", parseColor),
    "stroke-width": ("strokeWidth", parseLength),
    "stroke-linejoin": ("strokeLineJoin", lambda style: {"miter":0, "round":1, "bevel":2}[style]),
    "stroke-linecap": ("strokeLineCap",  lambda style: {"butt":0, "round":1, "square":2}[style]),
    "stroke-dasharray": ("strokeDashArray", parseDashArray),
    "fill-opacity": ('fillOpacity', parseOpacity),
    "stroke-opacity": ('strokeOpacity', parseOpacity),
    "font-family": ("fontName", lambda name: FONTMAPPING.get(name, 'Helvetica')),
    "font-size": ("fontSize", parseLength),
    "text-anchor": ("textAnchor", parseAnchor),
}

STYLE_NAMES = frozenset(list(STYLES.keys()) + ['color',])
STYLES_FONT = frozenset(('font-family','font-size','text-anchor'))


class Renderer:
    LINK = '{http://www.w3.org/1999/xlink}href'

    SVG_NS = '{http://www.w3.org/2000/svg}'
    SVG_ROOT        = SVG_NS + 'svg'
    SVG_A           = SVG_NS + 'a'
    SVG_G           = SVG_NS + 'g'
    SVG_TITLE       = SVG_NS + 'title'
    SVG_DESC        = SVG_NS + 'desc'
    SVG_DEFS        = SVG_NS + 'defs'
    SVG_SYMBOL      = SVG_NS + 'symbol'
    SVG_USE         = SVG_NS + 'use'
    SVG_RECT        = SVG_NS + 'rect'
    SVG_CIRCLE      = SVG_NS + 'circle'
    SVG_ELLIPSE     = SVG_NS + 'ellipse'
    SVG_LINE        = SVG_NS + 'line'
    SVG_POLYLINE    = SVG_NS + 'polyline'
    SVG_POLYGON     = SVG_NS + 'polygon'
    SVG_PATH        = SVG_NS + 'path'
    SVG_TEXT        = SVG_NS + 'text'
    SVG_TSPAN       = SVG_NS + 'tspan'
    SVG_IMAGE       = SVG_NS + 'image'

    SVG_NODES = frozenset((
        SVG_ROOT, SVG_A, SVG_G, SVG_TITLE, SVG_DESC, SVG_DEFS, SVG_SYMBOL,
        SVG_USE, SVG_RECT, SVG_CIRCLE, SVG_ELLIPSE, SVG_LINE, SVG_POLYLINE,
        SVG_POLYGON, SVG_PATH, SVG_TEXT, SVG_TSPAN, SVG_IMAGE
    ))

    SKIP_NODES = frozenset((SVG_TITLE, SVG_DESC, SVG_DEFS, SVG_SYMBOL))
    PATH_NODES = frozenset((SVG_RECT, SVG_CIRCLE, SVG_ELLIPSE, SVG_LINE,
                            SVG_POLYLINE, SVG_POLYGON, SVG_PATH, SVG_TEXT))

    def __init__(self, filename):
        self.filename = filename
        self.level = 0
        self.styles = {}
        self.mainGroup = Group()
        self.drawing = None
        self.root = None

    def render(self, node, parent=None):
        if parent is None:
            parent = self.mainGroup

        # ignore if display = none
        display = node.get('display')
        if display == "none":
            return

        if node.tag == self.SVG_ROOT:
            self.level += 1

            if not self.drawing is None:
                raise SVGError('drawing already created!')

            self.root = node

            # default styles
            style = {
                'color':'none',
                'fill':'none',
                'stroke':'none',
                'font-family':'Helvetica',
                'font-size':'12'
            }

            self.styles[self.level] = style

            # iterate children
            for child in node:
                self.render(child, self.mainGroup)

            # create drawing
            width = node.get('width', '100%')
            height = node.get('height', '100%')

            if node.get("viewBox"):
                try:
                    minx, miny, width, height = node.get("viewBox").split()
                except ValueError:
                    raise SVGError("viewBox values not valid")

            if width.endswith('%') and height.endswith('%'):
                # handle relative size
                wscale = parseLength(width) / 100.
                hscale = parseLength(height) / 100.

                xL,yL,xH,yH =  self.mainGroup.getBounds()
                self.drawing = Drawing(xH*wscale + xL, yH*hscale + yL)

            else:
                self.drawing = Drawing(parseLength(width), parseLength(height))

            height = self.drawing.height
            self.mainGroup.scale(1, -1)
            self.mainGroup.translate(0, -height)
            self.drawing.add(self.mainGroup)

            self.level -= 1

            return self.drawing

        elif node.tag in (self.SVG_G, self.SVG_A):
            self.level += 1

            # set this levels style
            style = self.styles[self.level - 1].copy()
            style = self.nodeStyle(node, style)
            self.styles[self.level] = style

            group = Group()

            # iterate children
            for child in node:
                self.render(child, group)

            parent.add(group)

            transforms = node.get('transform')
            if transforms:
                for op in parseTransform.iterparse(transforms):
                    self.applyTransformOnGroup(group, op)

            self.level -= 1

        elif node.tag == self.SVG_USE:
            self.level += 1

            # set this levels style
            style = self.styles[self.level - 1].copy()
            style = self.nodeStyle(node, style)
            self.styles[self.level] = style

            group = Group()

            # link id
            link_id = node.get(self.LINK).lstrip('#')

            # find linked node in defs or symbol section
            target = None
            for defs in self.root.getiterator(self.SVG_DEFS):
                for element in defs:
                    if element.get('id') == link_id:
                        target = element
                        break

            if target is None:
                for defs in self.root.getiterator(self.SVG_SYMBOL):
                    for element in defs:
                        if element.get('id') == link_id:
                            target = element
                            break

                if target is None:
                    msg = "Could not find use node '%s'" % link_id
                    raise SVGError(msg)

            self.render(target, group)

            parent.add(group)

            # apply transform
            transforms = node.get('transform')
            if transforms:
                for op in parseTransform.iterparse(transforms):
                    self.applyTransformOnGroup(group, op)

            # apply 'x' and 'y' attribute as translation of defs object
            if node.get('x') or node.get('y'):
                dx = parseLength(node.get('x','0'))
                dy = parseLength(node.get('y','0'))

                self.applyTransformOnGroup(group, ('translate', (dx,dy)))

            self.level -= 1

        elif node.tag == self.SVG_LINE:
            # get coordinates
            x1 = parseLength(node.get('x1', '0'))
            y1 = parseLength(node.get('y1', '0'))
            x2 = parseLength(node.get('x2', '0'))
            y2 = parseLength(node.get('y2', '0'))

            shape = Line(x1, y1, x2, y2)
            self.addShape(parent, node, shape)

        elif node.tag == self.SVG_RECT:
            # get coordinates
            x = parseLength(node.get('x', '0'))
            y = parseLength(node.get('y', '0'))
            width = parseLength(node.get('width'))
            height = parseLength(node.get('height'))

            rx = parseLength(node.get('rx', '0'))
            ry = parseLength(node.get('ry', '0'))

            shape = Rect(x, y, width, height, rx=rx, ry=ry)
            self.addShape(parent, node, shape)

        elif node.tag == self.SVG_CIRCLE:
            cx = parseLength(node.get('cx', '0'))
            cy = parseLength(node.get('cy', '0'))
            r = parseLength(node.get('r'))

            if r > 0.:
                shape = Circle(cx, cy, r)
                self.addShape(parent, node, shape)

        elif node.tag == self.SVG_ELLIPSE:
            cx = parseLength(node.get('cx', '0'))
            cy = parseLength(node.get('cy', '0'))
            rx = parseLength(node.get('rx'))
            ry = parseLength(node.get('ry'))

            if rx > 0. and ry > 0.:
                shape = Ellipse(cx, cy, rx, ry)
                self.addShape(parent, node, shape)

        elif node.tag == self.SVG_POLYLINE:
            # convert points
            points = node.get('points').strip()
            if len(points) == 0:
                return

            points = list(map(parseLength, re.split('[ ,]+', points)))

            # Need to use two shapes, because standard RLG polylines
            # do not support filling...
            group = Group()
            shape = Polygon(points)
            self.applyStyleToShape(shape, node)
            shape.strokeColor = None
            group.add(shape)

            shape = PolyLine(points)
            self.applyStyleToShape(shape, node)
            group.add(shape)

            self.addShape(parent, node, group)

        elif node.tag == self.SVG_POLYGON:
            # convert points
            points = node.get('points').strip()
            if len(points) == 0:
                return

            points = list(map(parseLength, re.split('[ ,]+', points)))

            shape = Polygon(points)
            self.addShape(parent, node, shape)

        elif node.tag == self.SVG_IMAGE:
            x = parseLength(node.get('x', '0'))
            y = parseLength(node.get('y', '0'))
            width = parseLength(node.get('width', '0'))
            height = parseLength(node.get('height', '0'))

            # link id
            link_id = node.get(self.LINK)

            filename = os.path.join(os.path.dirname(self.filename), link_id)
            shape = Image(x, y, width, height, filename)

            self.addShape(parent, node, shape)

        elif node.tag == self.SVG_TEXT:
            # Todo:
            # - rotation not handled
            # - baseshift not handled
            # - embedded span node not handled
            #
            def parsePos(node, subnode, name, default='0'):
                values = subnode.get(name)
                if values is None:
                    if node is not None:
                        values = node.get(name, default)
                    else:
                        values = default

                return list(map(parseLength, values.split()))

            def getPos(values, i, default=None):
                if i >= len(values):
                    if default is None:
                        return values[-1]
                    else:
                        return default
                else:
                    return values[i]

            def handleText(node, subnode, text):
                # get position variables
                xs = parsePos(node, subnode, 'x')
                dxs = parsePos(node, subnode, 'dx')
                ys = parsePos(node, subnode,'y')
                dys = parsePos(node, subnode,'dy')

                if sum(map(len, (xs,ys,dxs,dys))) == 4:
                    # single value
                    shape = String(xs[0] + dxs[0], -ys[0] - dys[0], text)
                    self.applyStyleToShape(shape, subnode)
                    group.add(shape)

                else:
                    # multiple values
                    for i, c in enumerate(text):
                        x = getPos(xs, i)
                        dx = getPos(dxs, i, 0)
                        y = getPos(ys, i)
                        dy = getPos(dys, i, 0)

                        shape = String(x + dx, -y -dy, c)
                        self.applyStyleToShape(shape, subnode)
                        group.add(shape)

            if node.text and node.text.strip():
                group = Group()

                handleText(None, node, node.text.strip())

                group.scale(1, -1)

                self.addShape(parent, node, group)

            if len(node) > 0:
                group = Group()

                self.level += 1

                # set this levels style
                style = self.styles[self.level - 1].copy()
                nodestylestyle = self.nodeStyle(node, style)
                self.styles[self.level] = nodestylestyle

                for subnode in node:
                    if subnode.tag == self.SVG_TSPAN:
                        if subnode.text is not None:
                            handleText(node, subnode, subnode.text.strip())
                        else:
                            handleText(node, subnode, "")

                self.level -= 1

                group.scale(1, -1)
                self.addShape(parent, node, group)

        elif node.tag == self.SVG_PATH:
            def convertQuadratic(Q0, Q1, Q2):
                C1 = (Q0[0] + 2./3*(Q1[0] - Q0[0]), Q0[1] + 2./3*(Q1[1] - Q0[1]))
                C2 = (C1[0] + 1./3*(Q2[0] - Q0[0]), C1[1] + 1./3*(Q2[1] - Q0[1]))
                C3 = Q2
                return C1[0], C1[1], C2[0], C2[1], C3[0], C3[1]

            def prevCtrl(lastOp, lastArgs, currentX, currentY):
                # fetch last controll point
                if lastOp in 'CScsQqTt':
                    x, y = lastArgs[-2]

                    # mirror about current point
                    return currentX + (currentX-x), currentY + (currentY-y)

                else:
                    # defaults to current point
                    return currentX, currentY

            # store sub paths in 'paths' list
            shape = Path()

            # keep track of current point and path start point
            startX, startY = 0., 0.
            currentX, currentY = 0., 0.

            # keep track of last operation
            lastOp = None
            lastArgs = None

            # avoid empty path data
            data = node.get('d')
            if data is None or len(data) == 0:
                return

            for op, args in parsePath.iterparse(data):
                if op == 'z' or op == 'Z':
                    # close path or subpath
                    shape.closePath()

                    # next sub path starts at begining of current path
                    currentX, currentY = startX, startY

                elif op == 'M':
                    # moveto absolute
                    if lastOp is not None and lastOp not in ('z', 'Z'):
                        # close sub path
                        shape.closePath()

                    x, y = args[0]
                    shape.moveTo(x, y)

                    startX, startY = x, y

                    # multiple moveto arge result in line
                    for x, y in args[1:]:
                        shape.lineTo(x, y)

                    currentX, currentY = x, y

                elif op == 'm':
                    if lastOp is not None and lastOp not in ('z', 'Z'):
                        # close sub path
                        shape.closePath()

                    # moveto relative
                    rx, ry = args[0]
                    x, y = currentX + rx, currentY + ry
                    shape.moveTo(x, y)

                    startX, startY = x, y
                    currentX, currentY = x, y

                    # multiple moveto arge result in line
                    for rx, ry in args[1:]:
                        x, y = currentX + rx, currentY + ry
                        shape.lineTo(x, y)
                        currentX, currentY = x, y

                elif op == 'L':
                    # lineto absolute
                    for x, y in args:
                        shape.lineTo(x, y)

                    currentX, currentY = x, y

                elif op == 'l':
                    # lineto relative
                    for rx, ry in args:
                        x, y = currentX + rx, currentY + ry
                        shape.lineTo(x, y)
                        currentX, currentY = x, y

                elif op == 'V':
                    # vertical line absolute
                    for y in args:
                        shape.lineTo(currentX, y)

                    currentY = y

                elif op == 'v':
                    # vertical line relative
                    for ry in args:
                        y = currentY + ry
                        shape.lineTo(currentX, y)
                        currentY = y

                elif op == 'H':
                    # horisontal line absolute
                    for x in args:
                        shape.lineTo(x, currentY)
                    currentX = x

                elif op == 'h':
                    # horisontal line relative
                    for rx in args:
                        x = currentX + rx
                        shape.lineTo(x, currentY)
                    currentX = x

                elif op == 'C':
                    # cubic bezier absolute
                    for p1, p2, p3 in zip(*([iter(args)] * 3)):
                        shape.curveTo(*(p1 + p2 + p3))
                        currentX, currentY = p3

                elif op == 'c':
                    # cubic bezier relative
                    for pnts in zip(*([iter(args)] * 3)):
                        (x1, y1), (x2, y2), (x3, y3) = pnts
                        pnts = tuple((p[0] + currentX, p[1] + currentY) for p in pnts)
                        shape.curveTo(*(pnts[0] + pnts[1] + pnts[2]))
                        currentX, currentY = pnts[2]
                        lastOp = op
                        lastArgs = pnts
                    continue

                elif op == 'S':
                    # shorthand cubic bezier absolute
                    for p2, p3 in zip(*([iter(args)] * 2)):
                        x1, y1 = prevCtrl(lastOp, lastArgs, currentX, currentY)
                        x2, y2 = p2
                        x3, y3 = p3
                        shape.curveTo(x1, y1, x2, y2, x3, y3)
                        lastOp = op
                        lastArgs = (x2, y2), (x3, y3)
                        currentX, currentY = x3, y3
                    continue

                elif op == 's':
                    # shorthand cubic bezier relative
                    for p2, p3 in zip(*([iter(args)] * 2)):
                        x1, y1 = prevCtrl(lastOp, lastArgs, currentX, currentY)
                        x2, y2 = p2
                        x2, y2 = x2 + currentX, y2 + currentY
                        x3, y3 = p3
                        x3, y3 = x3 + currentX, y3 + currentY
                        shape.curveTo(x1, y1, x2, y2, x3, y3)
                        currentX, currentY = x3, y3
                        lastOp = op
                        lastArgs = (x1, y1), (x2, y2), (x3, y3)
                    continue

                elif op == 'Q':
                    # quadratic bezier absolute
                    for p2, p3 in zip(*([iter(args)] * 2)):
                        x1, y1 = currentX, currentY
                        x2, y2 = p2
                        x3, y3 = p3
                        ctrls = convertQuadratic((x1, y1), (x2, y2), (x3, y3))
                        shape.curveTo(*ctrls)
                        currentX, currentY = x3, y3
                    lastOp = op
                    lastArgs = (x2, y2), (x3, y3)
                    continue

                elif op == 'q':
                    # quadratic bezier relative
                    for p2, p3 in zip(*([iter(args)] * 2)):
                        x1, y1 = currentX, currentY
                        x2, y2 = p2
                        x2, y2 = x2 + currentX, y2 + currentY
                        x3, y3 = p3
                        x3, y3 = x3 + currentX, y3 + currentY
                        ctrls = convertQuadratic((x1, y1), (x2, y2), (x3, y3))
                        shape.curveTo(*ctrls)
                        currentX, currentY = x3, y3

                    lastOp = op
                    lastArgs = (x2, y2), (x3, y3)
                    continue

                elif op == 'T':
                    # shorthand quadratic bezier absolute
                    for i in range(len(args)):
                        x1, y1 = currentX, currentY
                        x2, y2 = prevCtrl(lastOp, lastArgs, currentX, currentY)
                        x3, y3 = args[i]
                        ctrls = convertQuadratic((x1, y1), (x2, y2), (x3, y3))
                        shape.curveTo(*ctrls)
                        currentX, currentY = x3, y3
                        lastOp = op
                        lastArgs = (x2, y2), (x3, y3)

                    continue

                elif op == 't':
                    # shorthand quadratic bezier relative
                    for i in range(len(args)):
                        x1, y1 = currentX, currentY
                        x2, y2 = prevCtrl(lastOp, lastArgs, currentX, currentY)
                        x3, y3 = args[i]
                        x3, y3 = x3 + currentX, y3 + currentY
                        ctrls = convertQuadratic((x1, y1), (x2, y2), (x3, y3))
                        shape.curveTo(*ctrls)
                        currentX, currentY = x3, y3
                        lastOp = op
                        lastArgs = (x2, y2), (x3, y3)

                    continue

                elif op == 'A' or op == 'a':
                    # elliptic arc missing
                    continue

                lastOp = op
                lastArgs = args

            # check if fill applies to path
            fill = None
            if node.get('fill'):
                # inline style
                fill = node.get('fill')
            else:
                # try local style
                if node.get('style'):
                    style = parseStyle.parse(node.get('style'))

                    if 'fill' in style:
                        fill = style['fill']

                # try global style
                if fill is None:
                    style = self.styles[self.level]

                    if 'fill' in style:
                        fill = style['fill']
                    else:
                        fill = 'none'

            # hack because RLG has no "semi-closed" paths...
            if lastOp == 'z' or lastOp == 'Z' or fill == 'none':
                self.addShape(parent, node, shape)

            else:
                group = Group()
                strokeshape = shape.copy()
                self.addShape(group, node, strokeshape, fill='none')
                shape.closePath()
                self.addShape(group, node, shape, stroke='none')
                self.addShape(parent, node, group)

        elif node.tag == self.SVG_TITLE or node.tag == self.SVG_DESC:
            # Skip non-graphics elements
            return

    def addShape(self, parent, node, shape, **kwargs):
        self.applyStyleToShape(shape, node, **kwargs)

        transform = node.get('transform')
        if transform:
            # transform can only be applied to a group
            if isinstance(node, Group):
                group = node
            else:
                group = Group()

            for op in parseTransform.iterparse(transform):
                self.applyTransformOnGroup(group, op)

            if not isinstance(node, Group):
                group.add(shape)

            parent.add(group)

        else:
            parent.add(shape)

        return shape

    def nodeStyle(self, node, style=None):
        if style is None:
            style = {}

        # update with local style
        if node.get('style'):
            localstyle = parseStyle.parse(node.get('style'))
            for name, value in localstyle.items():
                style[name] = value

        # update with inline style
        for name in STYLE_NAMES:
            value = node.get(name)
            if value:
                style[name] = value

        return style

    def applyStyleToShape(self, shape, node, **kwargs):
        # fetch global styles for this level
        globalstyle = self.styles[self.level]

        # update with node style
        localstyle = self.nodeStyle(node)

        for name in STYLES:
            # avoid text styles if not a String subclass
            if not isinstance(shape, String) and name in STYLES_FONT:
                continue

            value = None
            if name in kwargs:
                value = kwargs[name]
            elif name in localstyle:
                value = localstyle[name]
            elif name in globalstyle:
                value = globalstyle[name]
            else:
                continue

            # handle 'currentColor'
            if value == 'currentColor':
                if 'color' in localstyle:
                    value = localstyle['color']
                elif 'color' in globalstyle:
                    value = globalstyle['color']

            # fetch shape attribute name and converter
            attrname, converter = STYLES[name]

            # Sett attribute of shape
            if hasattr(shape, attrname):
                setattr(shape, attrname, converter(value))

        # defaults
        if isinstance(shape, String):
            if shape.fillColor is None:
                shape.fillColor = colors.black

        elif isinstance(shape, (Line, PolyLine)):
            if shape.strokeColor is None:
                shape.strokeColor = colors.black

        elif isinstance(shape, (Rect, Circle, Ellipse, Polygon)):
            if shape.fillColor is None and shape.strokeColor is None:
                shape.strokeColor = colors.black

    def applyTransformOnGroup(self, group, transform):
        op, args = transform

        if op == 'scale':
            group.scale(*args)

        elif op == 'translate':
            group.translate(*args)

        elif op == 'rotate':
            if len(args) == 2:
                angle, center = args
                if center:
                    cx, cy = center
                    group.translate(cx, cy)
                    group.rotate(angle)
                    group.translate(-cx, -cy)
                else:
                    group.rotate(angle)

        elif op == "skewX":
            angle = args[0]
            group.skew(angle, 0)

        elif op == "skewY":
            angle = args[0]
            group.skew(0, angle)

        elif op == "matrix":
            group.transform = mmult(group.transform, args)


def readFile(filename):
    """
    Open svg file and return root xml object
    """
    GZIPMAGIC = '\037\213'

    try:
        fh = open(filename, 'rb')
    except IOError:
        raise SVGError("could not open file '%s' for reading" % filename)

    # test for gzip compression
    magic = fh.read(2)
    fh.close()

    if magic == GZIPMAGIC:
        svg = etree.parse(GzipFile(filename, 'r'))
    else:
        svg = etree.parse(filename)

    root = svg.getroot()

    if not root.tag == Renderer.SVG_ROOT:
        raise SVGError("Expected SVG fragment as root object")

    return root


def svg2rlg(filename):
    """
    Open svg file and return reportlab drawing object
    """
    xml = readFile(filename)
    renderer = Renderer(filename)

    return renderer.render(xml)


if __name__ == "__main__":
    import sys
    import os

    #sys.argv.append('compliance/paths-data-15-t.svg')

    source = sys.argv[1]
    path, filename = os.path.split(source)
    name, ext = os.path.splitext(filename)

    drawing = svg2rlg(source)
    drawing.save(formats=['pdf'], outDir='.', fnRoot=name)

    os.startfile(name + '.pdf')
