Import_3ds: Fixed keyframe transformation
[blender-addons.git] / io_curve_svg / import_svg.py
blobd2dca04942d26b8188eeb19a768094d09135147a
1 # SPDX-FileCopyrightText: 2004-2009 JM Soler
2 # SPDX-FileCopyrightText: 2011-2022 Blender Foundation
4 # SPDX-License-Identifier: GPL-2.0-or-later
6 import re
7 import xml.dom.minidom
8 from math import cos, sin, tan, atan2, pi, ceil
10 import bpy
11 from mathutils import Vector, Matrix
12 from bpy.app.translations import pgettext_tip as tip_
14 from . import svg_colors
15 from .svg_util import (units,
16 srgb_to_linearrgb,
17 check_points_equal,
18 parse_array_of_floats,
19 read_float)
21 #### Common utilities ####
23 SVGEmptyStyles = {'useFill': None,
24 'fill': None}
27 def SVGCreateCurve(context):
28 """
29 Create new curve object to hold splines in
30 """
32 cu = bpy.data.curves.new("Curve", 'CURVE')
33 obj = bpy.data.objects.new("Curve", cu)
35 context['collection'].objects.link(obj)
37 return obj
40 def SVGFinishCurve():
41 """
42 Finish curve creation
43 """
45 pass
48 def SVGFlipHandle(x, y, x1, y1):
49 """
50 Flip handle around base point
51 """
53 x = x + (x - x1)
54 y = y + (y - y1)
56 return x, y
59 def SVGParseCoord(coord, size):
60 """
61 Parse coordinate component to common basis
63 Needed to handle coordinates set in cm, mm, inches.
64 """
66 token, last_char = read_float(coord)
67 val = float(token)
68 unit = coord[last_char:].strip() # strip() in case there is a space
70 if unit == '%':
71 return float(size) / 100.0 * val
72 return val * units[unit]
75 def SVGRectFromNode(node, context):
76 """
77 Get display rectangle from node
78 """
80 w = context['rect'][0]
81 h = context['rect'][1]
83 if node.getAttribute('viewBox'):
84 viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
85 w = SVGParseCoord(viewBox[2], w)
86 h = SVGParseCoord(viewBox[3], h)
87 else:
88 if node.getAttribute('width'):
89 w = SVGParseCoord(node.getAttribute('width'), w)
91 if node.getAttribute('height'):
92 h = SVGParseCoord(node.getAttribute('height'), h)
94 return (w, h)
97 def SVGMatrixFromNode(node, context):
98 """
99 Get transformation matrix from given node
102 tagName = node.tagName.lower()
103 tags = ['svg:svg', 'svg:use', 'svg:symbol']
105 if tagName not in tags and 'svg:' + tagName not in tags:
106 return Matrix()
108 rect = context['rect']
109 has_user_coordinate = (len(context['rects']) > 1)
111 m = Matrix()
112 x = SVGParseCoord(node.getAttribute('x') or '0', rect[0])
113 y = SVGParseCoord(node.getAttribute('y') or '0', rect[1])
114 w = SVGParseCoord(node.getAttribute('width') or str(rect[0]), rect[0])
115 h = SVGParseCoord(node.getAttribute('height') or str(rect[1]), rect[1])
117 m = Matrix.Translation(Vector((x, y, 0.0)))
118 if has_user_coordinate:
119 if rect[0] != 0 and rect[1] != 0:
120 m = m @ Matrix.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0)))
121 m = m @ Matrix.Scale(h / rect[1], 4, Vector((0.0, 1.0, 0.0)))
123 if node.getAttribute('viewBox'):
124 viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
125 vx = SVGParseCoord(viewBox[0], w)
126 vy = SVGParseCoord(viewBox[1], h)
127 vw = SVGParseCoord(viewBox[2], w)
128 vh = SVGParseCoord(viewBox[3], h)
130 if vw == 0 or vh == 0:
131 return m
133 if has_user_coordinate or (w != 0 and h != 0):
134 sx = w / vw
135 sy = h / vh
136 scale = min(sx, sy)
137 else:
138 scale = 1.0
139 w = vw
140 h = vh
142 tx = (w - vw * scale) / 2
143 ty = (h - vh * scale) / 2
144 m = m @ Matrix.Translation(Vector((tx, ty, 0.0)))
146 m = m @ Matrix.Translation(Vector((-vx, -vy, 0.0)))
147 m = m @ Matrix.Scale(scale, 4, Vector((1.0, 0.0, 0.0)))
148 m = m @ Matrix.Scale(scale, 4, Vector((0.0, 1.0, 0.0)))
150 return m
153 def SVGParseTransform(transform):
155 Parse transform string and return transformation matrix
158 m = Matrix()
159 r = re.compile(r'\s*([A-z]+)\s*\((.*?)\)')
161 for match in r.finditer(transform):
162 func = match.group(1)
163 params = match.group(2)
164 params = params.replace(',', ' ').split()
166 proc = SVGTransforms.get(func)
167 if proc is None:
168 raise Exception('Unknown transform function: ' + func)
170 m = m @ proc(params)
172 return m
175 def SVGGetMaterial(color, context):
177 Get material for specified color
180 materials = context['materials']
181 rgb_re = re.compile(r'^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
183 if color in materials:
184 return materials[color]
186 diff = None
187 if color.startswith('#'):
188 color = color[1:]
190 if len(color) == 3:
191 color = color[0] * 2 + color[1] * 2 + color[2] * 2
193 diff = (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
194 elif color in svg_colors.SVGColors:
195 diff = svg_colors.SVGColors[color]
196 elif rgb_re.match(color):
197 c = rgb_re.findall(color)[0]
198 diff = (float(c[0]), float(c[1]), float(c[2]))
199 else:
200 return None
202 diffuse_color = ([x / 255.0 for x in diff])
204 if context['do_colormanage']:
205 diffuse_color[0] = srgb_to_linearrgb(diffuse_color[0])
206 diffuse_color[1] = srgb_to_linearrgb(diffuse_color[1])
207 diffuse_color[2] = srgb_to_linearrgb(diffuse_color[2])
209 mat = bpy.data.materials.new(name='SVGMat')
210 mat.diffuse_color = (*diffuse_color, 1.0)
212 materials[color] = mat
214 return mat
217 def SVGTransformTranslate(params):
219 translate SVG transform command
222 tx = float(params[0])
223 ty = float(params[1]) if len(params) > 1 else 0.0
225 return Matrix.Translation(Vector((tx, ty, 0.0)))
228 def SVGTransformMatrix(params):
230 matrix SVG transform command
233 a = float(params[0])
234 b = float(params[1])
235 c = float(params[2])
236 d = float(params[3])
237 e = float(params[4])
238 f = float(params[5])
240 return Matrix(((a, c, 0.0, e),
241 (b, d, 0.0, f),
242 (0, 0, 1.0, 0),
243 (0, 0, 0.0, 1)))
246 def SVGTransformScale(params):
248 scale SVG transform command
251 sx = float(params[0])
252 sy = float(params[1]) if len(params) > 1 else sx
254 m = Matrix()
256 m = m @ Matrix.Scale(sx, 4, Vector((1.0, 0.0, 0.0)))
257 m = m @ Matrix.Scale(sy, 4, Vector((0.0, 1.0, 0.0)))
259 return m
262 def SVGTransformSkewY(params):
264 skewY SVG transform command
267 ang = float(params[0]) * pi / 180.0
269 return Matrix(((1.0, 0.0, 0.0),
270 (tan(ang), 1.0, 0.0),
271 (0.0, 0.0, 1.0))).to_4x4()
274 def SVGTransformSkewX(params):
276 skewX SVG transform command
279 ang = float(params[0]) * pi / 180.0
281 return Matrix(((1.0, tan(ang), 0.0),
282 (0.0, 1.0, 0.0),
283 (0.0, 0.0, 1.0))).to_4x4()
286 def SVGTransformRotate(params):
288 skewX SVG transform command
291 ang = float(params[0]) * pi / 180.0
292 cx = cy = 0.0
294 if len(params) >= 3:
295 cx = float(params[1])
296 cy = float(params[2])
298 tm = Matrix.Translation(Vector((cx, cy, 0.0)))
299 rm = Matrix.Rotation(ang, 4, Vector((0.0, 0.0, 1.0)))
301 return tm @ rm @ tm.inverted()
303 SVGTransforms = {'translate': SVGTransformTranslate,
304 'scale': SVGTransformScale,
305 'skewX': SVGTransformSkewX,
306 'skewY': SVGTransformSkewY,
307 'matrix': SVGTransformMatrix,
308 'rotate': SVGTransformRotate}
311 def SVGParseStyles(node, context):
313 Parse node to get different styles for displaying geometries
314 (materials, filling flags, etc..)
317 styles = SVGEmptyStyles.copy()
319 style = node.getAttribute('style')
320 if style:
321 elems = style.split(';')
322 for elem in elems:
323 s = elem.split(':')
325 if len(s) != 2:
326 continue
328 name = s[0].strip().lower()
329 val = s[1].strip()
331 if name == 'fill':
332 val = val.lower()
333 if val == 'none':
334 styles['useFill'] = False
335 else:
336 styles['useFill'] = True
337 styles['fill'] = SVGGetMaterial(val, context)
339 if styles['useFill'] is None:
340 styles['useFill'] = True
341 styles['fill'] = SVGGetMaterial('#000', context)
343 return styles
345 if styles['useFill'] is None:
346 fill = node.getAttribute('fill')
347 if fill:
348 fill = fill.lower()
349 if fill == 'none':
350 styles['useFill'] = False
351 else:
352 styles['useFill'] = True
353 styles['fill'] = SVGGetMaterial(fill, context)
355 if styles['useFill'] is None and context['style']:
356 styles = context['style'].copy()
358 if styles['useFill'] is None:
359 styles['useFill'] = True
360 styles['fill'] = SVGGetMaterial('#000', context)
362 return styles
364 def id_names_from_node(node, ob):
365 if node.getAttribute('id'):
366 name = node.getAttribute('id')
367 ob.name = name
368 ob.data.name = name
370 #### SVG path helpers ####
373 class SVGPathData:
375 SVG Path data token supplier
378 __slots__ = ('_data', # List of tokens
379 '_index', # Index of current token in tokens list
380 '_len') # Length of tokens list
382 def __init__(self, d):
384 Initialize new path data supplier
386 d - the definition of the outline of a shape
389 spaces = ' ,\t'
390 commands = {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
391 current_command = ''
392 tokens = []
394 i = 0
395 n = len(d)
396 while i < n:
397 c = d[i]
399 if c in spaces:
400 pass
401 elif c.lower() in commands:
402 tokens.append(c)
403 current_command = c
404 arg_index = 1
405 elif c in ['-', '.'] or c.isdigit():
406 # Special case for 'a/A' commands.
407 # Arguments 4 and 5 are either 0 or 1 and might not
408 # be separated from the next argument with space or comma.
409 if current_command.lower() == 'a':
410 if arg_index % 7 in [4,5]:
411 token = d[i]
412 last_char = i + 1
413 else:
414 token, last_char = read_float(d, i)
415 else:
416 token, last_char = read_float(d, i)
418 arg_index += 1
419 tokens.append(token)
421 # in most cases len(token) and (last_char - i) are the same
422 # but with whitespace or ',' prefix they are not.
424 i += (last_char - i) - 1
426 i += 1
428 self._data = tokens
429 self._index = 0
430 self._len = len(tokens)
432 def eof(self):
434 Check if end of data reached
437 return self._index >= self._len
439 def cur(self):
441 Return current token
444 if self.eof():
445 return None
447 return self._data[self._index]
449 def lookupNext(self):
451 get next token without moving pointer
454 if self.eof():
455 return None
457 return self._data[self._index]
459 def next(self):
461 Return current token and go to next one
464 if self.eof():
465 return None
467 token = self._data[self._index]
468 self._index += 1
470 return token
472 def nextCoord(self):
474 Return coordinate created from current token and move to next token
477 token = self.next()
479 if token is None:
480 return None
482 return float(token)
485 class SVGPathParser:
487 Parser of SVG path data
490 __slots__ = ('_data', # Path data supplird
491 '_point', # Current point coordinate
492 '_handle', # Last handle coordinate
493 '_splines', # List of all splies created during parsing
494 '_spline', # Currently handling spline
495 '_commands', # Hash of all supported path commands
496 '_use_fill', # Splines would be filled, so expected to be closed
499 def __init__(self, d, use_fill):
501 Initialize path parser
503 d - the definition of the outline of a shape
506 self._data = SVGPathData(d)
507 self._point = None # Current point
508 self._handle = None # Last handle
509 self._splines = [] # List of splines in path
510 self._spline = None # Current spline
511 self._use_fill = use_fill
513 self._commands = {'M': self._pathMoveTo,
514 'L': self._pathLineTo,
515 'H': self._pathLineTo,
516 'V': self._pathLineTo,
517 'C': self._pathCurveToCS,
518 'S': self._pathCurveToCS,
519 'Q': self._pathCurveToQT,
520 'T': self._pathCurveToQT,
521 'A': self._pathCurveToA,
522 'Z': self._pathClose,
524 'm': self._pathMoveTo,
525 'l': self._pathLineTo,
526 'h': self._pathLineTo,
527 'v': self._pathLineTo,
528 'c': self._pathCurveToCS,
529 's': self._pathCurveToCS,
530 'q': self._pathCurveToQT,
531 't': self._pathCurveToQT,
532 'a': self._pathCurveToA,
533 'z': self._pathClose}
535 def _getCoordPair(self, relative, point):
537 Get next coordinate pair
540 x = self._data.nextCoord()
541 y = self._data.nextCoord()
543 if relative and point is not None:
544 x += point[0]
545 y += point[1]
547 return x, y
549 def _appendPoint(self, x, y, handle_left=None, handle_left_type='VECTOR',
550 handle_right=None, handle_right_type='VECTOR'):
552 Append point to spline
554 If there's no active spline, create one and set it's first point
555 to current point coordinate
558 if self._spline is None:
559 self._spline = {'points': [],
560 'closed': False}
562 self._splines.append(self._spline)
564 if len(self._spline['points']) > 0:
565 # Not sure about specifications, but Illustrator could create
566 # last point at the same position, as start point (which was
567 # reached by MoveTo command) to set needed handle coords.
568 # It's also could use last point at last position to make path
569 # filled.
571 first = self._spline['points'][0]
572 if check_points_equal((first['x'], first['y']), (x, y)):
573 if handle_left is not None:
574 first['handle_left'] = handle_left
575 first['handle_left_type'] = 'FREE'
577 if handle_left_type != 'VECTOR':
578 first['handle_left_type'] = handle_left_type
580 if self._data.eof() or self._data.lookupNext().lower() == 'm':
581 self._spline['closed'] = True
583 return
585 last = self._spline['points'][-1]
586 if last['handle_right_type'] == 'VECTOR' and handle_left_type == 'FREE':
587 last['handle_right'] = (last['x'], last['y'])
588 last['handle_right_type'] = 'FREE'
589 if last['handle_right_type'] == 'FREE' and handle_left_type == 'VECTOR':
590 handle_left = (x, y)
591 handle_left_type = 'FREE'
593 point = {'x': x,
594 'y': y,
596 'handle_left': handle_left,
597 'handle_left_type': handle_left_type,
599 'handle_right': handle_right,
600 'handle_right_type': handle_right_type}
602 self._spline['points'].append(point)
604 def _updateHandle(self, handle=None, handle_type=None):
606 Update right handle of previous point when adding new point to spline
609 point = self._spline['points'][-1]
611 if handle_type is not None:
612 point['handle_right_type'] = handle_type
614 if handle is not None:
615 point['handle_right'] = handle
617 def _pathMoveTo(self, code):
619 MoveTo path command
622 relative = code.islower()
623 x, y = self._getCoordPair(relative, self._point)
625 self._spline = None # Flag to start new spline
626 self._point = (x, y)
628 cur = self._data.cur()
629 while cur is not None and not cur.isalpha():
630 x, y = self._getCoordPair(relative, self._point)
632 if self._spline is None:
633 self._appendPoint(self._point[0], self._point[1])
635 self._appendPoint(x, y)
637 self._point = (x, y)
638 cur = self._data.cur()
640 self._handle = None
642 def _pathLineTo(self, code):
644 LineTo path command
647 c = code.lower()
649 cur = self._data.cur()
650 while cur is not None and not cur.isalpha():
651 if c == 'l':
652 x, y = self._getCoordPair(code == 'l', self._point)
653 elif c == 'h':
654 x = self._data.nextCoord()
655 y = self._point[1]
656 else:
657 x = self._point[0]
658 y = self._data.nextCoord()
660 if code == 'h':
661 x += self._point[0]
662 elif code == 'v':
663 y += self._point[1]
665 if self._spline is None:
666 self._appendPoint(self._point[0], self._point[1])
668 self._appendPoint(x, y)
670 self._point = (x, y)
671 cur = self._data.cur()
673 self._handle = None
675 def _pathCurveToCS(self, code):
677 Cubic BEZIER CurveTo path command
680 c = code.lower()
681 cur = self._data.cur()
682 while cur is not None and not cur.isalpha():
683 if c == 'c':
684 x1, y1 = self._getCoordPair(code.islower(), self._point)
685 x2, y2 = self._getCoordPair(code.islower(), self._point)
686 else:
687 if self._handle is not None:
688 x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
689 self._handle[0], self._handle[1])
690 else:
691 x1, y1 = self._point
693 x2, y2 = self._getCoordPair(code.islower(), self._point)
695 x, y = self._getCoordPair(code.islower(), self._point)
697 if self._spline is None:
698 self._appendPoint(self._point[0], self._point[1],
699 handle_left_type='FREE', handle_left=self._point,
700 handle_right_type='FREE', handle_right=(x1, y1))
701 else:
702 self._updateHandle(handle=(x1, y1), handle_type='FREE')
704 self._appendPoint(x, y,
705 handle_left_type='FREE', handle_left=(x2, y2),
706 handle_right_type='FREE', handle_right=(x, y))
708 self._point = (x, y)
709 self._handle = (x2, y2)
710 cur = self._data.cur()
712 def _pathCurveToQT(self, code):
714 Quadratic BEZIER CurveTo path command
717 c = code.lower()
718 cur = self._data.cur()
720 while cur is not None and not cur.isalpha():
721 if c == 'q':
722 x1, y1 = self._getCoordPair(code.islower(), self._point)
723 else:
724 if self._handle is not None:
725 x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
726 self._handle[0], self._handle[1])
727 else:
728 x1, y1 = self._point
730 x, y = self._getCoordPair(code.islower(), self._point)
732 if not check_points_equal((x, y), self._point):
733 if self._spline is None:
734 self._appendPoint(self._point[0], self._point[1],
735 handle_left_type='FREE', handle_left=self._point,
736 handle_right_type='FREE', handle_right=self._point)
738 self._appendPoint(x, y,
739 handle_left_type='FREE', handle_left=(x1, y1),
740 handle_right_type='FREE', handle_right=(x, y))
742 self._point = (x, y)
743 self._handle = (x1, y1)
744 cur = self._data.cur()
746 def _calcArc(self, rx, ry, ang, fa, fs, x, y):
748 Calc arc paths
750 Copied and adopted from `paths_svg2obj.py` script for Blender 2.49:
751 ``Copyright (c) jm soler juillet/novembre 2004-april 2009``.
754 cpx = self._point[0]
755 cpy = self._point[1]
756 rx = abs(rx)
757 ry = abs(ry)
758 px = abs((cos(ang) * (cpx - x) + sin(ang) * (cpy - y)) * 0.5) ** 2.0
759 py = abs((cos(ang) * (cpy - y) - sin(ang) * (cpx - x)) * 0.5) ** 2.0
760 rpx = rpy = 0.0
762 if abs(rx) > 0.0:
763 px = px / (rx ** 2.0)
765 if abs(ry) > 0.0:
766 rpy = py / (ry ** 2.0)
768 pl = rpx + rpy
769 if pl > 1.0:
770 pl = pl ** 0.5
771 rx *= pl
772 ry *= pl
774 carx = sarx = cary = sary = 0.0
776 if abs(rx) > 0.0:
777 carx = cos(ang) / rx
778 sarx = sin(ang) / rx
780 if abs(ry) > 0.0:
781 cary = cos(ang) / ry
782 sary = sin(ang) / ry
784 x0 = carx * cpx + sarx * cpy
785 y0 = -sary * cpx + cary * cpy
786 x1 = carx * x + sarx * y
787 y1 = -sary * x + cary * y
788 d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
790 if abs(d) > 0.0:
791 sq = 1.0 / d - 0.25
792 else:
793 sq = -0.25
795 if sq < 0.0:
796 sq = 0.0
798 sf = sq ** 0.5
799 if fs == fa:
800 sf = -sf
802 xc = 0.5 * (x0 + x1) - sf * (y1 - y0)
803 yc = 0.5 * (y0 + y1) + sf * (x1 - x0)
804 ang_0 = atan2(y0 - yc, x0 - xc)
805 ang_1 = atan2(y1 - yc, x1 - xc)
806 ang_arc = ang_1 - ang_0
808 if ang_arc < 0.0 and fs == 1:
809 ang_arc += 2.0 * pi
810 elif ang_arc > 0.0 and fs == 0:
811 ang_arc -= 2.0 * pi
813 n_segs = int(ceil(abs(ang_arc * 2.0 / (pi * 0.5 + 0.001))))
815 if self._spline is None:
816 self._appendPoint(cpx, cpy,
817 handle_left_type='FREE', handle_left=(cpx, cpy),
818 handle_right_type='FREE', handle_right=(cpx, cpy))
820 for i in range(n_segs):
821 ang0 = ang_0 + i * ang_arc / n_segs
822 ang1 = ang_0 + (i + 1) * ang_arc / n_segs
823 ang_demi = 0.25 * (ang1 - ang0)
824 t = 2.66666 * sin(ang_demi) * sin(ang_demi) / sin(ang_demi * 2.0)
825 x1 = xc + cos(ang0) - t * sin(ang0)
826 y1 = yc + sin(ang0) + t * cos(ang0)
827 x2 = xc + cos(ang1)
828 y2 = yc + sin(ang1)
829 x3 = x2 + t * sin(ang1)
830 y3 = y2 - t * cos(ang1)
832 coord1 = ((cos(ang) * rx) * x1 + (-sin(ang) * ry) * y1,
833 (sin(ang) * rx) * x1 + (cos(ang) * ry) * y1)
834 coord2 = ((cos(ang) * rx) * x3 + (-sin(ang) * ry) * y3,
835 (sin(ang) * rx) * x3 + (cos(ang) * ry) * y3)
836 coord3 = ((cos(ang) * rx) * x2 + (-sin(ang) * ry) * y2,
837 (sin(ang) * rx) * x2 + (cos(ang) * ry) * y2)
839 self._updateHandle(handle=coord1, handle_type='FREE')
841 self._appendPoint(coord3[0], coord3[1],
842 handle_left_type='FREE', handle_left=coord2,
843 handle_right_type='FREE', handle_right=coord3)
845 def _pathCurveToA(self, code):
847 Elliptical arc CurveTo path command
850 cur = self._data.cur()
852 while cur is not None and not cur.isalpha():
853 rx = float(self._data.next())
854 ry = float(self._data.next())
855 ang = float(self._data.next()) / 180 * pi
856 fa = float(self._data.next())
857 fs = float(self._data.next())
858 x, y = self._getCoordPair(code.islower(), self._point)
860 self._calcArc(rx, ry, ang, fa, fs, x, y)
862 self._point = (x, y)
863 self._handle = None
864 cur = self._data.cur()
866 def _pathClose(self, code):
868 Close path command
871 if self._spline:
872 self._spline['closed'] = True
874 cv = self._spline['points'][0]
875 self._point = (cv['x'], cv['y'])
877 def _pathCloseImplicitly(self):
879 Close path implicitly without changing current point coordinate
882 if self._spline:
883 self._spline['closed'] = True
885 def parse(self):
887 Execute parser
890 closed = False
892 while not self._data.eof():
893 code = self._data.next()
894 cmd = self._commands.get(code)
896 if cmd is None:
897 raise Exception('Unknown path command: {0}' . format(code))
899 if code in {'Z', 'z'}:
900 closed = True
901 else:
902 closed = False
904 if code in {'M', 'm'} and self._use_fill and not closed:
905 self._pathCloseImplicitly() # Ensure closed before MoveTo path command
907 cmd(code)
908 if self._use_fill and not closed:
909 self._pathCloseImplicitly() # Ensure closed at the end of parsing
911 def getSplines(self):
913 Get splines definitions
916 return self._splines
919 class SVGGeometry:
921 Abstract SVG geometry
924 __slots__ = ('_node', # XML node for geometry
925 '_context', # Global SVG context (holds matrices stack, i.e.)
926 '_creating') # Flag if geometry is already creating
927 # for this node
928 # need to detect cycles for USE node
930 def __init__(self, node, context):
932 Initialize SVG geometry
935 self._node = node
936 self._context = context
937 self._creating = False
939 if hasattr(node, 'getAttribute'):
940 defs = context['defines']
942 attr_id = node.getAttribute('id')
943 if attr_id and defs.get('#' + attr_id) is None:
944 defs['#' + attr_id] = self
946 className = node.getAttribute('class')
947 if className and defs.get(className) is None:
948 defs[className] = self
950 def _pushRect(self, rect):
952 Push display rectangle
955 self._context['rects'].append(rect)
956 self._context['rect'] = rect
958 def _popRect(self):
960 Pop display rectangle
963 self._context['rects'].pop()
964 self._context['rect'] = self._context['rects'][-1]
966 def _pushMatrix(self, matrix):
968 Push transformation matrix
971 current_matrix = self._context['matrix']
972 self._context['matrix_stack'].append(current_matrix)
973 self._context['matrix'] = current_matrix @ matrix
975 def _popMatrix(self):
977 Pop transformation matrix
980 old_matrix = self._context['matrix_stack'].pop()
981 self._context['matrix'] = old_matrix
983 def _pushStyle(self, style):
985 Push style
988 self._context['styles'].append(style)
989 self._context['style'] = style
991 def _popStyle(self):
993 Pop style
996 self._context['styles'].pop()
997 self._context['style'] = self._context['styles'][-1]
999 def _transformCoord(self, point):
1001 Transform SVG-file coords
1004 v = Vector((point[0], point[1], 0.0))
1006 return self._context['matrix'] @ v
1008 def getNodeMatrix(self):
1010 Get transformation matrix of node
1013 return SVGMatrixFromNode(self._node, self._context)
1015 def parse(self):
1017 Parse XML node to memory
1020 pass
1022 def _doCreateGeom(self, instancing):
1024 Internal handler to create real geometries
1027 pass
1029 def getTransformMatrix(self):
1031 Get matrix created from "transform" attribute
1034 transform = self._node.getAttribute('transform')
1036 if transform:
1037 return SVGParseTransform(transform)
1039 return None
1041 def createGeom(self, instancing):
1043 Create real geometries
1046 if self._creating:
1047 return
1049 self._creating = True
1051 matrix = self.getTransformMatrix()
1052 if matrix is not None:
1053 self._pushMatrix(matrix)
1055 self._doCreateGeom(instancing)
1057 if matrix is not None:
1058 self._popMatrix()
1060 self._creating = False
1063 class SVGGeometryContainer(SVGGeometry):
1065 Container of SVG geometries
1068 __slots__ = ('_geometries', # List of chold geometries
1069 '_styles') # Styles, used for displaying
1071 def __init__(self, node, context):
1073 Initialize SVG geometry container
1076 super().__init__(node, context)
1078 self._geometries = []
1079 self._styles = SVGEmptyStyles
1081 def parse(self):
1083 Parse XML node to memory
1086 if type(self._node) is xml.dom.minidom.Element:
1087 self._styles = SVGParseStyles(self._node, self._context)
1089 self._pushStyle(self._styles)
1091 for node in self._node.childNodes:
1092 if type(node) is not xml.dom.minidom.Element:
1093 continue
1095 ob = parseAbstractNode(node, self._context)
1096 if ob is not None:
1097 self._geometries.append(ob)
1099 self._popStyle()
1101 def _doCreateGeom(self, instancing):
1103 Create real geometries
1106 for geom in self._geometries:
1107 geom.createGeom(instancing)
1109 def getGeometries(self):
1111 Get list of parsed geometries
1114 return self._geometries
1117 class SVGGeometryPATH(SVGGeometry):
1119 SVG path geometry
1122 __slots__ = ('_splines', # List of splines after parsing
1123 '_styles') # Styles, used for displaying
1125 def __init__(self, node, context):
1127 Initialize SVG path
1130 super().__init__(node, context)
1132 self._splines = []
1133 self._styles = SVGEmptyStyles
1135 def parse(self):
1137 Parse SVG path node
1140 d = self._node.getAttribute('d')
1142 self._styles = SVGParseStyles(self._node, self._context)
1144 pathParser = SVGPathParser(d, self._styles['useFill'])
1145 pathParser.parse()
1147 self._splines = pathParser.getSplines()
1149 def _doCreateGeom(self, instancing):
1151 Create real geometries
1154 ob = SVGCreateCurve(self._context)
1155 cu = ob.data
1157 id_names_from_node(self._node, ob)
1159 if self._styles['useFill']:
1160 cu.dimensions = '2D'
1161 cu.fill_mode = 'BOTH'
1162 cu.materials.append(self._styles['fill'])
1163 else:
1164 cu.dimensions = '3D'
1166 for spline in self._splines:
1167 act_spline = None
1169 if spline['closed'] and len(spline['points']) >= 2:
1170 first = spline['points'][0]
1171 last = spline['points'][-1]
1172 if ( first['handle_left_type'] == 'FREE' and
1173 last['handle_right_type'] == 'VECTOR'):
1174 last['handle_right_type'] = 'FREE'
1175 last['handle_right'] = (last['x'], last['y'])
1176 if ( last['handle_right_type'] == 'FREE' and
1177 first['handle_left_type'] == 'VECTOR'):
1178 first['handle_left_type'] = 'FREE'
1179 first['handle_left'] = (first['x'], first['y'])
1181 for point in spline['points']:
1182 co = self._transformCoord((point['x'], point['y']))
1184 if act_spline is None:
1185 cu.splines.new('BEZIER')
1187 act_spline = cu.splines[-1]
1188 act_spline.use_cyclic_u = spline['closed']
1189 else:
1190 act_spline.bezier_points.add(1)
1192 bezt = act_spline.bezier_points[-1]
1193 bezt.co = co
1195 bezt.handle_left_type = point['handle_left_type']
1196 if point['handle_left'] is not None:
1197 handle = point['handle_left']
1198 bezt.handle_left = self._transformCoord(handle)
1200 bezt.handle_right_type = point['handle_right_type']
1201 if point['handle_right'] is not None:
1202 handle = point['handle_right']
1203 bezt.handle_right = self._transformCoord(handle)
1205 SVGFinishCurve()
1208 class SVGGeometryDEFS(SVGGeometryContainer):
1210 Container for referenced elements
1213 def createGeom(self, instancing):
1215 Create real geometries
1218 pass
1221 class SVGGeometrySYMBOL(SVGGeometryContainer):
1223 Referenced element
1226 def _doCreateGeom(self, instancing):
1228 Create real geometries
1231 self._pushMatrix(self.getNodeMatrix())
1233 super()._doCreateGeom(False)
1235 self._popMatrix()
1237 def createGeom(self, instancing):
1239 Create real geometries
1242 if not instancing:
1243 return
1245 super().createGeom(instancing)
1248 class SVGGeometryG(SVGGeometryContainer):
1250 Geometry group
1253 pass
1256 class SVGGeometryUSE(SVGGeometry):
1258 User of referenced elements
1261 def _doCreateGeom(self, instancing):
1263 Create real geometries
1266 ref = self._node.getAttribute('xlink:href')
1267 geom = self._context['defines'].get(ref)
1269 if geom is not None:
1270 rect = SVGRectFromNode(self._node, self._context)
1271 self._pushRect(rect)
1273 self._pushMatrix(self.getNodeMatrix())
1275 geom.createGeom(True)
1277 self._popMatrix()
1279 self._popRect()
1282 class SVGGeometryRECT(SVGGeometry):
1284 SVG rectangle
1287 __slots__ = ('_rect', # coordinate and dimensions of rectangle
1288 '_radius', # Rounded corner radiuses
1289 '_styles') # Styles, used for displaying
1291 def __init__(self, node, context):
1293 Initialize new rectangle
1296 super().__init__(node, context)
1298 self._rect = ('0', '0', '0', '0')
1299 self._radius = ('0', '0')
1300 self._styles = SVGEmptyStyles
1302 def parse(self):
1304 Parse SVG rectangle node
1307 self._styles = SVGParseStyles(self._node, self._context)
1309 rect = []
1310 for attr in ['x', 'y', 'width', 'height']:
1311 val = self._node.getAttribute(attr)
1312 rect.append(val or '0')
1314 self._rect = (rect)
1316 rx = self._node.getAttribute('rx')
1317 ry = self._node.getAttribute('ry')
1319 self._radius = (rx, ry)
1321 def _appendCorner(self, spline, coord, firstTime, rounded):
1323 Append new corner to rectangle
1326 handle = None
1327 if len(coord) == 3:
1328 handle = self._transformCoord(coord[2])
1329 coord = (coord[0], coord[1])
1331 co = self._transformCoord(coord)
1333 if not firstTime:
1334 spline.bezier_points.add(1)
1336 bezt = spline.bezier_points[-1]
1337 bezt.co = co
1339 if rounded:
1340 if handle:
1341 bezt.handle_left_type = 'VECTOR'
1342 bezt.handle_right_type = 'FREE'
1344 bezt.handle_right = handle
1345 else:
1346 bezt.handle_left_type = 'FREE'
1347 bezt.handle_right_type = 'VECTOR'
1348 bezt.handle_left = co
1350 else:
1351 bezt.handle_left_type = 'VECTOR'
1352 bezt.handle_right_type = 'VECTOR'
1354 def _doCreateGeom(self, instancing):
1356 Create real geometries
1359 # Run-time parsing -- percents would be correct only if
1360 # parsing them now
1361 crect = self._context['rect']
1362 rect = []
1364 for i in range(4):
1365 rect.append(SVGParseCoord(self._rect[i], crect[i % 2]))
1367 r = self._radius
1368 rx = ry = 0.0
1370 if r[0] and r[1]:
1371 rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
1372 ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
1373 elif r[0]:
1374 rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
1375 ry = min(rx, rect[3] / 2)
1376 rx = ry = min(rx, ry)
1377 elif r[1]:
1378 ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
1379 rx = min(ry, rect[2] / 2)
1380 rx = ry = min(rx, ry)
1382 radius = (rx, ry)
1384 # Geometry creation
1385 ob = SVGCreateCurve(self._context)
1386 cu = ob.data
1388 id_names_from_node(self._node, ob)
1390 if self._styles['useFill']:
1391 cu.dimensions = '2D'
1392 cu.fill_mode = 'BOTH'
1393 cu.materials.append(self._styles['fill'])
1394 else:
1395 cu.dimensions = '3D'
1397 cu.splines.new('BEZIER')
1399 spline = cu.splines[-1]
1400 spline.use_cyclic_u = True
1402 x, y = rect[0], rect[1]
1403 w, h = rect[2], rect[3]
1404 rx, ry = radius[0], radius[1]
1405 rounded = False
1407 if rx or ry:
1409 # 0 _______ 1
1410 # / \
1411 # / \
1412 # 7 2
1413 # | |
1414 # | |
1415 # 6 3
1416 # \ /
1417 # \ /
1418 # 5 _______ 4
1421 # Optional third component -- right handle coord
1422 coords = [(x + rx, y),
1423 (x + w - rx, y, (x + w, y)),
1424 (x + w, y + ry),
1425 (x + w, y + h - ry, (x + w, y + h)),
1426 (x + w - rx, y + h),
1427 (x + rx, y + h, (x, y + h)),
1428 (x, y + h - ry),
1429 (x, y + ry, (x, y))]
1431 rounded = True
1432 else:
1433 coords = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
1435 firstTime = True
1436 for coord in coords:
1437 self._appendCorner(spline, coord, firstTime, rounded)
1438 firstTime = False
1440 SVGFinishCurve()
1443 class SVGGeometryELLIPSE(SVGGeometry):
1445 SVG ellipse
1448 __slots__ = ('_cx', # X-coordinate of center
1449 '_cy', # Y-coordinate of center
1450 '_rx', # X-axis radius of circle
1451 '_ry', # Y-axis radius of circle
1452 '_styles') # Styles, used for displaying
1454 def __init__(self, node, context):
1456 Initialize new ellipse
1459 super().__init__(node, context)
1461 self._cx = '0.0'
1462 self._cy = '0.0'
1463 self._rx = '0.0'
1464 self._ry = '0.0'
1465 self._styles = SVGEmptyStyles
1467 def parse(self):
1469 Parse SVG ellipse node
1472 self._styles = SVGParseStyles(self._node, self._context)
1474 self._cx = self._node.getAttribute('cx') or '0'
1475 self._cy = self._node.getAttribute('cy') or '0'
1476 self._rx = self._node.getAttribute('rx') or '0'
1477 self._ry = self._node.getAttribute('ry') or '0'
1479 def _doCreateGeom(self, instancing):
1481 Create real geometries
1484 # Run-time parsing -- percents would be correct only if
1485 # parsing them now
1486 crect = self._context['rect']
1488 cx = SVGParseCoord(self._cx, crect[0])
1489 cy = SVGParseCoord(self._cy, crect[1])
1490 rx = SVGParseCoord(self._rx, crect[0])
1491 ry = SVGParseCoord(self._ry, crect[1])
1493 if not rx or not ry:
1494 # Automaic handles will work incorrect in this case
1495 return
1497 # Create circle
1498 ob = SVGCreateCurve(self._context)
1499 cu = ob.data
1501 id_names_from_node(self._node, ob)
1503 if self._styles['useFill']:
1504 cu.dimensions = '2D'
1505 cu.fill_mode = 'BOTH'
1506 cu.materials.append(self._styles['fill'])
1507 else:
1508 cu.dimensions = '3D'
1510 coords = [((cx - rx, cy),
1511 (cx - rx, cy + ry * 0.552),
1512 (cx - rx, cy - ry * 0.552)),
1514 ((cx, cy - ry),
1515 (cx - rx * 0.552, cy - ry),
1516 (cx + rx * 0.552, cy - ry)),
1518 ((cx + rx, cy),
1519 (cx + rx, cy - ry * 0.552),
1520 (cx + rx, cy + ry * 0.552)),
1522 ((cx, cy + ry),
1523 (cx + rx * 0.552, cy + ry),
1524 (cx - rx * 0.552, cy + ry))]
1526 spline = None
1527 for coord in coords:
1528 co = self._transformCoord(coord[0])
1529 handle_left = self._transformCoord(coord[1])
1530 handle_right = self._transformCoord(coord[2])
1532 if spline is None:
1533 cu.splines.new('BEZIER')
1534 spline = cu.splines[-1]
1535 spline.use_cyclic_u = True
1536 else:
1537 spline.bezier_points.add(1)
1539 bezt = spline.bezier_points[-1]
1540 bezt.co = co
1541 bezt.handle_left_type = 'FREE'
1542 bezt.handle_right_type = 'FREE'
1543 bezt.handle_left = handle_left
1544 bezt.handle_right = handle_right
1546 SVGFinishCurve()
1549 class SVGGeometryCIRCLE(SVGGeometryELLIPSE):
1551 SVG circle
1554 def parse(self):
1556 Parse SVG circle node
1559 self._styles = SVGParseStyles(self._node, self._context)
1561 self._cx = self._node.getAttribute('cx') or '0'
1562 self._cy = self._node.getAttribute('cy') or '0'
1564 r = self._node.getAttribute('r') or '0'
1565 self._rx = self._ry = r
1568 class SVGGeometryLINE(SVGGeometry):
1570 SVG line
1573 __slots__ = ('_x1', # X-coordinate of beginning
1574 '_y1', # Y-coordinate of beginning
1575 '_x2', # X-coordinate of ending
1576 '_y2') # Y-coordinate of ending
1578 def __init__(self, node, context):
1580 Initialize new line
1583 super().__init__(node, context)
1585 self._x1 = '0.0'
1586 self._y1 = '0.0'
1587 self._x2 = '0.0'
1588 self._y2 = '0.0'
1590 def parse(self):
1592 Parse SVG line node
1595 self._x1 = self._node.getAttribute('x1') or '0'
1596 self._y1 = self._node.getAttribute('y1') or '0'
1597 self._x2 = self._node.getAttribute('x2') or '0'
1598 self._y2 = self._node.getAttribute('y2') or '0'
1600 def _doCreateGeom(self, instancing):
1602 Create real geometries
1605 # Run-time parsing -- percents would be correct only if
1606 # parsing them now
1607 crect = self._context['rect']
1609 x1 = SVGParseCoord(self._x1, crect[0])
1610 y1 = SVGParseCoord(self._y1, crect[1])
1611 x2 = SVGParseCoord(self._x2, crect[0])
1612 y2 = SVGParseCoord(self._y2, crect[1])
1614 # Create cline
1615 ob = SVGCreateCurve(self._context)
1616 cu = ob.data
1618 id_names_from_node(self._node, ob)
1620 coords = [(x1, y1), (x2, y2)]
1621 spline = None
1623 for coord in coords:
1624 co = self._transformCoord(coord)
1626 if spline is None:
1627 cu.splines.new('BEZIER')
1628 spline = cu.splines[-1]
1629 spline.use_cyclic_u = True
1630 else:
1631 spline.bezier_points.add(1)
1633 bezt = spline.bezier_points[-1]
1634 bezt.co = co
1635 bezt.handle_left_type = 'VECTOR'
1636 bezt.handle_right_type = 'VECTOR'
1638 SVGFinishCurve()
1641 class SVGGeometryPOLY(SVGGeometry):
1643 Abstract class for handling poly-geometries
1644 (polylines and polygons)
1647 __slots__ = ('_points', # Array of points for poly geometry
1648 '_styles', # Styles, used for displaying
1649 '_closed') # Should generated curve be closed?
1651 def __init__(self, node, context):
1653 Initialize new poly geometry
1656 super().__init__(node, context)
1658 self._points = []
1659 self._styles = SVGEmptyStyles
1660 self._closed = False
1662 def parse(self):
1664 Parse poly node
1667 self._styles = SVGParseStyles(self._node, self._context)
1669 points = parse_array_of_floats(self._node.getAttribute('points'))
1671 prev = None
1672 self._points = []
1674 for p in points:
1675 if prev is None:
1676 prev = p
1677 else:
1678 self._points.append((prev, p))
1679 prev = None
1681 def _doCreateGeom(self, instancing):
1683 Create real geometries
1686 ob = SVGCreateCurve(self._context)
1687 cu = ob.data
1689 id_names_from_node(self._node, ob)
1691 if self._closed and self._styles['useFill']:
1692 cu.dimensions = '2D'
1693 cu.fill_mode = 'BOTH'
1694 cu.materials.append(self._styles['fill'])
1695 else:
1696 cu.dimensions = '3D'
1698 spline = None
1700 for point in self._points:
1701 co = self._transformCoord(point)
1703 if spline is None:
1704 cu.splines.new('BEZIER')
1705 spline = cu.splines[-1]
1706 spline.use_cyclic_u = self._closed
1707 else:
1708 spline.bezier_points.add(1)
1710 bezt = spline.bezier_points[-1]
1711 bezt.co = co
1712 bezt.handle_left_type = 'VECTOR'
1713 bezt.handle_right_type = 'VECTOR'
1715 SVGFinishCurve()
1718 class SVGGeometryPOLYLINE(SVGGeometryPOLY):
1720 SVG polyline geometry
1723 pass
1726 class SVGGeometryPOLYGON(SVGGeometryPOLY):
1728 SVG polygon geometry
1731 def __init__(self, node, context):
1733 Initialize new polygon geometry
1736 super().__init__(node, context)
1738 self._closed = True
1741 class SVGGeometrySVG(SVGGeometryContainer):
1743 Main geometry holder
1746 def _doCreateGeom(self, instancing):
1748 Create real geometries
1751 rect = SVGRectFromNode(self._node, self._context)
1753 matrix = self.getNodeMatrix()
1755 # Better SVG compatibility: match svg-document units
1756 # with blender units
1758 viewbox = []
1759 unit = ''
1761 if self._node.getAttribute('height'):
1762 raw_height = self._node.getAttribute('height')
1763 token, last_char = read_float(raw_height)
1764 document_height = float(token)
1765 unit = raw_height[last_char:].strip()
1767 if self._node.getAttribute('viewBox'):
1768 viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
1770 if len(viewbox) == 4 and unit in ('cm', 'mm', 'in', 'pt', 'pc'):
1772 #convert units to BU:
1773 unitscale = units[unit] / 90 * 1000 / 39.3701
1775 #apply blender unit scale:
1776 unitscale = unitscale / bpy.context.scene.unit_settings.scale_length
1778 matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((1.0, 0.0, 0.0)))
1779 matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((0.0, 1.0, 0.0)))
1781 # match document origin with 3D space origin.
1782 if self._node.getAttribute('viewBox'):
1783 viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
1784 matrix = matrix @ matrix.Translation([0.0, - viewbox[1] - viewbox[3], 0.0])
1786 self._pushMatrix(matrix)
1787 self._pushRect(rect)
1789 super()._doCreateGeom(False)
1791 self._popRect()
1792 self._popMatrix()
1795 class SVGLoader(SVGGeometryContainer):
1797 SVG file loader
1800 def getTransformMatrix(self):
1802 Get matrix created from "transform" attribute
1805 # SVG document doesn't support transform specification
1806 # it can't even hold attributes
1808 return None
1810 def __init__(self, context, filepath, do_colormanage):
1812 Initialize SVG loader
1814 import os
1816 svg_name = os.path.basename(filepath)
1817 scene = context.scene
1818 collection = bpy.data.collections.new(name=svg_name)
1819 scene.collection.children.link(collection)
1821 node = xml.dom.minidom.parse(filepath)
1823 m = Matrix()
1824 m = m @ Matrix.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
1825 m = m @ Matrix.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
1827 rect = (0, 0)
1829 self._context = {'defines': {},
1830 'rects': [rect],
1831 'rect': rect,
1832 'matrix_stack': [],
1833 'matrix': m,
1834 'materials': {},
1835 'styles': [None],
1836 'style': None,
1837 'do_colormanage': do_colormanage,
1838 'collection': collection}
1840 super().__init__(node, self._context)
1843 svgGeometryClasses = {
1844 'svg': SVGGeometrySVG,
1845 'path': SVGGeometryPATH,
1846 'defs': SVGGeometryDEFS,
1847 'symbol': SVGGeometrySYMBOL,
1848 'use': SVGGeometryUSE,
1849 'rect': SVGGeometryRECT,
1850 'ellipse': SVGGeometryELLIPSE,
1851 'circle': SVGGeometryCIRCLE,
1852 'line': SVGGeometryLINE,
1853 'polyline': SVGGeometryPOLYLINE,
1854 'polygon': SVGGeometryPOLYGON,
1855 'g': SVGGeometryG}
1858 def parseAbstractNode(node, context):
1859 name = node.tagName.lower()
1861 if name.startswith('svg:'):
1862 name = name[4:]
1864 geomClass = svgGeometryClasses.get(name)
1866 if geomClass is not None:
1867 ob = geomClass(node, context)
1868 ob.parse()
1870 return ob
1872 return None
1875 def load_svg(context, filepath, do_colormanage):
1877 Load specified SVG file
1880 if bpy.ops.object.mode_set.poll():
1881 bpy.ops.object.mode_set(mode='OBJECT')
1883 loader = SVGLoader(context, filepath, do_colormanage)
1884 loader.parse()
1885 loader.createGeom(False)
1888 def load(operator, context, filepath=""):
1890 # error in code should raise exceptions but loading
1891 # non SVG files can give useful messages.
1892 do_colormanage = context.scene.display_settings.display_device != 'NONE'
1893 try:
1894 load_svg(context, filepath, do_colormanage)
1895 except (xml.parsers.expat.ExpatError, UnicodeEncodeError) as e:
1896 import traceback
1897 traceback.print_exc()
1899 operator.report({'WARNING'}, tip_("Unable to parse XML, %s:%s for file %r") % (type(e).__name__, e, filepath))
1900 return {'CANCELLED'}
1902 return {'FINISHED'}