diff options
Diffstat (limited to 'io_curve_svg/import_svg.py')
-rw-r--r-- | io_curve_svg/import_svg.py | 1831 |
1 files changed, 1831 insertions, 0 deletions
diff --git a/io_curve_svg/import_svg.py b/io_curve_svg/import_svg.py new file mode 100644 index 00000000..231986e5 --- /dev/null +++ b/io_curve_svg/import_svg.py @@ -0,0 +1,1831 @@ +# ##### 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, + "mm": 90 / 25.4, + "cm": 90 / 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} + + +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 + while i < n and (s[i].isspace() or s[i] == ','): + i += 1 + + if i == n: + return None, i + + # 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 + 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]) + + return token, i + + +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) + val = float(token) + unit = coord[last_char:].strip() # strip() incase 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 + """ + + 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 = m.Translation(Vector((x, y, 0.0))) + if len(context['rects']) > 1: + m = m * m.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0))) + m = m * m.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) + + sx = w / vw + sy = h / vh + scale = min(sx, sy) + + tx = (w - vw * scale) / 2 + ty = (h - vh * scale) / 2 + m = m * m.Translation(Vector((tx, ty, 0.0))) + + m = m * m.Translation(Vector((-vx, -vy, 0.0))) + m = m * m.Scale(scale, 4, Vector((1.0, 0.0, 0.0))) + m = m * m.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'] + 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] + elif rgb_re.match(color): + c = rgb_re.findall(color)[0] + diff = (float(c[0]), float(c[1]), float(c[2])) + else: + return None + + mat = bpy.data.materials.new(name='SVGMat') + 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, b, 0.0, 0.0), + (c, d, 0.0, 0.0), + (0, 0, 1.0, 0.0), + (e, f, 0.0, 1.0))) + + +def SVGTransformScale(params): + """ + scale SVG transform command + """ + + sx = float(params[0]) + sy = float(params[1]) if len(params) > 1 else sx + + m = Matrix() + + m = m * m.Scale(sx, 4, Vector((1.0, 0.0, 0.0))) + m = m * m.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} + + +def SVGParseStyles(node, context): + """ + Parse node to get different styles for displaying geometries + (materilas, filling flags, etc..) + """ + + styles = SVGEmptyStyles.copy() + + style = node.getAttribute('style') + if style: + elems = style.split(';') + for elem in elems: + s = elem.split(':') + + if len(s) != 2: + continue + + 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) + + if styles['useFill'] is None: + styles['useFill'] = True + styles['fill'] = SVGGetMaterial('#000', context) + + return styles + + if styles['useFill'] is None: + fill = node.getAttribute('fill') + 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: + styles['useFill'] = True + styles['fill'] = SVGGetMaterial('#000', context) + + 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') # Lenght og tokens list + + def __init__(self, d): + """ + Initialize new path data supplier + + d - the definition of the outline of a shape + """ + + spaces = ' ,\t' + commands = ['m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'] + 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) + 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 + + i += 1 + + self._data = tokens + self._index = 0 + 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] + + 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) + + 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() + + 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 + """ + + c = code.lower() + 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) + + 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'] + + id = node.getAttribute('id') + if id and defs.get('#' + id) is None: + defs['#' + 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['rects'].pop + 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 + """ + + matrix = self._context['transform'].pop() + self._context['matrix'] = self._context['matrix'] * matrix.inverted() + + def _transformCoord(self, point): + """ + Transform SVG-file coords + """ + + v = Vector((point[0], point[1], 0.0)) + + return v * self._context['matrix'] + + def getNodeMatrix(self): + """ + Get transformation matrix of node + """ + + return SVGMatrixFromNode(self._node, self._context) + + def parse(self): + """ + Parse XML node to memory + """ + + pass + + def _doCreateGeom(self, instancing): + """ + Internal handler to create real geometries + """ + + pass + + def getTransformMatrix(self): + """ + Get matrix created from "transform" attribute + """ + + transform = self._node.getAttribute('transform') + + if transform: + return SVGParseTransform(transform) + + return None + + def createGeom(self, instancing): + """ + Create real geometries + """ + + if self._creating: + return + + self._creating = True + + matrix = self.getTransformMatrix() + if matrix is not None: + self._pushMatrix(matrix) + + self._doCreateGeom(instancing) + + if matrix is not None: + self._popMatrix() + + self._creating = False + + +class SVGGeometryContainer(SVGGeometry): + """ + Container of SVG geometries + """ + + __slots__ = ('_geometries') # List of chold geometries + + def __init__(self, node, context): + """ + Initialize SVG geometry container + """ + + super().__init__(node, context) + + self._geometries = [] + + def parse(self): + """ + Parse XML node to memory + """ + + for node in self._node.childNodes: + if type(node) is not xml.dom.minidom.Element: + continue + + ob = parseAbstractNode(node, self._context) + if ob is not None: + self._geometries.append(ob) + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + for geom in self._geometries: + geom.createGeom(instancing) + + def getGeometries(self): + """ + Get list of parsed geometries + """ + + return self._geometries + + +class SVGGeometryPATH(SVGGeometry): + """ + SVG path geometry + """ + + __slots__ = ('_splines', # List of splines after parsing + '_styles') # Styles, used for displaying + + def __init__(self, node, context): + """ + Initialize SVG path + """ + + super().__init__(node, context) + + self._splines = [] + self._styles = SVGEmptyStyles + + def parse(self): + """ + Parse SVG path node + """ + + d = self._node.getAttribute('d') + + pathParser = SVGPathParser(d) + pathParser.parse() + + self._splines = pathParser.getSplines() + self._styles = SVGParseStyles(self._node, self._context) + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + ob = SVGCreateCurve() + cu = ob.data + + if self._node.getAttribute('id'): + cu.name = self._node.getAttribute('id') + + if self._styles['useFill']: + cu.dimensions = '2D' + cu.materials.append(self._styles['fill']) + else: + cu.dimensions = '3D' + + for spline in self._splines: + act_spline = None + for point in spline['points']: + co = self._transformCoord((point['x'], point['y'])) + + if act_spline is None: + cu.splines.new('BEZIER') + + act_spline = cu.splines[-1] + act_spline.use_cyclic_u = spline['closed'] + else: + act_spline.bezier_points.add() + + bezt = act_spline.bezier_points[-1] + bezt.co = co + + bezt.handle_left_type = point['handle_left_type'] + if point['handle_left'] is not None: + handle = point['handle_left'] + bezt.handle_left = self._transformCoord(handle) + + bezt.handle_right_type = point['handle_right_type'] + if point['handle_right'] is not None: + handle = point['handle_right'] + bezt.handle_right = self._transformCoord(handle) + + SVGFinishCurve() + + +class SVGGeometryDEFS(SVGGeometryContainer): + """ + Container for referenced elements + """ + + def createGeom(self, instancing): + """ + Create real geometries + """ + + pass + + +class SVGGeometrySYMBOL(SVGGeometryContainer): + """ + Referenced element + """ + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + self._pushMatrix(self.getNodeMatrix()) + + super()._doCreateGeom(False) + + self._popMatrix() + + def createGeom(self, instancing): + """ + Create real geometries + """ + + if not instancing: + return + + super().createGeom(instancing) + + +class SVGGeometryG(SVGGeometryContainer): + """ + Geometry group + """ + + pass + + +class SVGGeometryUSE(SVGGeometry): + """ + User of referenced elements + """ + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + geometries = [] + ref = self._node.getAttribute('xlink:href') + geom = self._context['defines'].get(ref) + + if geom is not None: + rect = SVGRectFromNode(self._node, self._context) + self._pushRect(rect) + + self._pushMatrix(self.getNodeMatrix()) + + geom.createGeom(True) + + self._popMatrix() + + self._popRect() + + +class SVGGeometryRECT(SVGGeometry): + """ + SVG rectangle + """ + + __slots__ = ('_rect', # coordinate and domensions of rectangle + '_radius', # Rounded corner radiuses + '_styles') # Styles, used for displaying + + def __init__(self, node, context): + """ + Initialize new rectangle + """ + + super().__init__(node, context) + + self._rect = ('0', '0', '0', '0') + self._radius = ('0', '0') + self._styles = SVGEmptyStyles + + def parse(self): + """ + Parse SVG rectangle node + """ + + self._styles = SVGParseStyles(self._node, self._context) + + rect = [] + for attr in ['x', 'y', 'width', 'height']: + val = self._node.getAttribute(attr) + rect.append(val or '0') + + self._rect = (rect) + + rx = self._node.getAttribute('rx') + ry = self._node.getAttribute('ry') + + self._radius = (rx, ry) + + def _appendCorner(self, spline, coord, firstTime, rounded): + """ + Append new corner to rectangle + """ + + handle = None + if len(coord) == 3: + handle = self._transformCoord(coord[2]) + coord = (coord[0], coord[1]) + + co = self._transformCoord(coord) + + if not firstTime: + spline.bezier_points.add() + + bezt = spline.bezier_points[-1] + bezt.co = co + + if rounded: + if handle: + bezt.handle_left_type = 'VECTOR' + bezt.handle_right_type = 'FREE' + + bezt.handle_right = handle + else: + bezt.handle_left_type = 'FREE' + bezt.handle_right_type = 'VECTOR' + bezt.handle_left = co + + else: + bezt.handle_left_type = 'VECTOR' + bezt.handle_right_type = 'VECTOR' + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + # Run-time parsing -- percents would be correct only if + # parsing them now + crect = self._context['rect'] + rect = [] + + for i in range(4): + rect.append(SVGParseCoord(self._rect[i], crect[i % 2])) + + r = self._radius + rx = ry = 0.0 + + if r[0] and r[1]: + rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2) + ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2) + elif r[0]: + rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2) + ry = min(rx, rect[3] / 2) + rx = ry = min(rx, ry) + elif r[1]: + ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2) + rx = min(ry, rect[2] / 2) + rx = ry = min(rx, ry) + + radius = (rx, ry) + + # Geometry creation + ob = SVGCreateCurve() + cu = ob.data + + if self._styles['useFill']: + cu.dimensions = '2D' + cu.materials.append(self._styles['fill']) + else: + cu.dimensions = '3D' + + cu.splines.new('BEZIER') + + spline = cu.splines[-1] + spline.use_cyclic_u = True + + x, y = rect[0], rect[1] + w, h = rect[2], rect[3] + rx, ry = radius[0], radius[1] + rounded = False + + if rx or ry: + # + # 0 _______ 1 + # / \ + # / \ + # 7 2 + # | | + # | | + # 6 3 + # \ / + # \ / + # 5 _______ 4 + # + + # Optional third component -- right handle coord + coords = [(x + rx, y), + (x + w - rx, y, (x + w, y)), + (x + w, y + ry), + (x + w, y + h - ry, (x + w, y + h)), + (x + w - rx, y + h), + (x + rx, y + h, (x, y + h)), + (x, y + h - ry), + (x, y + ry, (x, y))] + + rounded = True + else: + coords = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)] + + firstTime = True + for coord in coords: + self._appendCorner(spline, coord, firstTime, rounded) + firstTime = False + + SVGFinishCurve() + + +class SVGGeometryELLIPSE(SVGGeometry): + """ + SVG ellipse + """ + + __slots__ = ('_cx', # X-coordinate of center + '_cy', # Y-coordinate of center + '_rx', # X-axis radius of circle + '_ry', # Y-axis radius of circle + '_styles') # Styles, used for displaying + + def __init__(self, node, context): + """ + Initialize new ellipse + """ + + super().__init__(node, context) + + self._cx = '0.0' + self._cy = '0.0' + self._rx = '0.0' + self._ry = '0.0' + self._styles = SVGEmptyStyles + + def parse(self): + """ + Parse SVG ellipse node + """ + + self._styles = SVGParseStyles(self._node, self._context) + + self._cx = self._node.getAttribute('cx') or '0' + self._cy = self._node.getAttribute('cy') or '0' + self._rx = self._node.getAttribute('rx') or '0' + self._ry = self._node.getAttribute('ry') or '0' + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + # Run-time parsing -- percents would be correct only if + # parsing them now + crect = self._context['rect'] + + cx = SVGParseCoord(self._cx, crect[0]) + cy = SVGParseCoord(self._cy, crect[1]) + rx = SVGParseCoord(self._rx, crect[0]) + ry = SVGParseCoord(self._ry, crect[1]) + + if not rx or not ry: + # Automaic handles will work incorrect in this case + return + + # Create circle + ob = SVGCreateCurve() + cu = ob.data + + if self._styles['useFill']: + cu.dimensions = '2D' + cu.materials.append(self._styles['fill']) + else: + cu.dimensions = '3D' + + coords = [((cx - rx, cy), + (cx - rx, cy + ry * 0.552), + (cx - rx, cy - ry * 0.552)), + + ((cx, cy - ry), + (cx - rx * 0.552, cy - ry), + (cx + rx * 0.552, cy - ry)), + + ((cx + rx, cy), + (cx + rx, cy - ry * 0.552), + (cx + rx, cy + ry * 0.552)), + + ((cx, cy + ry), + (cx + rx * 0.552, cy + ry), + (cx - rx * 0.552, cy + ry))] + + spline = None + for coord in coords: + co = self._transformCoord(coord[0]) + handle_left = self._transformCoord(coord[1]) + handle_right = self._transformCoord(coord[2]) + + if spline is None: + cu.splines.new('BEZIER') + spline = cu.splines[-1] + spline.use_cyclic_u = True + else: + spline.bezier_points.add() + + bezt = spline.bezier_points[-1] + bezt.co = co + bezt.handle_left_type = 'FREE' + bezt.handle_right_type = 'FREE' + bezt.handle_left = handle_left + bezt.handle_right = handle_right + + SVGFinishCurve() + + +class SVGGeometryCIRCLE(SVGGeometryELLIPSE): + """ + SVG circle + """ + + def parse(self): + """ + Parse SVG circle node + """ + + self._styles = SVGParseStyles(self._node, self._context) + + self._cx = self._node.getAttribute('cx') or '0' + self._cy = self._node.getAttribute('cy') or '0' + + r = self._node.getAttribute('r') or '0' + self._rx = self._ry = r + + +class SVGGeometryLINE(SVGGeometry): + """ + SVG line + """ + + __slots__ = ('_x1', # X-coordinate of beginning + '_y1', # Y-coordinate of beginning + '_x2', # X-coordinate of ending + '_y2') # Y-coordinate of ending + + def __init__(self, node, context): + """ + Initialize new line + """ + + super().__init__(node, context) + + self._x1 = '0.0' + self._y1 = '0.0' + self._x2 = '0.0' + self._y2 = '0.0' + + def parse(self): + """ + Parse SVG line node + """ + + self._x1 = self._node.getAttribute('x1') or '0' + self._y1 = self._node.getAttribute('y1') or '0' + self._x2 = self._node.getAttribute('x2') or '0' + self._y2 = self._node.getAttribute('y2') or '0' + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + # Run-time parsing -- percents would be correct only if + # parsing them now + crect = self._context['rect'] + + x1 = SVGParseCoord(self._x1, crect[0]) + y1 = SVGParseCoord(self._y1, crect[1]) + x2 = SVGParseCoord(self._x2, crect[0]) + y2 = SVGParseCoord(self._y2, crect[1]) + + # Create cline + ob = SVGCreateCurve() + cu = ob.data + + coords = [(x1, y1), (x2, y2)] + spline = None + + for coord in coords: + co = self._transformCoord(coord) + + if spline is None: + cu.splines.new('BEZIER') + spline = cu.splines[-1] + spline.use_cyclic_u = True + else: + spline.bezier_points.add() + + bezt = spline.bezier_points[-1] + bezt.co = co + bezt.handle_left_type = 'VECTOR' + bezt.handle_right_type = 'VECTOR' + + SVGFinishCurve() + + +class SVGGeometryPOLY(SVGGeometry): + """ + Abstract class for handling poly-geometries + (polylines and polygons) + """ + + __slots__ = ('_points', # Array of points for poly geometry + '_styles', # Styles, used for displaying + '_closed') # Should generated curve be closed? + + def __init__(self, node, context): + """ + Initialize new poly geometry + """ + + super().__init__(node, context) + + self._points = [] + self._styles = SVGEmptyStyles + self._closed = False + + def parse(self): + """ + Parse poly node + """ + + self._styles = SVGParseStyles(self._node, self._context) + + points = self._node.getAttribute('points') + points = points.replace(',', ' ').replace('-', ' -') + points = points.split() + + prev = None + self._points = [] + + for p in points: + if prev is None: + prev = p + else: + self._points.append((float(prev), float(p))) + prev = None + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + ob = SVGCreateCurve() + cu = ob.data + + if self._closed and self._styles['useFill']: + cu.dimensions = '2D' + cu.materials.append(self._styles['fill']) + else: + cu.dimensions = '3D' + + spline = None + + for point in self._points: + co = self._transformCoord(point) + + if spline is None: + cu.splines.new('BEZIER') + spline = cu.splines[-1] + spline.use_cyclic_u = self._closed + else: + spline.bezier_points.add() + + bezt = spline.bezier_points[-1] + bezt.co = co + bezt.handle_left_type = 'VECTOR' + bezt.handle_right_type = 'VECTOR' + + SVGFinishCurve() + + +class SVGGeometryPOLYLINE(SVGGeometryPOLY): + """ + SVG polyline geometry + """ + + pass + + +class SVGGeometryPOLYGON(SVGGeometryPOLY): + """ + SVG polygon geometry + """ + + def __init__(self, node, context): + """ + Initialize new polygon geometry + """ + + super().__init__(node, context) + + self._closed = True + + +class SVGGeometrySVG(SVGGeometryContainer): + """ + Main geometry holder + """ + + def _doCreateGeom(self, instancing): + """ + Create real geometries + """ + + rect = SVGRectFromNode(self._node, self._context) + + self._pushMatrix(self.getNodeMatrix()) + self._pushRect(rect) + + super()._doCreateGeom(False) + + self._popRect() + self._popMatrix() + + +class SVGLoader(SVGGeometryContainer): + """ + SVG file loader + """ + + def getTransformMatrix(self): + """ + Get matrix created from "transform" attribute + """ + + # SVG document doesn't support transform specification + # it can't even hold attributes + + return None + + def __init__(self, filepath): + """ + Initialize SVG loader + """ + + node = xml.dom.minidom.parse(filepath) + + m = Matrix() + m = m * m.Scale(1.0 / 90.0, 4, Vector((1.0, 0.0, 0.0))) + m = m * m.Scale(-1.0 / 90.0, 4, Vector((0.0, 1.0, 0.0))) + + rect = (1, 1) + + self._context = {'defines': {}, + 'transform': [], + 'rects': [rect], + 'rect': rect, + 'matrix': m, + 'materials': {}} + + super().__init__(node, self._context) + + +svgGeometryClasses = { + 'svg': SVGGeometrySVG, + 'path': SVGGeometryPATH, + 'defs': SVGGeometryDEFS, + 'symbol': SVGGeometrySYMBOL, + 'use': SVGGeometryUSE, + 'rect': SVGGeometryRECT, + 'ellipse': SVGGeometryELLIPSE, + 'circle': SVGGeometryCIRCLE, + 'line': SVGGeometryLINE, + 'polyline': SVGGeometryPOLYLINE, + 'polygon': SVGGeometryPOLYGON, + 'g': SVGGeometryG} + + +def parseAbstractNode(node, context): + name = node.tagName.lower() + + if name.startswith('svg:'): + name = name[4:] + + geomClass = svgGeometryClasses.get(name) + + if geomClass is not None: + ob = geomClass(node, context) + ob.parse() + + return ob + + return None + + +def load_svg(filepath): + """ + Load specified SVG file + """ + + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode='OBJECT') + + loader = SVGLoader(filepath) + loader.parse() + loader.createGeom(False) + + +def load(operator, context, filepath=""): + + # error in code should raise exceptions but loading + # non SVG files can give useful messages. + try: + load_svg(filepath) + except (xml.parsers.expat.ExpatError, UnicodeEncodeError) as e: + import traceback + traceback.print_exc() + + operator.report({'WARNING'}, "Unable to parse XML, %s:%s for file %r" % (type(e).__name__, e, filepath)) + return {'CANCELLED'} + + return {'FINISHED'} |