Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'io_curve_svg/import_svg.py')
-rw-r--r--io_curve_svg/import_svg.py1831
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'}