Fix: Node Wrangler: new reroutes offset when output hidden
[blender-addons.git] / io_curve_svg / import_svg.py
blob5ebbfe607034ca56583e47d5f81cea2d3792c248
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import re
4 import xml.dom.minidom
5 from math import cos, sin, tan, atan2, pi, ceil
7 import bpy
8 from mathutils import Vector, Matrix
9 from bpy.app.translations import pgettext_tip as tip_
11 from . import svg_colors
12 from .svg_util import (units,
13 srgb_to_linearrgb,
14 check_points_equal,
15 parse_array_of_floats,
16 read_float)
18 #### Common utilities ####
20 SVGEmptyStyles = {'useFill': None,
21 'fill': None}
24 def SVGCreateCurve(context):
25 """
26 Create new curve object to hold splines in
27 """
29 cu = bpy.data.curves.new("Curve", 'CURVE')
30 obj = bpy.data.objects.new("Curve", cu)
32 context['collection'].objects.link(obj)
34 return obj
37 def SVGFinishCurve():
38 """
39 Finish curve creation
40 """
42 pass
45 def SVGFlipHandle(x, y, x1, y1):
46 """
47 Flip handle around base point
48 """
50 x = x + (x - x1)
51 y = y + (y - y1)
53 return x, y
56 def SVGParseCoord(coord, size):
57 """
58 Parse coordinate component to common basis
60 Needed to handle coordinates set in cm, mm, inches.
61 """
63 token, last_char = read_float(coord)
64 val = float(token)
65 unit = coord[last_char:].strip() # strip() in case there is a space
67 if unit == '%':
68 return float(size) / 100.0 * val
69 return val * units[unit]
72 def SVGRectFromNode(node, context):
73 """
74 Get display rectangle from node
75 """
77 w = context['rect'][0]
78 h = context['rect'][1]
80 if node.getAttribute('viewBox'):
81 viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
82 w = SVGParseCoord(viewBox[2], w)
83 h = SVGParseCoord(viewBox[3], h)
84 else:
85 if node.getAttribute('width'):
86 w = SVGParseCoord(node.getAttribute('width'), w)
88 if node.getAttribute('height'):
89 h = SVGParseCoord(node.getAttribute('height'), h)
91 return (w, h)
94 def SVGMatrixFromNode(node, context):
95 """
96 Get transformation matrix from given node
97 """
99 tagName = node.tagName.lower()
100 tags = ['svg:svg', 'svg:use', 'svg:symbol']
102 if tagName not in tags and 'svg:' + tagName not in tags:
103 return Matrix()
105 rect = context['rect']
106 has_user_coordinate = (len(context['rects']) > 1)
108 m = Matrix()
109 x = SVGParseCoord(node.getAttribute('x') or '0', rect[0])
110 y = SVGParseCoord(node.getAttribute('y') or '0', rect[1])
111 w = SVGParseCoord(node.getAttribute('width') or str(rect[0]), rect[0])
112 h = SVGParseCoord(node.getAttribute('height') or str(rect[1]), rect[1])
114 m = Matrix.Translation(Vector((x, y, 0.0)))
115 if has_user_coordinate:
116 if rect[0] != 0 and rect[1] != 0:
117 m = m @ Matrix.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0)))
118 m = m @ Matrix.Scale(h / rect[1], 4, Vector((0.0, 1.0, 0.0)))
120 if node.getAttribute('viewBox'):
121 viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
122 vx = SVGParseCoord(viewBox[0], w)
123 vy = SVGParseCoord(viewBox[1], h)
124 vw = SVGParseCoord(viewBox[2], w)
125 vh = SVGParseCoord(viewBox[3], h)
127 if vw == 0 or vh == 0:
128 return m
130 if has_user_coordinate or (w != 0 and h != 0):
131 sx = w / vw
132 sy = h / vh
133 scale = min(sx, sy)
134 else:
135 scale = 1.0
136 w = vw
137 h = vh
139 tx = (w - vw * scale) / 2
140 ty = (h - vh * scale) / 2
141 m = m @ Matrix.Translation(Vector((tx, ty, 0.0)))
143 m = m @ Matrix.Translation(Vector((-vx, -vy, 0.0)))
144 m = m @ Matrix.Scale(scale, 4, Vector((1.0, 0.0, 0.0)))
145 m = m @ Matrix.Scale(scale, 4, Vector((0.0, 1.0, 0.0)))
147 return m
150 def SVGParseTransform(transform):
152 Parse transform string and return transformation matrix
155 m = Matrix()
156 r = re.compile(r'\s*([A-z]+)\s*\((.*?)\)')
158 for match in r.finditer(transform):
159 func = match.group(1)
160 params = match.group(2)
161 params = params.replace(',', ' ').split()
163 proc = SVGTransforms.get(func)
164 if proc is None:
165 raise Exception('Unknown transform function: ' + func)
167 m = m @ proc(params)
169 return m
172 def SVGGetMaterial(color, context):
174 Get material for specified color
177 materials = context['materials']
178 rgb_re = re.compile(r'^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
180 if color in materials:
181 return materials[color]
183 diff = None
184 if color.startswith('#'):
185 color = color[1:]
187 if len(color) == 3:
188 color = color[0] * 2 + color[1] * 2 + color[2] * 2
190 diff = (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
191 elif color in svg_colors.SVGColors:
192 diff = svg_colors.SVGColors[color]
193 elif rgb_re.match(color):
194 c = rgb_re.findall(color)[0]
195 diff = (float(c[0]), float(c[1]), float(c[2]))
196 else:
197 return None
199 diffuse_color = ([x / 255.0 for x in diff])
201 if context['do_colormanage']:
202 diffuse_color[0] = srgb_to_linearrgb(diffuse_color[0])
203 diffuse_color[1] = srgb_to_linearrgb(diffuse_color[1])
204 diffuse_color[2] = srgb_to_linearrgb(diffuse_color[2])
206 mat = bpy.data.materials.new(name='SVGMat')
207 mat.diffuse_color = (*diffuse_color, 1.0)
209 materials[color] = mat
211 return mat
214 def SVGTransformTranslate(params):
216 translate SVG transform command
219 tx = float(params[0])
220 ty = float(params[1]) if len(params) > 1 else 0.0
222 return Matrix.Translation(Vector((tx, ty, 0.0)))
225 def SVGTransformMatrix(params):
227 matrix SVG transform command
230 a = float(params[0])
231 b = float(params[1])
232 c = float(params[2])
233 d = float(params[3])
234 e = float(params[4])
235 f = float(params[5])
237 return Matrix(((a, c, 0.0, e),
238 (b, d, 0.0, f),
239 (0, 0, 1.0, 0),
240 (0, 0, 0.0, 1)))
243 def SVGTransformScale(params):
245 scale SVG transform command
248 sx = float(params[0])
249 sy = float(params[1]) if len(params) > 1 else sx
251 m = Matrix()
253 m = m @ Matrix.Scale(sx, 4, Vector((1.0, 0.0, 0.0)))
254 m = m @ Matrix.Scale(sy, 4, Vector((0.0, 1.0, 0.0)))
256 return m
259 def SVGTransformSkewY(params):
261 skewY SVG transform command
264 ang = float(params[0]) * pi / 180.0
266 return Matrix(((1.0, 0.0, 0.0),
267 (tan(ang), 1.0, 0.0),
268 (0.0, 0.0, 1.0))).to_4x4()
271 def SVGTransformSkewX(params):
273 skewX SVG transform command
276 ang = float(params[0]) * pi / 180.0
278 return Matrix(((1.0, tan(ang), 0.0),
279 (0.0, 1.0, 0.0),
280 (0.0, 0.0, 1.0))).to_4x4()
283 def SVGTransformRotate(params):
285 skewX SVG transform command
288 ang = float(params[0]) * pi / 180.0
289 cx = cy = 0.0
291 if len(params) >= 3:
292 cx = float(params[1])
293 cy = float(params[2])
295 tm = Matrix.Translation(Vector((cx, cy, 0.0)))
296 rm = Matrix.Rotation(ang, 4, Vector((0.0, 0.0, 1.0)))
298 return tm @ rm @ tm.inverted()
300 SVGTransforms = {'translate': SVGTransformTranslate,
301 'scale': SVGTransformScale,
302 'skewX': SVGTransformSkewX,
303 'skewY': SVGTransformSkewY,
304 'matrix': SVGTransformMatrix,
305 'rotate': SVGTransformRotate}
308 def SVGParseStyles(node, context):
310 Parse node to get different styles for displaying geometries
311 (materials, filling flags, etc..)
314 styles = SVGEmptyStyles.copy()
316 style = node.getAttribute('style')
317 if style:
318 elems = style.split(';')
319 for elem in elems:
320 s = elem.split(':')
322 if len(s) != 2:
323 continue
325 name = s[0].strip().lower()
326 val = s[1].strip()
328 if name == 'fill':
329 val = val.lower()
330 if val == 'none':
331 styles['useFill'] = False
332 else:
333 styles['useFill'] = True
334 styles['fill'] = SVGGetMaterial(val, context)
336 if styles['useFill'] is None:
337 styles['useFill'] = True
338 styles['fill'] = SVGGetMaterial('#000', context)
340 return styles
342 if styles['useFill'] is None:
343 fill = node.getAttribute('fill')
344 if fill:
345 fill = fill.lower()
346 if fill == 'none':
347 styles['useFill'] = False
348 else:
349 styles['useFill'] = True
350 styles['fill'] = SVGGetMaterial(fill, context)
352 if styles['useFill'] is None and context['style']:
353 styles = context['style'].copy()
355 if styles['useFill'] is None:
356 styles['useFill'] = True
357 styles['fill'] = SVGGetMaterial('#000', context)
359 return styles
361 def id_names_from_node(node, ob):
362 if node.getAttribute('id'):
363 name = node.getAttribute('id')
364 ob.name = name
365 ob.data.name = name
367 #### SVG path helpers ####
370 class SVGPathData:
372 SVG Path data token supplier
375 __slots__ = ('_data', # List of tokens
376 '_index', # Index of current token in tokens list
377 '_len') # Length of tokens list
379 def __init__(self, d):
381 Initialize new path data supplier
383 d - the definition of the outline of a shape
386 spaces = ' ,\t'
387 commands = {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
388 current_command = ''
389 tokens = []
391 i = 0
392 n = len(d)
393 while i < n:
394 c = d[i]
396 if c in spaces:
397 pass
398 elif c.lower() in commands:
399 tokens.append(c)
400 current_command = c
401 arg_index = 1
402 elif c in ['-', '.'] or c.isdigit():
403 # Special case for 'a/A' commands.
404 # Arguments 4 and 5 are either 0 or 1 and might not
405 # be separated from the next argument with space or comma.
406 if current_command.lower() == 'a':
407 if arg_index % 7 in [4,5]:
408 token = d[i]
409 last_char = i + 1
410 else:
411 token, last_char = read_float(d, i)
412 else:
413 token, last_char = read_float(d, i)
415 arg_index += 1
416 tokens.append(token)
418 # in most cases len(token) and (last_char - i) are the same
419 # but with whitespace or ',' prefix they are not.
421 i += (last_char - i) - 1
423 i += 1
425 self._data = tokens
426 self._index = 0
427 self._len = len(tokens)
429 def eof(self):
431 Check if end of data reached
434 return self._index >= self._len
436 def cur(self):
438 Return current token
441 if self.eof():
442 return None
444 return self._data[self._index]
446 def lookupNext(self):
448 get next token without moving pointer
451 if self.eof():
452 return None
454 return self._data[self._index]
456 def next(self):
458 Return current token and go to next one
461 if self.eof():
462 return None
464 token = self._data[self._index]
465 self._index += 1
467 return token
469 def nextCoord(self):
471 Return coordinate created from current token and move to next token
474 token = self.next()
476 if token is None:
477 return None
479 return float(token)
482 class SVGPathParser:
484 Parser of SVG path data
487 __slots__ = ('_data', # Path data supplird
488 '_point', # Current point coordinate
489 '_handle', # Last handle coordinate
490 '_splines', # List of all splies created during parsing
491 '_spline', # Currently handling spline
492 '_commands', # Hash of all supported path commands
493 '_use_fill', # Splines would be filled, so expected to be closed
496 def __init__(self, d, use_fill):
498 Initialize path parser
500 d - the definition of the outline of a shape
503 self._data = SVGPathData(d)
504 self._point = None # Current point
505 self._handle = None # Last handle
506 self._splines = [] # List of splines in path
507 self._spline = None # Current spline
508 self._use_fill = use_fill
510 self._commands = {'M': self._pathMoveTo,
511 'L': self._pathLineTo,
512 'H': self._pathLineTo,
513 'V': self._pathLineTo,
514 'C': self._pathCurveToCS,
515 'S': self._pathCurveToCS,
516 'Q': self._pathCurveToQT,
517 'T': self._pathCurveToQT,
518 'A': self._pathCurveToA,
519 'Z': self._pathClose,
521 'm': self._pathMoveTo,
522 'l': self._pathLineTo,
523 'h': self._pathLineTo,
524 'v': self._pathLineTo,
525 'c': self._pathCurveToCS,
526 's': self._pathCurveToCS,
527 'q': self._pathCurveToQT,
528 't': self._pathCurveToQT,
529 'a': self._pathCurveToA,
530 'z': self._pathClose}
532 def _getCoordPair(self, relative, point):
534 Get next coordinate pair
537 x = self._data.nextCoord()
538 y = self._data.nextCoord()
540 if relative and point is not None:
541 x += point[0]
542 y += point[1]
544 return x, y
546 def _appendPoint(self, x, y, handle_left=None, handle_left_type='VECTOR',
547 handle_right=None, handle_right_type='VECTOR'):
549 Append point to spline
551 If there's no active spline, create one and set it's first point
552 to current point coordinate
555 if self._spline is None:
556 self._spline = {'points': [],
557 'closed': False}
559 self._splines.append(self._spline)
561 if len(self._spline['points']) > 0:
562 # Not sure about specifications, but Illustrator could create
563 # last point at the same position, as start point (which was
564 # reached by MoveTo command) to set needed handle coords.
565 # It's also could use last point at last position to make path
566 # filled.
568 first = self._spline['points'][0]
569 if check_points_equal((first['x'], first['y']), (x, y)):
570 if handle_left is not None:
571 first['handle_left'] = handle_left
572 first['handle_left_type'] = 'FREE'
574 if handle_left_type != 'VECTOR':
575 first['handle_left_type'] = handle_left_type
577 if self._data.eof() or self._data.lookupNext().lower() == 'm':
578 self._spline['closed'] = True
580 return
582 last = self._spline['points'][-1]
583 if last['handle_right_type'] == 'VECTOR' and handle_left_type == 'FREE':
584 last['handle_right'] = (last['x'], last['y'])
585 last['handle_right_type'] = 'FREE'
586 if last['handle_right_type'] == 'FREE' and handle_left_type == 'VECTOR':
587 handle_left = (x, y)
588 handle_left_type = 'FREE'
590 point = {'x': x,
591 'y': y,
593 'handle_left': handle_left,
594 'handle_left_type': handle_left_type,
596 'handle_right': handle_right,
597 'handle_right_type': handle_right_type}
599 self._spline['points'].append(point)
601 def _updateHandle(self, handle=None, handle_type=None):
603 Update right handle of previous point when adding new point to spline
606 point = self._spline['points'][-1]
608 if handle_type is not None:
609 point['handle_right_type'] = handle_type
611 if handle is not None:
612 point['handle_right'] = handle
614 def _pathMoveTo(self, code):
616 MoveTo path command
619 relative = code.islower()
620 x, y = self._getCoordPair(relative, self._point)
622 self._spline = None # Flag to start new spline
623 self._point = (x, y)
625 cur = self._data.cur()
626 while cur is not None and not cur.isalpha():
627 x, y = self._getCoordPair(relative, self._point)
629 if self._spline is None:
630 self._appendPoint(self._point[0], self._point[1])
632 self._appendPoint(x, y)
634 self._point = (x, y)
635 cur = self._data.cur()
637 self._handle = None
639 def _pathLineTo(self, code):
641 LineTo path command
644 c = code.lower()
646 cur = self._data.cur()
647 while cur is not None and not cur.isalpha():
648 if c == 'l':
649 x, y = self._getCoordPair(code == 'l', self._point)
650 elif c == 'h':
651 x = self._data.nextCoord()
652 y = self._point[1]
653 else:
654 x = self._point[0]
655 y = self._data.nextCoord()
657 if code == 'h':
658 x += self._point[0]
659 elif code == 'v':
660 y += self._point[1]
662 if self._spline is None:
663 self._appendPoint(self._point[0], self._point[1])
665 self._appendPoint(x, y)
667 self._point = (x, y)
668 cur = self._data.cur()
670 self._handle = None
672 def _pathCurveToCS(self, code):
674 Cubic BEZIER CurveTo path command
677 c = code.lower()
678 cur = self._data.cur()
679 while cur is not None and not cur.isalpha():
680 if c == 'c':
681 x1, y1 = self._getCoordPair(code.islower(), self._point)
682 x2, y2 = self._getCoordPair(code.islower(), self._point)
683 else:
684 if self._handle is not None:
685 x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
686 self._handle[0], self._handle[1])
687 else:
688 x1, y1 = self._point
690 x2, y2 = self._getCoordPair(code.islower(), self._point)
692 x, y = self._getCoordPair(code.islower(), self._point)
694 if self._spline is None:
695 self._appendPoint(self._point[0], self._point[1],
696 handle_left_type='FREE', handle_left=self._point,
697 handle_right_type='FREE', handle_right=(x1, y1))
698 else:
699 self._updateHandle(handle=(x1, y1), handle_type='FREE')
701 self._appendPoint(x, y,
702 handle_left_type='FREE', handle_left=(x2, y2),
703 handle_right_type='FREE', handle_right=(x, y))
705 self._point = (x, y)
706 self._handle = (x2, y2)
707 cur = self._data.cur()
709 def _pathCurveToQT(self, code):
711 Quadratic BEZIER CurveTo path command
714 c = code.lower()
715 cur = self._data.cur()
717 while cur is not None and not cur.isalpha():
718 if c == 'q':
719 x1, y1 = self._getCoordPair(code.islower(), self._point)
720 else:
721 if self._handle is not None:
722 x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
723 self._handle[0], self._handle[1])
724 else:
725 x1, y1 = self._point
727 x, y = self._getCoordPair(code.islower(), self._point)
729 if not check_points_equal((x, y), self._point):
730 if self._spline is None:
731 self._appendPoint(self._point[0], self._point[1],
732 handle_left_type='FREE', handle_left=self._point,
733 handle_right_type='FREE', handle_right=self._point)
735 self._appendPoint(x, y,
736 handle_left_type='FREE', handle_left=(x1, y1),
737 handle_right_type='FREE', handle_right=(x, y))
739 self._point = (x, y)
740 self._handle = (x1, y1)
741 cur = self._data.cur()
743 def _calcArc(self, rx, ry, ang, fa, fs, x, y):
745 Calc arc paths
747 Copied and adoptedfrom paths_svg2obj.py script for Blender 2.49
748 which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
751 cpx = self._point[0]
752 cpy = self._point[1]
753 rx = abs(rx)
754 ry = abs(ry)
755 px = abs((cos(ang) * (cpx - x) + sin(ang) * (cpy - y)) * 0.5) ** 2.0
756 py = abs((cos(ang) * (cpy - y) - sin(ang) * (cpx - x)) * 0.5) ** 2.0
757 rpx = rpy = 0.0
759 if abs(rx) > 0.0:
760 px = px / (rx ** 2.0)
762 if abs(ry) > 0.0:
763 rpy = py / (ry ** 2.0)
765 pl = rpx + rpy
766 if pl > 1.0:
767 pl = pl ** 0.5
768 rx *= pl
769 ry *= pl
771 carx = sarx = cary = sary = 0.0
773 if abs(rx) > 0.0:
774 carx = cos(ang) / rx
775 sarx = sin(ang) / rx
777 if abs(ry) > 0.0:
778 cary = cos(ang) / ry
779 sary = sin(ang) / ry
781 x0 = carx * cpx + sarx * cpy
782 y0 = -sary * cpx + cary * cpy
783 x1 = carx * x + sarx * y
784 y1 = -sary * x + cary * y
785 d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
787 if abs(d) > 0.0:
788 sq = 1.0 / d - 0.25
789 else:
790 sq = -0.25
792 if sq < 0.0:
793 sq = 0.0
795 sf = sq ** 0.5
796 if fs == fa:
797 sf = -sf
799 xc = 0.5 * (x0 + x1) - sf * (y1 - y0)
800 yc = 0.5 * (y0 + y1) + sf * (x1 - x0)
801 ang_0 = atan2(y0 - yc, x0 - xc)
802 ang_1 = atan2(y1 - yc, x1 - xc)
803 ang_arc = ang_1 - ang_0
805 if ang_arc < 0.0 and fs == 1:
806 ang_arc += 2.0 * pi
807 elif ang_arc > 0.0 and fs == 0:
808 ang_arc -= 2.0 * pi
810 n_segs = int(ceil(abs(ang_arc * 2.0 / (pi * 0.5 + 0.001))))
812 if self._spline is None:
813 self._appendPoint(cpx, cpy,
814 handle_left_type='FREE', handle_left=(cpx, cpy),
815 handle_right_type='FREE', handle_right=(cpx, cpy))
817 for i in range(n_segs):
818 ang0 = ang_0 + i * ang_arc / n_segs
819 ang1 = ang_0 + (i + 1) * ang_arc / n_segs
820 ang_demi = 0.25 * (ang1 - ang0)
821 t = 2.66666 * sin(ang_demi) * sin(ang_demi) / sin(ang_demi * 2.0)
822 x1 = xc + cos(ang0) - t * sin(ang0)
823 y1 = yc + sin(ang0) + t * cos(ang0)
824 x2 = xc + cos(ang1)
825 y2 = yc + sin(ang1)
826 x3 = x2 + t * sin(ang1)
827 y3 = y2 - t * cos(ang1)
829 coord1 = ((cos(ang) * rx) * x1 + (-sin(ang) * ry) * y1,
830 (sin(ang) * rx) * x1 + (cos(ang) * ry) * y1)
831 coord2 = ((cos(ang) * rx) * x3 + (-sin(ang) * ry) * y3,
832 (sin(ang) * rx) * x3 + (cos(ang) * ry) * y3)
833 coord3 = ((cos(ang) * rx) * x2 + (-sin(ang) * ry) * y2,
834 (sin(ang) * rx) * x2 + (cos(ang) * ry) * y2)
836 self._updateHandle(handle=coord1, handle_type='FREE')
838 self._appendPoint(coord3[0], coord3[1],
839 handle_left_type='FREE', handle_left=coord2,
840 handle_right_type='FREE', handle_right=coord3)
842 def _pathCurveToA(self, code):
844 Elliptical arc CurveTo path command
847 cur = self._data.cur()
849 while cur is not None and not cur.isalpha():
850 rx = float(self._data.next())
851 ry = float(self._data.next())
852 ang = float(self._data.next()) / 180 * pi
853 fa = float(self._data.next())
854 fs = float(self._data.next())
855 x, y = self._getCoordPair(code.islower(), self._point)
857 self._calcArc(rx, ry, ang, fa, fs, x, y)
859 self._point = (x, y)
860 self._handle = None
861 cur = self._data.cur()
863 def _pathClose(self, code):
865 Close path command
868 if self._spline:
869 self._spline['closed'] = True
871 cv = self._spline['points'][0]
872 self._point = (cv['x'], cv['y'])
874 def _pathCloseImplicitly(self):
876 Close path implicitly without changing current point coordinate
879 if self._spline:
880 self._spline['closed'] = True
882 def parse(self):
884 Execute parser
887 closed = False
889 while not self._data.eof():
890 code = self._data.next()
891 cmd = self._commands.get(code)
893 if cmd is None:
894 raise Exception('Unknown path command: {0}' . format(code))
896 if code in {'Z', 'z'}:
897 closed = True
898 else:
899 closed = False
901 if code in {'M', 'm'} and self._use_fill and not closed:
902 self._pathCloseImplicitly() # Ensure closed before MoveTo path command
904 cmd(code)
905 if self._use_fill and not closed:
906 self._pathCloseImplicitly() # Ensure closed at the end of parsing
908 def getSplines(self):
910 Get splines definitions
913 return self._splines
916 class SVGGeometry:
918 Abstract SVG geometry
921 __slots__ = ('_node', # XML node for geometry
922 '_context', # Global SVG context (holds matrices stack, i.e.)
923 '_creating') # Flag if geometry is already creating
924 # for this node
925 # need to detect cycles for USE node
927 def __init__(self, node, context):
929 Initialize SVG geometry
932 self._node = node
933 self._context = context
934 self._creating = False
936 if hasattr(node, 'getAttribute'):
937 defs = context['defines']
939 attr_id = node.getAttribute('id')
940 if attr_id and defs.get('#' + attr_id) is None:
941 defs['#' + attr_id] = self
943 className = node.getAttribute('class')
944 if className and defs.get(className) is None:
945 defs[className] = self
947 def _pushRect(self, rect):
949 Push display rectangle
952 self._context['rects'].append(rect)
953 self._context['rect'] = rect
955 def _popRect(self):
957 Pop display rectangle
960 self._context['rects'].pop()
961 self._context['rect'] = self._context['rects'][-1]
963 def _pushMatrix(self, matrix):
965 Push transformation matrix
968 current_matrix = self._context['matrix']
969 self._context['matrix_stack'].append(current_matrix)
970 self._context['matrix'] = current_matrix @ matrix
972 def _popMatrix(self):
974 Pop transformation matrix
977 old_matrix = self._context['matrix_stack'].pop()
978 self._context['matrix'] = old_matrix
980 def _pushStyle(self, style):
982 Push style
985 self._context['styles'].append(style)
986 self._context['style'] = style
988 def _popStyle(self):
990 Pop style
993 self._context['styles'].pop()
994 self._context['style'] = self._context['styles'][-1]
996 def _transformCoord(self, point):
998 Transform SVG-file coords
1001 v = Vector((point[0], point[1], 0.0))
1003 return self._context['matrix'] @ v
1005 def getNodeMatrix(self):
1007 Get transformation matrix of node
1010 return SVGMatrixFromNode(self._node, self._context)
1012 def parse(self):
1014 Parse XML node to memory
1017 pass
1019 def _doCreateGeom(self, instancing):
1021 Internal handler to create real geometries
1024 pass
1026 def getTransformMatrix(self):
1028 Get matrix created from "transform" attribute
1031 transform = self._node.getAttribute('transform')
1033 if transform:
1034 return SVGParseTransform(transform)
1036 return None
1038 def createGeom(self, instancing):
1040 Create real geometries
1043 if self._creating:
1044 return
1046 self._creating = True
1048 matrix = self.getTransformMatrix()
1049 if matrix is not None:
1050 self._pushMatrix(matrix)
1052 self._doCreateGeom(instancing)
1054 if matrix is not None:
1055 self._popMatrix()
1057 self._creating = False
1060 class SVGGeometryContainer(SVGGeometry):
1062 Container of SVG geometries
1065 __slots__ = ('_geometries', # List of chold geometries
1066 '_styles') # Styles, used for displaying
1068 def __init__(self, node, context):
1070 Initialize SVG geometry container
1073 super().__init__(node, context)
1075 self._geometries = []
1076 self._styles = SVGEmptyStyles
1078 def parse(self):
1080 Parse XML node to memory
1083 if type(self._node) is xml.dom.minidom.Element:
1084 self._styles = SVGParseStyles(self._node, self._context)
1086 self._pushStyle(self._styles)
1088 for node in self._node.childNodes:
1089 if type(node) is not xml.dom.minidom.Element:
1090 continue
1092 ob = parseAbstractNode(node, self._context)
1093 if ob is not None:
1094 self._geometries.append(ob)
1096 self._popStyle()
1098 def _doCreateGeom(self, instancing):
1100 Create real geometries
1103 for geom in self._geometries:
1104 geom.createGeom(instancing)
1106 def getGeometries(self):
1108 Get list of parsed geometries
1111 return self._geometries
1114 class SVGGeometryPATH(SVGGeometry):
1116 SVG path geometry
1119 __slots__ = ('_splines', # List of splines after parsing
1120 '_styles') # Styles, used for displaying
1122 def __init__(self, node, context):
1124 Initialize SVG path
1127 super().__init__(node, context)
1129 self._splines = []
1130 self._styles = SVGEmptyStyles
1132 def parse(self):
1134 Parse SVG path node
1137 d = self._node.getAttribute('d')
1139 self._styles = SVGParseStyles(self._node, self._context)
1141 pathParser = SVGPathParser(d, self._styles['useFill'])
1142 pathParser.parse()
1144 self._splines = pathParser.getSplines()
1146 def _doCreateGeom(self, instancing):
1148 Create real geometries
1151 ob = SVGCreateCurve(self._context)
1152 cu = ob.data
1154 id_names_from_node(self._node, ob)
1156 if self._styles['useFill']:
1157 cu.dimensions = '2D'
1158 cu.fill_mode = 'BOTH'
1159 cu.materials.append(self._styles['fill'])
1160 else:
1161 cu.dimensions = '3D'
1163 for spline in self._splines:
1164 act_spline = None
1166 if spline['closed'] and len(spline['points']) >= 2:
1167 first = spline['points'][0]
1168 last = spline['points'][-1]
1169 if ( first['handle_left_type'] == 'FREE' and
1170 last['handle_right_type'] == 'VECTOR'):
1171 last['handle_right_type'] = 'FREE'
1172 last['handle_right'] = (last['x'], last['y'])
1173 if ( last['handle_right_type'] == 'FREE' and
1174 first['handle_left_type'] == 'VECTOR'):
1175 first['handle_left_type'] = 'FREE'
1176 first['handle_left'] = (first['x'], first['y'])
1178 for point in spline['points']:
1179 co = self._transformCoord((point['x'], point['y']))
1181 if act_spline is None:
1182 cu.splines.new('BEZIER')
1184 act_spline = cu.splines[-1]
1185 act_spline.use_cyclic_u = spline['closed']
1186 else:
1187 act_spline.bezier_points.add(1)
1189 bezt = act_spline.bezier_points[-1]
1190 bezt.co = co
1192 bezt.handle_left_type = point['handle_left_type']
1193 if point['handle_left'] is not None:
1194 handle = point['handle_left']
1195 bezt.handle_left = self._transformCoord(handle)
1197 bezt.handle_right_type = point['handle_right_type']
1198 if point['handle_right'] is not None:
1199 handle = point['handle_right']
1200 bezt.handle_right = self._transformCoord(handle)
1202 SVGFinishCurve()
1205 class SVGGeometryDEFS(SVGGeometryContainer):
1207 Container for referenced elements
1210 def createGeom(self, instancing):
1212 Create real geometries
1215 pass
1218 class SVGGeometrySYMBOL(SVGGeometryContainer):
1220 Referenced element
1223 def _doCreateGeom(self, instancing):
1225 Create real geometries
1228 self._pushMatrix(self.getNodeMatrix())
1230 super()._doCreateGeom(False)
1232 self._popMatrix()
1234 def createGeom(self, instancing):
1236 Create real geometries
1239 if not instancing:
1240 return
1242 super().createGeom(instancing)
1245 class SVGGeometryG(SVGGeometryContainer):
1247 Geometry group
1250 pass
1253 class SVGGeometryUSE(SVGGeometry):
1255 User of referenced elements
1258 def _doCreateGeom(self, instancing):
1260 Create real geometries
1263 ref = self._node.getAttribute('xlink:href')
1264 geom = self._context['defines'].get(ref)
1266 if geom is not None:
1267 rect = SVGRectFromNode(self._node, self._context)
1268 self._pushRect(rect)
1270 self._pushMatrix(self.getNodeMatrix())
1272 geom.createGeom(True)
1274 self._popMatrix()
1276 self._popRect()
1279 class SVGGeometryRECT(SVGGeometry):
1281 SVG rectangle
1284 __slots__ = ('_rect', # coordinate and dimensions of rectangle
1285 '_radius', # Rounded corner radiuses
1286 '_styles') # Styles, used for displaying
1288 def __init__(self, node, context):
1290 Initialize new rectangle
1293 super().__init__(node, context)
1295 self._rect = ('0', '0', '0', '0')
1296 self._radius = ('0', '0')
1297 self._styles = SVGEmptyStyles
1299 def parse(self):
1301 Parse SVG rectangle node
1304 self._styles = SVGParseStyles(self._node, self._context)
1306 rect = []
1307 for attr in ['x', 'y', 'width', 'height']:
1308 val = self._node.getAttribute(attr)
1309 rect.append(val or '0')
1311 self._rect = (rect)
1313 rx = self._node.getAttribute('rx')
1314 ry = self._node.getAttribute('ry')
1316 self._radius = (rx, ry)
1318 def _appendCorner(self, spline, coord, firstTime, rounded):
1320 Append new corner to rectangle
1323 handle = None
1324 if len(coord) == 3:
1325 handle = self._transformCoord(coord[2])
1326 coord = (coord[0], coord[1])
1328 co = self._transformCoord(coord)
1330 if not firstTime:
1331 spline.bezier_points.add(1)
1333 bezt = spline.bezier_points[-1]
1334 bezt.co = co
1336 if rounded:
1337 if handle:
1338 bezt.handle_left_type = 'VECTOR'
1339 bezt.handle_right_type = 'FREE'
1341 bezt.handle_right = handle
1342 else:
1343 bezt.handle_left_type = 'FREE'
1344 bezt.handle_right_type = 'VECTOR'
1345 bezt.handle_left = co
1347 else:
1348 bezt.handle_left_type = 'VECTOR'
1349 bezt.handle_right_type = 'VECTOR'
1351 def _doCreateGeom(self, instancing):
1353 Create real geometries
1356 # Run-time parsing -- percents would be correct only if
1357 # parsing them now
1358 crect = self._context['rect']
1359 rect = []
1361 for i in range(4):
1362 rect.append(SVGParseCoord(self._rect[i], crect[i % 2]))
1364 r = self._radius
1365 rx = ry = 0.0
1367 if r[0] and r[1]:
1368 rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
1369 ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
1370 elif r[0]:
1371 rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
1372 ry = min(rx, rect[3] / 2)
1373 rx = ry = min(rx, ry)
1374 elif r[1]:
1375 ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
1376 rx = min(ry, rect[2] / 2)
1377 rx = ry = min(rx, ry)
1379 radius = (rx, ry)
1381 # Geometry creation
1382 ob = SVGCreateCurve(self._context)
1383 cu = ob.data
1385 id_names_from_node(self._node, ob)
1387 if self._styles['useFill']:
1388 cu.dimensions = '2D'
1389 cu.fill_mode = 'BOTH'
1390 cu.materials.append(self._styles['fill'])
1391 else:
1392 cu.dimensions = '3D'
1394 cu.splines.new('BEZIER')
1396 spline = cu.splines[-1]
1397 spline.use_cyclic_u = True
1399 x, y = rect[0], rect[1]
1400 w, h = rect[2], rect[3]
1401 rx, ry = radius[0], radius[1]
1402 rounded = False
1404 if rx or ry:
1406 # 0 _______ 1
1407 # / \
1408 # / \
1409 # 7 2
1410 # | |
1411 # | |
1412 # 6 3
1413 # \ /
1414 # \ /
1415 # 5 _______ 4
1418 # Optional third component -- right handle coord
1419 coords = [(x + rx, y),
1420 (x + w - rx, y, (x + w, y)),
1421 (x + w, y + ry),
1422 (x + w, y + h - ry, (x + w, y + h)),
1423 (x + w - rx, y + h),
1424 (x + rx, y + h, (x, y + h)),
1425 (x, y + h - ry),
1426 (x, y + ry, (x, y))]
1428 rounded = True
1429 else:
1430 coords = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
1432 firstTime = True
1433 for coord in coords:
1434 self._appendCorner(spline, coord, firstTime, rounded)
1435 firstTime = False
1437 SVGFinishCurve()
1440 class SVGGeometryELLIPSE(SVGGeometry):
1442 SVG ellipse
1445 __slots__ = ('_cx', # X-coordinate of center
1446 '_cy', # Y-coordinate of center
1447 '_rx', # X-axis radius of circle
1448 '_ry', # Y-axis radius of circle
1449 '_styles') # Styles, used for displaying
1451 def __init__(self, node, context):
1453 Initialize new ellipse
1456 super().__init__(node, context)
1458 self._cx = '0.0'
1459 self._cy = '0.0'
1460 self._rx = '0.0'
1461 self._ry = '0.0'
1462 self._styles = SVGEmptyStyles
1464 def parse(self):
1466 Parse SVG ellipse node
1469 self._styles = SVGParseStyles(self._node, self._context)
1471 self._cx = self._node.getAttribute('cx') or '0'
1472 self._cy = self._node.getAttribute('cy') or '0'
1473 self._rx = self._node.getAttribute('rx') or '0'
1474 self._ry = self._node.getAttribute('ry') or '0'
1476 def _doCreateGeom(self, instancing):
1478 Create real geometries
1481 # Run-time parsing -- percents would be correct only if
1482 # parsing them now
1483 crect = self._context['rect']
1485 cx = SVGParseCoord(self._cx, crect[0])
1486 cy = SVGParseCoord(self._cy, crect[1])
1487 rx = SVGParseCoord(self._rx, crect[0])
1488 ry = SVGParseCoord(self._ry, crect[1])
1490 if not rx or not ry:
1491 # Automaic handles will work incorrect in this case
1492 return
1494 # Create circle
1495 ob = SVGCreateCurve(self._context)
1496 cu = ob.data
1498 id_names_from_node(self._node, ob)
1500 if self._styles['useFill']:
1501 cu.dimensions = '2D'
1502 cu.fill_mode = 'BOTH'
1503 cu.materials.append(self._styles['fill'])
1504 else:
1505 cu.dimensions = '3D'
1507 coords = [((cx - rx, cy),
1508 (cx - rx, cy + ry * 0.552),
1509 (cx - rx, cy - ry * 0.552)),
1511 ((cx, cy - ry),
1512 (cx - rx * 0.552, cy - ry),
1513 (cx + rx * 0.552, cy - ry)),
1515 ((cx + rx, cy),
1516 (cx + rx, cy - ry * 0.552),
1517 (cx + rx, cy + ry * 0.552)),
1519 ((cx, cy + ry),
1520 (cx + rx * 0.552, cy + ry),
1521 (cx - rx * 0.552, cy + ry))]
1523 spline = None
1524 for coord in coords:
1525 co = self._transformCoord(coord[0])
1526 handle_left = self._transformCoord(coord[1])
1527 handle_right = self._transformCoord(coord[2])
1529 if spline is None:
1530 cu.splines.new('BEZIER')
1531 spline = cu.splines[-1]
1532 spline.use_cyclic_u = True
1533 else:
1534 spline.bezier_points.add(1)
1536 bezt = spline.bezier_points[-1]
1537 bezt.co = co
1538 bezt.handle_left_type = 'FREE'
1539 bezt.handle_right_type = 'FREE'
1540 bezt.handle_left = handle_left
1541 bezt.handle_right = handle_right
1543 SVGFinishCurve()
1546 class SVGGeometryCIRCLE(SVGGeometryELLIPSE):
1548 SVG circle
1551 def parse(self):
1553 Parse SVG circle node
1556 self._styles = SVGParseStyles(self._node, self._context)
1558 self._cx = self._node.getAttribute('cx') or '0'
1559 self._cy = self._node.getAttribute('cy') or '0'
1561 r = self._node.getAttribute('r') or '0'
1562 self._rx = self._ry = r
1565 class SVGGeometryLINE(SVGGeometry):
1567 SVG line
1570 __slots__ = ('_x1', # X-coordinate of beginning
1571 '_y1', # Y-coordinate of beginning
1572 '_x2', # X-coordinate of ending
1573 '_y2') # Y-coordinate of ending
1575 def __init__(self, node, context):
1577 Initialize new line
1580 super().__init__(node, context)
1582 self._x1 = '0.0'
1583 self._y1 = '0.0'
1584 self._x2 = '0.0'
1585 self._y2 = '0.0'
1587 def parse(self):
1589 Parse SVG line node
1592 self._x1 = self._node.getAttribute('x1') or '0'
1593 self._y1 = self._node.getAttribute('y1') or '0'
1594 self._x2 = self._node.getAttribute('x2') or '0'
1595 self._y2 = self._node.getAttribute('y2') or '0'
1597 def _doCreateGeom(self, instancing):
1599 Create real geometries
1602 # Run-time parsing -- percents would be correct only if
1603 # parsing them now
1604 crect = self._context['rect']
1606 x1 = SVGParseCoord(self._x1, crect[0])
1607 y1 = SVGParseCoord(self._y1, crect[1])
1608 x2 = SVGParseCoord(self._x2, crect[0])
1609 y2 = SVGParseCoord(self._y2, crect[1])
1611 # Create cline
1612 ob = SVGCreateCurve(self._context)
1613 cu = ob.data
1615 id_names_from_node(self._node, ob)
1617 coords = [(x1, y1), (x2, y2)]
1618 spline = None
1620 for coord in coords:
1621 co = self._transformCoord(coord)
1623 if spline is None:
1624 cu.splines.new('BEZIER')
1625 spline = cu.splines[-1]
1626 spline.use_cyclic_u = True
1627 else:
1628 spline.bezier_points.add(1)
1630 bezt = spline.bezier_points[-1]
1631 bezt.co = co
1632 bezt.handle_left_type = 'VECTOR'
1633 bezt.handle_right_type = 'VECTOR'
1635 SVGFinishCurve()
1638 class SVGGeometryPOLY(SVGGeometry):
1640 Abstract class for handling poly-geometries
1641 (polylines and polygons)
1644 __slots__ = ('_points', # Array of points for poly geometry
1645 '_styles', # Styles, used for displaying
1646 '_closed') # Should generated curve be closed?
1648 def __init__(self, node, context):
1650 Initialize new poly geometry
1653 super().__init__(node, context)
1655 self._points = []
1656 self._styles = SVGEmptyStyles
1657 self._closed = False
1659 def parse(self):
1661 Parse poly node
1664 self._styles = SVGParseStyles(self._node, self._context)
1666 points = parse_array_of_floats(self._node.getAttribute('points'))
1668 prev = None
1669 self._points = []
1671 for p in points:
1672 if prev is None:
1673 prev = p
1674 else:
1675 self._points.append((prev, p))
1676 prev = None
1678 def _doCreateGeom(self, instancing):
1680 Create real geometries
1683 ob = SVGCreateCurve(self._context)
1684 cu = ob.data
1686 id_names_from_node(self._node, ob)
1688 if self._closed and self._styles['useFill']:
1689 cu.dimensions = '2D'
1690 cu.fill_mode = 'BOTH'
1691 cu.materials.append(self._styles['fill'])
1692 else:
1693 cu.dimensions = '3D'
1695 spline = None
1697 for point in self._points:
1698 co = self._transformCoord(point)
1700 if spline is None:
1701 cu.splines.new('BEZIER')
1702 spline = cu.splines[-1]
1703 spline.use_cyclic_u = self._closed
1704 else:
1705 spline.bezier_points.add(1)
1707 bezt = spline.bezier_points[-1]
1708 bezt.co = co
1709 bezt.handle_left_type = 'VECTOR'
1710 bezt.handle_right_type = 'VECTOR'
1712 SVGFinishCurve()
1715 class SVGGeometryPOLYLINE(SVGGeometryPOLY):
1717 SVG polyline geometry
1720 pass
1723 class SVGGeometryPOLYGON(SVGGeometryPOLY):
1725 SVG polygon geometry
1728 def __init__(self, node, context):
1730 Initialize new polygon geometry
1733 super().__init__(node, context)
1735 self._closed = True
1738 class SVGGeometrySVG(SVGGeometryContainer):
1740 Main geometry holder
1743 def _doCreateGeom(self, instancing):
1745 Create real geometries
1748 rect = SVGRectFromNode(self._node, self._context)
1750 matrix = self.getNodeMatrix()
1752 # Better SVG compatibility: match svg-document units
1753 # with blender units
1755 viewbox = []
1756 unit = ''
1758 if self._node.getAttribute('height'):
1759 raw_height = self._node.getAttribute('height')
1760 token, last_char = read_float(raw_height)
1761 document_height = float(token)
1762 unit = raw_height[last_char:].strip()
1764 if self._node.getAttribute('viewBox'):
1765 viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
1767 if len(viewbox) == 4 and unit in ('cm', 'mm', 'in', 'pt', 'pc'):
1769 #convert units to BU:
1770 unitscale = units[unit] / 90 * 1000 / 39.3701
1772 #apply blender unit scale:
1773 unitscale = unitscale / bpy.context.scene.unit_settings.scale_length
1775 matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((1.0, 0.0, 0.0)))
1776 matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((0.0, 1.0, 0.0)))
1778 # match document origin with 3D space origin.
1779 if self._node.getAttribute('viewBox'):
1780 viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
1781 matrix = matrix @ matrix.Translation([0.0, - viewbox[1] - viewbox[3], 0.0])
1783 self._pushMatrix(matrix)
1784 self._pushRect(rect)
1786 super()._doCreateGeom(False)
1788 self._popRect()
1789 self._popMatrix()
1792 class SVGLoader(SVGGeometryContainer):
1794 SVG file loader
1797 def getTransformMatrix(self):
1799 Get matrix created from "transform" attribute
1802 # SVG document doesn't support transform specification
1803 # it can't even hold attributes
1805 return None
1807 def __init__(self, context, filepath, do_colormanage):
1809 Initialize SVG loader
1811 import os
1813 svg_name = os.path.basename(filepath)
1814 scene = context.scene
1815 collection = bpy.data.collections.new(name=svg_name)
1816 scene.collection.children.link(collection)
1818 node = xml.dom.minidom.parse(filepath)
1820 m = Matrix()
1821 m = m @ Matrix.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
1822 m = m @ Matrix.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
1824 rect = (0, 0)
1826 self._context = {'defines': {},
1827 'rects': [rect],
1828 'rect': rect,
1829 'matrix_stack': [],
1830 'matrix': m,
1831 'materials': {},
1832 'styles': [None],
1833 'style': None,
1834 'do_colormanage': do_colormanage,
1835 'collection': collection}
1837 super().__init__(node, self._context)
1840 svgGeometryClasses = {
1841 'svg': SVGGeometrySVG,
1842 'path': SVGGeometryPATH,
1843 'defs': SVGGeometryDEFS,
1844 'symbol': SVGGeometrySYMBOL,
1845 'use': SVGGeometryUSE,
1846 'rect': SVGGeometryRECT,
1847 'ellipse': SVGGeometryELLIPSE,
1848 'circle': SVGGeometryCIRCLE,
1849 'line': SVGGeometryLINE,
1850 'polyline': SVGGeometryPOLYLINE,
1851 'polygon': SVGGeometryPOLYGON,
1852 'g': SVGGeometryG}
1855 def parseAbstractNode(node, context):
1856 name = node.tagName.lower()
1858 if name.startswith('svg:'):
1859 name = name[4:]
1861 geomClass = svgGeometryClasses.get(name)
1863 if geomClass is not None:
1864 ob = geomClass(node, context)
1865 ob.parse()
1867 return ob
1869 return None
1872 def load_svg(context, filepath, do_colormanage):
1874 Load specified SVG file
1877 if bpy.ops.object.mode_set.poll():
1878 bpy.ops.object.mode_set(mode='OBJECT')
1880 loader = SVGLoader(context, filepath, do_colormanage)
1881 loader.parse()
1882 loader.createGeom(False)
1885 def load(operator, context, filepath=""):
1887 # error in code should raise exceptions but loading
1888 # non SVG files can give useful messages.
1889 do_colormanage = context.scene.display_settings.display_device != 'NONE'
1890 try:
1891 load_svg(context, filepath, do_colormanage)
1892 except (xml.parsers.expat.ExpatError, UnicodeEncodeError) as e:
1893 import traceback
1894 traceback.print_exc()
1896 operator.report({'WARNING'}, tip_("Unable to parse XML, %s:%s for file %r") % (type(e).__name__, e, filepath))
1897 return {'CANCELLED'}
1899 return {'FINISHED'}