Skip to content
Snippets Groups Projects
import_svg.py 47.3 KiB
Newer Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

import re
import xml.dom.minidom
from math import cos, sin, tan, atan2, pi, ceil

import bpy
from mathutils import Vector, Matrix

from . import svg_colors

#### Common utilities ####

# TODO: "em" and "ex" aren't actually supported
SVGUnits = {"": 1.0,
            "px": 1.0,
            "in": 90.0,
            "mm": 90.0 / 25.4,
            "cm": 90.0 / 2.54,
            "pt": 1.25,
            "pc": 15.0,
            "em": 1.0,
            "ex": 1.0,
            "INVALID": 1.0,  # some DocBook files contain this
            }
SVGEmptyStyles = {'useFill': None,
                  'fill': None}

Sergey Sharybin's avatar
Sergey Sharybin committed
def SVGParseFloat(s, i=0):
    """
    Parse first float value from string

    Returns value as string
    """

    start = i
    n = len(s)
    token = ''

    # Skip leading whitespace characters
Sergey Sharybin's avatar
Sergey Sharybin committed
    while i < n and (s[i].isspace() or s[i] == ','):
        i += 1

    if i == n:
Sergey Sharybin's avatar
Sergey Sharybin committed

    # Read sign
    if s[i] == '-':
        token += '-'
        i += 1
    elif s[i] == '+':
        i += 1

    # Read integer part
    if s[i].isdigit():
        while i < n and s[i].isdigit():
            token += s[i]
            i += 1

    # Fractional part
    if i < n and s[i] == '.':
        token += '.'
        i += 1

        if s[i].isdigit():
            while i < n and s[i].isdigit():
                token += s[i]
                i += 1
        elif s[i].isspace() or s[i] == ',':
            # Inkscape sometimes uses qeird float format with missed
            # fractional part after dot. Suppose zero fractional part
            # for this case
            pass
Sergey Sharybin's avatar
Sergey Sharybin committed
        else:
            raise Exception('Invalid float value near ' + s[start:start + 10])

    # Degree
    if  i < n and (s[i] == 'e' or s[i] == 'E'):
        token += s[i]
        i += 1
        if s[i] == '+' or s[i] == '-':
            token += s[i]
            i += 1

            if s[i].isdigit():
                while i < n and s[i].isdigit():
                    token += s[i]
                    i += 1
            else:
                raise Exception('Invalid float value near ' +
                    s[start:start + 10])
        else:
            raise Exception('Invalid float value near ' + s[start:start + 10])

def SVGCreateCurve():
    """
    Create new curve object to hold splines in
    """

    cu = bpy.data.curves.new("Curve", 'CURVE')
    obj = bpy.data.objects.new("Curve", cu)
    bpy.context.scene.objects.link(obj)

    return obj


def SVGFinishCurve():
    """
    Finish curve creation
    """

    pass


def SVGFlipHandle(x, y, x1, y1):
    """
    Flip handle around base point
    """

    x = x + (x - x1)
    y = y + (y - y1)

    return x, y


def SVGParseCoord(coord, size):
    """
    Parse coordinate component to common basis

    Needed to handle coordinates set in cm, mm, iches..
    """

    token, last_char = SVGParseFloat(coord)
Sergey Sharybin's avatar
Sergey Sharybin committed
    val = float(token)
    unit = coord[last_char:].strip()  # strip() in case there is a space

    if unit == '%':
        return float(size) / 100.0 * val
    else:
        return val * SVGUnits[unit]

    return val


def SVGRectFromNode(node, context):
    """
    Get display rectangle from node
    """

    w = context['rect'][0]
    h = context['rect'][1]

    if node.getAttribute('viewBox'):
        viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
        w = SVGParseCoord(viewBox[2], w)
        h = SVGParseCoord(viewBox[3], h)
    else:
        if node.getAttribute('width'):
            w = SVGParseCoord(node.getAttribute('width'), w)

        if node.getAttribute('height'):
            h = SVGParseCoord(node.getAttribute('height'), h)

    return (w, h)


def SVGMatrixFromNode(node, context):
    """
    Get transformation matrix from given node
    """

Sergey Sharybin's avatar
Sergey Sharybin committed
    tagName = node.tagName.lower()
    tags = ['svg:svg', 'svg:use', 'svg:symbol']

    if tagName not in tags and 'svg:' + tagName not in tags:
        return Matrix()

    rect = context['rect']

    m = Matrix()
    x = SVGParseCoord(node.getAttribute('x') or '0', rect[0])
    y = SVGParseCoord(node.getAttribute('y') or '0', rect[1])
    w = SVGParseCoord(node.getAttribute('width') or str(rect[0]), rect[0])
    h = SVGParseCoord(node.getAttribute('height') or str(rect[1]), rect[1])

    m = Matrix.Translation(Vector((x, y, 0.0)))
    if len(context['rects']) > 1:
        m = m * Matrix.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0)))
        m = m * Matrix.Scale(h / rect[1], 4, Vector((0.0, 1.0, 0.0)))

    if node.getAttribute('viewBox'):
        viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
        vx = SVGParseCoord(viewBox[0], w)
        vy = SVGParseCoord(viewBox[1], h)
        vw = SVGParseCoord(viewBox[2], w)
        vh = SVGParseCoord(viewBox[3], h)

Sergey Sharybin's avatar
Sergey Sharybin committed
        sx = w / vw
        sy = h / vh
        scale = min(sx, sy)

        tx = (w - vw * scale) / 2
        ty = (h - vh * scale) / 2
        m = m * Matrix.Translation(Vector((tx, ty, 0.0)))
Sergey Sharybin's avatar
Sergey Sharybin committed

        m = m * Matrix.Translation(Vector((-vx, -vy, 0.0)))
        m = m * Matrix.Scale(scale, 4, Vector((1.0, 0.0, 0.0)))
        m = m * Matrix.Scale(scale, 4, Vector((0.0, 1.0, 0.0)))

    return m


def SVGParseTransform(transform):
    """
    Parse transform string and return transformation matrix
    """

    m = Matrix()
    r = re.compile('\s*([A-z]+)\s*\((.*?)\)')

    for match in r.finditer(transform):
        func = match.group(1)
        params = match.group(2)
        params = params.replace(',', ' ').split()

        proc = SVGTransforms.get(func)
        if proc is None:
            raise Exception('Unknown trasnform function: ' + func)

        m = m * proc(params)

    return m


def SVGGetMaterial(color, context):
    """
    Get material for specified color
    """

    materials = context['materials']
Sergey Sharybin's avatar
Sergey Sharybin committed
    rgb_re = re.compile('^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')

    if color in materials:
        return materials[color]

    diff = None
    if color.startswith('#'):
        color = color[1:]

        if len(color) == 3:
            color = color[0] * 2 + color[1] * 2 + color[2] * 2

        diff = (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
    elif color in svg_colors.SVGColors:
        diff = svg_colors.SVGColors[color]
Sergey Sharybin's avatar
Sergey Sharybin committed
    elif rgb_re.match(color):
Campbell Barton's avatar
Campbell Barton committed
        c = rgb_re.findall(color)[0]
Sergey Sharybin's avatar
Sergey Sharybin committed
        diff = (float(c[0]), float(c[1]), float(c[2]))
    else:
        return None

    mat = bpy.data.materials.new(name='SVGMat')
Sergey Sharybin's avatar
Sergey Sharybin committed
    mat.diffuse_color = ([x / 255.0 for x in diff])

    materials[color] = mat

    return mat


def SVGTransformTranslate(params):
    """
    translate SVG transform command
    """

    tx = float(params[0])
    ty = float(params[1]) if len(params) > 1 else 0.0
    return Matrix.Translation(Vector((tx, ty, 0.0)))


def SVGTransformMatrix(params):
    """
    matrix SVG transform command
    """

    a = float(params[0])
    b = float(params[1])
    c = float(params[2])
    d = float(params[3])
    e = float(params[4])
    f = float(params[5])

    return Matrix(((a, c, 0.0, e),
                   (b, d, 0.0, f),
                   (0, 0, 1.0, 0),
                   (0, 0, 0.0, 1)))


def SVGTransformScale(params):
    """
    scale SVG transform command
    """

    sx = float(params[0])
    sy = float(params[1]) if len(params) > 1 else sx
    m = m * Matrix.Scale(sx, 4, Vector((1.0, 0.0, 0.0)))
    m = m * Matrix.Scale(sy, 4, Vector((0.0, 1.0, 0.0)))

    return m


def SVGTransformSkewX(params):
    """
    skewX SVG transform command
    """

    ang = float(params[0]) * pi / 180.0

    return Matrix(((1.0, 0.0, 0.0),
                  (tan(ang), 1.0, 0.0),
                  (0.0, 0.0, 1.0))).to_4x4()


def SVGTransformSkewY(params):
    """
    skewX SVG transform command
    """

    ang = float(params[0]) * pi / 180.0

    return Matrix(((1.0, tan(ang), 0.0),
                  (0.0, 1.0, 0.0),
                  (0.0, 0.0, 1.0))).to_4x4()


def SVGTransformRotate(params):
    """
    skewX SVG transform command
    """

    ang = float(params[0]) * pi / 180.0
    cx = cy = 0.0
    if len(params) >= 3:
        cx = float(params[1])
        cy = float(params[2])

    tm = Matrix.Translation(Vector((cx, cy, 0.0)))
    rm = Matrix.Rotation(ang, 4, Vector((0.0, 0.0, 1.0)))

    return tm * rm * tm.inverted()

SVGTransforms = {'translate': SVGTransformTranslate,
                 'scale': SVGTransformScale,
                 'skewX': SVGTransformSkewX,
                 'skewY': SVGTransformSkewY,
                 'matrix': SVGTransformMatrix,
                 'rotate': SVGTransformRotate}

Sergey Sharybin's avatar
Sergey Sharybin committed

def SVGParseStyles(node, context):
    """
    Parse node to get different styles for displaying geometries
    (materilas, filling flags, etc..)
    """

    styles = SVGEmptyStyles.copy()
Sergey Sharybin's avatar
Sergey Sharybin committed

    style = node.getAttribute('style')
    if style:
        elems = style.split(';')
        for elem in elems:
            s = elem.split(':')

Sergey Sharybin's avatar
Sergey Sharybin committed
            if len(s) != 2:
                continue

Sergey Sharybin's avatar
Sergey Sharybin committed
            name = s[0].strip().lower()
            val = s[1].strip()

            if name == 'fill':
                val = val.lower()
                if val == 'none':
                    styles['useFill'] = False
                else:
                    styles['useFill'] = True
                    styles['fill'] = SVGGetMaterial(val, context)

Sergey Sharybin's avatar
Sergey Sharybin committed
        if styles['useFill'] is None:
            styles['useFill'] = True
            styles['fill'] = SVGGetMaterial('#000', context)

Sergey Sharybin's avatar
Sergey Sharybin committed
        return styles

    if styles['useFill'] is None:
Sergey Sharybin's avatar
Sergey Sharybin committed
        fill = node.getAttribute('fill')
Sergey Sharybin's avatar
Sergey Sharybin committed
        if fill:
            fill = fill.lower()
            if fill == 'none':
                styles['useFill'] = False
            else:
                styles['useFill'] = True
                styles['fill'] = SVGGetMaterial(fill, context)

    if styles['useFill'] is None and context['style']:
        styles = context['style'].copy()

Sergey Sharybin's avatar
Sergey Sharybin committed
    if styles['useFill'] is None:
        styles['useFill'] = True
        styles['fill'] = SVGGetMaterial('#000', context)

Sergey Sharybin's avatar
Sergey Sharybin committed
    return styles

#### SVG path helpers ####


class SVGPathData:
    """
    SVG Path data token supplier
    """

    __slots__ = ('_data',   # List of tokens
                 '_index',  # Index of current token in tokens list
                 '_len')    # Length of tokens list

    def __init__(self, d):
        """
        Initialize new path data supplier

        d - the definition of the outline of a shape
        """

Sergey Sharybin's avatar
Sergey Sharybin committed
        spaces = ' ,\t'
        commands = {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
Sergey Sharybin's avatar
Sergey Sharybin committed
        tokens = []

        i = 0
        n = len(d)
        while i < n:
            c = d[i]

            if c in spaces:
                pass
            elif c.lower() in commands:
                tokens.append(c)
            elif c in ['-', '.'] or c.isdigit():
                token, last_char = SVGParseFloat(d, i)
Sergey Sharybin's avatar
Sergey Sharybin committed
                tokens.append(token)
                # in most cases len(token) and (last_char - i) are the same
                # but with whitespace or ',' prefix they are not.

                i += (last_char - i) - 1
Sergey Sharybin's avatar
Sergey Sharybin committed

            i += 1

        self._data = tokens
Sergey Sharybin's avatar
Sergey Sharybin committed
        self._len = len(tokens)

    def eof(self):
        """
        Check if end of data reached
        """

        return self._index >= self._len

    def cur(self):
        """
        Return current token
        """

        if self.eof():
            return None

        return self._data[self._index]

Sergey Sharybin's avatar
Sergey Sharybin committed
    def lookupNext(self):
        """
        get next token without moving pointer
        """

        if self.eof():
            return None

        return self._data[self._index]

    def next(self):
        """
        Return current token and go to next one
        """

        if self.eof():
            return None

        token = self._data[self._index]
        self._index += 1

        return token

    def nextCoord(self):
        """
        Return coordinate created from current token and move to next token
        """

        token = self.next()

        if token is None:
            return None

        return float(token)


class SVGPathParser:
    """
    Parser of SVG path data
    """

    __slots__ = ('_data',  # Path data supplird
                 '_point',  # Current point coorfinate
                 '_handle',  # Last handle coordinate
                 '_splines',  # List of all splies created during parsing
                 '_spline',  # Currently handling spline
                 '_commands')  # Hash of all supported path commands

    def __init__(self, d):
        """
        Initialize path parser

        d - the definition of the outline of a shape
        """

        self._data = SVGPathData(d)
        self._point = None   # Current point
        self._handle = None  # Last handle
        self._splines = []   # List of splines in path
        self._spline = None  # Current spline

        self._commands = {'M': self._pathMoveTo,
                          'L': self._pathLineTo,
                          'H': self._pathLineTo,
                          'V': self._pathLineTo,
                          'C': self._pathCurveToCS,
                          'S': self._pathCurveToCS,
                          'Q': self._pathCurveToQT,
                          'T': self._pathCurveToQT,
                          'A': self._pathCurveToA,
                          'Z': self._pathClose,

                          'm': self._pathMoveTo,
                          'l': self._pathLineTo,
                          'h': self._pathLineTo,
                          'v': self._pathLineTo,
                          'c': self._pathCurveToCS,
                          's': self._pathCurveToCS,
                          'q': self._pathCurveToQT,
                          't': self._pathCurveToQT,
                          'a': self._pathCurveToA,
                          'z': self._pathClose}

    def _getCoordPair(self, relative, point):
        """
        Get next coordinate pair
        """

        x = self._data.nextCoord()
        y = self._data.nextCoord()

        if relative and point is not None:
            x += point[0]
            y += point[1]

        return x, y

    def _appendPoint(self, x, y, handle_left=None, handle_left_type='VECTOR',
                    handle_right=None, handle_right_type='VECTOR'):
        """
        Append point to spline

        If there's no active spline, create one and set it's first point
        to current point coordinate
        """

        if self._spline is None:
            self._spline = {'points': [],
                            'closed': False}

            self._splines.append(self._spline)

Sergey Sharybin's avatar
Sergey Sharybin committed
        if len(self._spline['points']) > 0:
            # Not sure bout specifications, but Illustrator could create
            # last point at the same position, as start point (which was
            # reached by MoveTo command) to set needed handle coords.
            # It's also could use last point at last position to make path
            # filled.

            first = self._spline['points'][0]
            if abs(first['x'] - x) < 1e-6 and abs(first['y'] - y) < 1e-6:
                if handle_left is not None:
                    first['handle_left'] = handle_left
                    first['handle_left_type'] = 'FREE'

                if handle_left_type != 'VECTOR':
                    first['handle_left_type'] = handle_left_type

                if self._data.eof() or self._data.lookupNext().lower() == 'm':
                    self._spline['closed'] = True

                return

        point = {'x': x,
                 'y': y,

                 'handle_left': handle_left,
                 'handle_left_type': handle_left_type,

                 'handle_right': handle_right,
                 'handle_right_type': handle_right_type}

        self._spline['points'].append(point)

    def _updateHandle(self, handle=None, handle_type=None):
        """
        Update right handle of previous point when adding new point to spline
        """

        point = self._spline['points'][-1]

        if handle_type is not None:
            point['handle_right_type'] = handle_type

        if handle is not None:
            point['handle_right'] = handle

    def _pathMoveTo(self, code):
        """
        MoveTo path command
        """

        relative = code.islower()
        x, y = self._getCoordPair(relative, self._point)

        self._spline = None  # Flag to start new spline
        self._point = (x, y)

        cur = self._data.cur()
        while  cur is not None and not cur.isalpha():
            x, y = self._getCoordPair(relative, self._point)

            if self._spline is None:
                self._appendPoint(self._point[0], self._point[1])

            self._appendPoint(x, y)

            self._point = (x, y)
            cur = self._data.cur()

        self._handle = None

    def _pathLineTo(self, code):
        """
        LineTo path command
        """

        c = code.lower()

        cur = self._data.cur()
        while cur is not None and not cur.isalpha():
            if c == 'l':
                x, y = self._getCoordPair(code == 'l', self._point)
            elif c == 'h':
                x = self._data.nextCoord()
                y = self._point[1]
            else:
                x = self._point[0]
                y = self._data.nextCoord()

            if code == 'h':
                x += self._point[0]
            elif code == 'v':
                y += self._point[1]

            if self._spline is None:
                self._appendPoint(self._point[0], self._point[1])

            self._appendPoint(x, y)

            self._point = (x, y)
            cur = self._data.cur()

        self._handle = None

    def _pathCurveToCS(self, code):
        """
        Cubic BEZIER CurveTo  path command
        """

        c = code.lower()
        cur = self._data.cur()
        while cur is not None and not cur.isalpha():
            if c == 'c':
                x1, y1 = self._getCoordPair(code.islower(), self._point)
                x2, y2 = self._getCoordPair(code.islower(), self._point)
            else:
                if self._handle is not None:
                    x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
                                        self._handle[0], self._handle[1])
                else:
                    x1, y1 = self._point

                x2, y2 = self._getCoordPair(code.islower(), self._point)

            x, y = self._getCoordPair(code.islower(), self._point)

            if self._spline is None:
                self._appendPoint(self._point[0], self._point[1],
                    handle_left_type='FREE', handle_left=self._point,
                    handle_right_type='FREE', handle_right=(x1, y1))
            else:
                self._updateHandle(handle=(x1, y1), handle_type='FREE')

            self._appendPoint(x, y,
                handle_left_type='FREE', handle_left=(x2, y2),
                handle_right_type='FREE', handle_right=(x, y))

            self._point = (x, y)
            self._handle = (x2, y2)
            cur = self._data.cur()

    def _pathCurveToQT(self, code):
        """
        Qyadracic BEZIER CurveTo  path command
        """

        c = code.lower()
        cur = self._data.cur()

        while cur is not None and not cur.isalpha():
            if c == 'q':
                x1, y1 = self._getCoordPair(code.islower(), self._point)
            else:
                if self._handle is not None:
                    x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
                                        self._handle[0], self._handle[1])
                else:
                    x1, y1 = self._point

            x, y = self._getCoordPair(code.islower(), self._point)

            if self._spline is None:
                self._appendPoint(self._point[0], self._point[1],
                    handle_left_type='FREE', handle_left=self._point,
                    handle_right_type='FREE', handle_right=self._point)

            self._appendPoint(x, y,
                handle_left_type='FREE', handle_left=(x1, y1),
                handle_right_type='FREE', handle_right=(x, y))

            self._point = (x, y)
            self._handle = (x1, y1)
            cur = self._data.cur()

Campbell Barton's avatar
Campbell Barton committed
    def _calcArc(self, rx, ry, ang, fa, fs, x, y):
        """
        Calc arc paths

        Copied and adoptedfrom paths_svg2obj.py scring for Blender 2.49
        which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
        """

        cpx = self._point[0]
        cpy = self._point[1]
        rx = abs(rx)
        ry = abs(ry)
        px = abs((cos(ang) * (cpx - x) + sin(ang) * (cpy - y)) * 0.5) ** 2.0
        py = abs((cos(ang) * (cpy - y) - sin(ang) * (cpx - x)) * 0.5) ** 2.0
        rpx = rpy = 0.0

        if abs(rx) > 0.0:
            px = px / (rx ** 2.0)

        if abs(ry) > 0.0:
            rpy = py / (ry ** 2.0)

        pl = rpx + rpy
        if pl > 1.0:
            pl = pl ** 0.5
            rx *= pl
            ry *= pl

        carx = sarx = cary = sary = 0.0

        if abs(rx) > 0.0:
            carx = cos(ang) / rx
            sarx = sin(ang) / rx

        if abs(ry) > 0.0:
            cary = cos(ang) / ry
            sary = sin(ang) / ry

        x0 = carx * cpx + sarx * cpy
        y0 = -sary * cpx + cary * cpy
        x1 = carx * x + sarx * y
        y1 = -sary * x + cary * y
        d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)

        if abs(d) > 0.0:
            sq = 1.0 / d - 0.25
        else:
            sq = -0.25

        if sq < 0.0:
            sq = 0.0

        sf = sq ** 0.5
        if fs == fa:
            sf = -sf

        xc = 0.5 * (x0 + x1) - sf * (y1 - y0)
        yc = 0.5 * (y0 + y1) + sf * (x1 - x0)
        ang_0 = atan2(y0 - yc, x0 - xc)
        ang_1 = atan2(y1 - yc, x1 - xc)
        ang_arc = ang_1 - ang_0

        if ang_arc < 0.0 and fs == 1:
            ang_arc += 2.0 * pi
        elif ang_arc > 0.0 and fs == 0:
            ang_arc -= 2.0 * pi

        n_segs = int(ceil(abs(ang_arc * 2.0 / (pi * 0.5 + 0.001))))

        if self._spline is None:
            self._appendPoint(cpx, cpy,
                handle_left_type='FREE', handle_left=(cpx, cpy),
                handle_right_type='FREE', handle_right=(cpx, cpy))

        for i in range(n_segs):
            ang0 = ang_0 + i * ang_arc / n_segs
            ang1 = ang_0 + (i + 1) * ang_arc / n_segs
            ang_demi = 0.25 * (ang1 - ang0)
            t = 2.66666 * sin(ang_demi) * sin(ang_demi) / sin(ang_demi * 2.0)
            x1 = xc + cos(ang0) - t * sin(ang0)
            y1 = yc + sin(ang0) + t * cos(ang0)
            x2 = xc + cos(ang1)
            y2 = yc + sin(ang1)
            x3 = x2 + t * sin(ang1)
            y3 = y2 - t * cos(ang1)

            coord1 = ((cos(ang) * rx) * x1 + (-sin(ang) * ry) * y1,
                      (sin(ang) * rx) * x1 + (cos(ang) * ry) * y1)
            coord2 = ((cos(ang) * rx) * x3 + (-sin(ang) * ry) * y3,
                      (sin(ang) * rx) * x3 + (cos(ang) * ry) * y3)
            coord3 = ((cos(ang) * rx) * x2 + (-sin(ang) * ry) * y2,
                      (sin(ang) * rx) * x2 + (cos(ang) * ry) * y2)

            self._updateHandle(handle=coord1, handle_type='FREE')

            self._appendPoint(coord3[0], coord3[1],
                handle_left_type='FREE', handle_left=coord2,
                handle_right_type='FREE', handle_right=coord3)

    def _pathCurveToA(self, code):
        """
        Elliptical arc CurveTo path command
        """

        cur = self._data.cur()

        while cur is not None and not cur.isalpha():
            rx = float(self._data.next())
            ry = float(self._data.next())
            ang = float(self._data.next()) / 180 * pi
            fa = float(self._data.next())
            fs = float(self._data.next())
            x, y = self._getCoordPair(code.islower(), self._point)

Campbell Barton's avatar
Campbell Barton committed
            self._calcArc(rx, ry, ang, fa, fs, x, y)

            self._point = (x, y)
            self._handle = None
            cur = self._data.cur()

    def _pathClose(self, code):
        """
        Close path command
        """

        if self._spline:
            self._spline['closed'] = True

            cv = self._spline['points'][0]
            self._point = (cv['x'], cv['y'])

    def parse(self):
        """
        Execute parser
        """

        while not self._data.eof():
            code = self._data.next()
            cmd = self._commands.get(code)

            if cmd is None:
                raise Exception('Unknown path command: {0}' . format(code))

            cmd(code)

    def getSplines(self):
        """
        Get splines definitions
        """

        return self._splines


class SVGGeometry:
    """
    Abstract SVG geometry
    """

    __slots__ = ('_node',  # XML node for geometry
                 '_context',  # Global SVG context (holds matrices stack, i.e.)
                 '_creating')  # Flag if geometry is already creating
                               # for this node
                               # need to detect cycles for USE node

    def __init__(self, node, context):
        """
        Initialize SVG geometry
        """

        self._node = node
        self._context = context
        self._creating = False

        if hasattr(node, 'getAttribute'):
            defs = context['defines']

Campbell Barton's avatar
Campbell Barton committed
            attr_id = node.getAttribute('id')
            if attr_id and defs.get('#' + attr_id) is None:
                defs['#' + attr_id] = self

            className = node.getAttribute('class')
            if className and defs.get(className) is None:
                defs[className] = self

    def _pushRect(self, rect):
        """
        Push display rectangle
        """

        self._context['rects'].append(rect)
        self._context['rect'] = rect

    def _popRect(self):
        """
        Pop display rectangle
        """

        self._context['rect'] = self._context['rects'][-1]

    def _pushMatrix(self, matrix):
        """
        Push transformation matrix
        """

        self._context['transform'].append(matrix)
        self._context['matrix'] = self._context['matrix'] * matrix

    def _popMatrix(self):
        """
        Pop transformation matrix
        """