File headers: use SPDX license identifiers
[blender-addons.git] / io_curve_svg / import_svg.py
blobc9df4dfba8f2617fb70fd7cef092cd50db586462
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 import re
6 import xml.dom.minidom
7 from math import cos, sin, tan, atan2, pi, ceil
9 import bpy
10 from mathutils import Vector, Matrix
12 from . import svg_colors
13 from .svg_util import (units,
14 srgb_to_linearrgb,
15 check_points_equal,
16 parse_array_of_floats,
17 read_float)
19 #### Common utilities ####
21 SVGEmptyStyles = {'useFill': None,
22 'fill': None}
25 def SVGCreateCurve(context):
26 """
27 Create new curve object to hold splines in
28 """
30 cu = bpy.data.curves.new("Curve", 'CURVE')
31 obj = bpy.data.objects.new("Curve", cu)
33 context['collection'].objects.link(obj)
35 return obj
38 def SVGFinishCurve():
39 """
40 Finish curve creation
41 """
43 pass
46 def SVGFlipHandle(x, y, x1, y1):
47 """
48 Flip handle around base point
49 """
51 x = x + (x - x1)
52 y = y + (y - y1)
54 return x, y
57 def SVGParseCoord(coord, size):
58 """
59 Parse coordinate component to common basis
61 Needed to handle coordinates set in cm, mm, inches.
62 """
64 token, last_char = read_float(coord)
65 val = float(token)
66 unit = coord[last_char:].strip() # strip() in case there is a space
68 if unit == '%':
69 return float(size) / 100.0 * val
70 return val * units[unit]
73 def SVGRectFromNode(node, context):
74 """
75 Get display rectangle from node
76 """
78 w = context['rect'][0]
79 h = context['rect'][1]
81 if node.getAttribute('viewBox'):
82 viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
83 w = SVGParseCoord(viewBox[2], w)
84 h = SVGParseCoord(viewBox[3], h)
85 else:
86 if node.getAttribute('width'):
87 w = SVGParseCoord(node.getAttribute('width'), w)
89 if node.getAttribute('height'):
90 h = SVGParseCoord(node.getAttribute('height'), h)
92 return (w, h)
95 def SVGMatrixFromNode(node, context):
96 """
97 Get transformation matrix from given node
98 """
100 tagName = node.tagName.lower()
101 tags = ['svg:svg', 'svg:use', 'svg:symbol']
103 if tagName not in tags and 'svg:' + tagName not in tags:
104 return Matrix()
106 rect = context['rect']
107 has_user_coordinate = (len(context['rects']) > 1)
109 m = Matrix()
110 x = SVGParseCoord(node.getAttribute('x') or '0', rect[0])
111 y = SVGParseCoord(node.getAttribute('y') or '0', rect[1])
112 w = SVGParseCoord(node.getAttribute('width') or str(rect[0]), rect[0])
113 h = SVGParseCoord(node.getAttribute('height') or str(rect[1]), rect[1])
115 m = Matrix.Translation(Vector((x, y, 0.0)))
116 if has_user_coordinate:
117 if rect[0] != 0 and rect[1] != 0:
118 m = m @ Matrix.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0)))
119 m = m @ Matrix.Scale(h / rect[1], 4, Vector((0.0, 1.0, 0.0)))
121 if node.getAttribute('viewBox'):
122 viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
123 vx = SVGParseCoord(viewBox[0], w)
124 vy = SVGParseCoord(viewBox[1], h)
125 vw = SVGParseCoord(viewBox[2], w)
126 vh = SVGParseCoord(viewBox[3], h)
128 if vw == 0 or vh == 0:
129 return m
131 if has_user_coordinate or (w != 0 and h != 0):
132 sx = w / vw
133 sy = h / vh
134 scale = min(sx, sy)
135 else:
136 scale = 1.0
137 w = vw
138 h = vh
140 tx = (w - vw * scale) / 2
141 ty = (h - vh * scale) / 2
142 m = m @ Matrix.Translation(Vector((tx, ty, 0.0)))
144 m = m @ Matrix.Translation(Vector((-vx, -vy, 0.0)))
145 m = m @ Matrix.Scale(scale, 4, Vector((1.0, 0.0, 0.0)))
146 m = m @ Matrix.Scale(scale, 4, Vector((0.0, 1.0, 0.0)))
148 return m
151 def SVGParseTransform(transform):
153 Parse transform string and return transformation matrix
156 m = Matrix()
157 r = re.compile(r'\s*([A-z]+)\s*\((.*?)\)')
159 for match in r.finditer(transform):
160 func = match.group(1)
161 params = match.group(2)
162 params = params.replace(',', ' ').split()
164 proc = SVGTransforms.get(func)
165 if proc is None:
166 raise Exception('Unknown trasnform function: ' + func)
168 m = m @ proc(params)
170 return m
173 def SVGGetMaterial(color, context):
175 Get material for specified color
178 materials = context['materials']
179 rgb_re = re.compile(r'^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
181 if color in materials:
182 return materials[color]
184 diff = None
185 if color.startswith('#'):
186 color = color[1:]
188 if len(color) == 3:
189 color = color[0] * 2 + color[1] * 2 + color[2] * 2
191 diff = (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
192 elif color in svg_colors.SVGColors:
193 diff = svg_colors.SVGColors[color]
194 elif rgb_re.match(color):
195 c = rgb_re.findall(color)[0]
196 diff = (float(c[0]), float(c[1]), float(c[2]))
197 else:
198 return None
200 diffuse_color = ([x / 255.0 for x in diff])
202 if context['do_colormanage']:
203 diffuse_color[0] = srgb_to_linearrgb(diffuse_color[0])
204 diffuse_color[1] = srgb_to_linearrgb(diffuse_color[1])
205 diffuse_color[2] = srgb_to_linearrgb(diffuse_color[2])
207 mat = bpy.data.materials.new(name='SVGMat')
208 mat.diffuse_color = (*diffuse_color, 1.0)
210 materials[color] = mat
212 return mat
215 def SVGTransformTranslate(params):
217 translate SVG transform command
220 tx = float(params[0])
221 ty = float(params[1]) if len(params) > 1 else 0.0
223 return Matrix.Translation(Vector((tx, ty, 0.0)))
226 def SVGTransformMatrix(params):
228 matrix SVG transform command
231 a = float(params[0])
232 b = float(params[1])
233 c = float(params[2])
234 d = float(params[3])
235 e = float(params[4])
236 f = float(params[5])
238 return Matrix(((a, c, 0.0, e),
239 (b, d, 0.0, f),
240 (0, 0, 1.0, 0),
241 (0, 0, 0.0, 1)))
244 def SVGTransformScale(params):
246 scale SVG transform command
249 sx = float(params[0])
250 sy = float(params[1]) if len(params) > 1 else sx
252 m = Matrix()
254 m = m @ Matrix.Scale(sx, 4, Vector((1.0, 0.0, 0.0)))
255 m = m @ Matrix.Scale(sy, 4, Vector((0.0, 1.0, 0.0)))
257 return m
260 def SVGTransformSkewY(params):
262 skewY SVG transform command
265 ang = float(params[0]) * pi / 180.0
267 return Matrix(((1.0, 0.0, 0.0),
268 (tan(ang), 1.0, 0.0),
269 (0.0, 0.0, 1.0))).to_4x4()
272 def SVGTransformSkewX(params):
274 skewX SVG transform command
277 ang = float(params[0]) * pi / 180.0
279 return Matrix(((1.0, tan(ang), 0.0),
280 (0.0, 1.0, 0.0),
281 (0.0, 0.0, 1.0))).to_4x4()
284 def SVGTransformRotate(params):
286 skewX SVG transform command
289 ang = float(params[0]) * pi / 180.0
290 cx = cy = 0.0
292 if len(params) >= 3:
293 cx = float(params[1])
294 cy = float(params[2])
296 tm = Matrix.Translation(Vector((cx, cy, 0.0)))
297 rm = Matrix.Rotation(ang, 4, Vector((0.0, 0.0, 1.0)))
299 return tm @ rm @ tm.inverted()
301 SVGTransforms = {'translate': SVGTransformTranslate,
302 'scale': SVGTransformScale,
303 'skewX': SVGTransformSkewX,
304 'skewY': SVGTransformSkewY,
305 'matrix': SVGTransformMatrix,
306 'rotate': SVGTransformRotate}
309 def SVGParseStyles(node, context):
311 Parse node to get different styles for displaying geometries
312 (materials, filling flags, etc..)
315 styles = SVGEmptyStyles.copy()
317 style = node.getAttribute('style')
318 if style:
319 elems = style.split(';')
320 for elem in elems:
321 s = elem.split(':')
323 if len(s) != 2:
324 continue
326 name = s[0].strip().lower()
327 val = s[1].strip()
329 if name == 'fill':
330 val = val.lower()
331 if val == 'none':
332 styles['useFill'] = False
333 else:
334 styles['useFill'] = True
335 styles['fill'] = SVGGetMaterial(val, context)
337 if styles['useFill'] is None:
338 styles['useFill'] = True
339 styles['fill'] = SVGGetMaterial('#000', context)
341 return styles
343 if styles['useFill'] is None:
344 fill = node.getAttribute('fill')
345 if fill:
346 fill = fill.lower()
347 if fill == 'none':
348 styles['useFill'] = False
349 else:
350 styles['useFill'] = True
351 styles['fill'] = SVGGetMaterial(fill, context)
353 if styles['useFill'] is None and context['style']:
354 styles = context['style'].copy()
356 if styles['useFill'] is None:
357 styles['useFill'] = True
358 styles['fill'] = SVGGetMaterial('#000', context)
360 return styles
362 def id_names_from_node(node, ob):
363 if node.getAttribute('id'):
364 name = node.getAttribute('id')
365 ob.name = name
366 ob.data.name = name
368 #### SVG path helpers ####
371 class SVGPathData:
373 SVG Path data token supplier
376 __slots__ = ('_data', # List of tokens
377 '_index', # Index of current token in tokens list
378 '_len') # Length of tokens list
380 def __init__(self, d):
382 Initialize new path data supplier
384 d - the definition of the outline of a shape
387 spaces = ' ,\t'
388 commands = {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
389 current_command = ''
390 tokens = []
392 i = 0
393 n = len(d)
394 while i < n:
395 c = d[i]
397 if c in spaces:
398 pass
399 elif c.lower() in commands:
400 tokens.append(c)
401 current_command = c
402 arg_index = 1
403 elif c in ['-', '.'] or c.isdigit():
404 # Special case for 'a/A' commands.
405 # Arguments 4 and 5 are either 0 or 1 and might not
406 # be separated from the next argument with space or comma.
407 if current_command.lower() == 'a':
408 if arg_index % 7 in [4,5]:
409 token = d[i]
410 last_char = i + 1
411 else:
412 token, last_char = read_float(d, i)
413 else:
414 token, last_char = read_float(d, i)
416 arg_index += 1
417 tokens.append(token)
419 # in most cases len(token) and (last_char - i) are the same
420 # but with whitespace or ',' prefix they are not.
422 i += (last_char - i) - 1
424 i += 1
426 self._data = tokens
427 self._index = 0
428 self._len = len(tokens)
430 def eof(self):
432 Check if end of data reached
435 return self._index >= self._len
437 def cur(self):
439 Return current token
442 if self.eof():
443 return None
445 return self._data[self._index]
447 def lookupNext(self):
449 get next token without moving pointer
452 if self.eof():
453 return None
455 return self._data[self._index]
457 def next(self):
459 Return current token and go to next one
462 if self.eof():
463 return None
465 token = self._data[self._index]
466 self._index += 1
468 return token
470 def nextCoord(self):
472 Return coordinate created from current token and move to next token
475 token = self.next()
477 if token is None:
478 return None
480 return float(token)
483 class SVGPathParser:
485 Parser of SVG path data
488 __slots__ = ('_data', # Path data supplird
489 '_point', # Current point coorfinate
490 '_handle', # Last handle coordinate
491 '_splines', # List of all splies created during parsing
492 '_spline', # Currently handling spline
493 '_commands', # Hash of all supported path commands
494 '_use_fill', # Splines would be filled, so expected to be closed
497 def __init__(self, d, use_fill):
499 Initialize path parser
501 d - the definition of the outline of a shape
504 self._data = SVGPathData(d)
505 self._point = None # Current point
506 self._handle = None # Last handle
507 self._splines = [] # List of splines in path
508 self._spline = None # Current spline
509 self._use_fill = use_fill
511 self._commands = {'M': self._pathMoveTo,
512 'L': self._pathLineTo,
513 'H': self._pathLineTo,
514 'V': self._pathLineTo,
515 'C': self._pathCurveToCS,
516 'S': self._pathCurveToCS,
517 'Q': self._pathCurveToQT,
518 'T': self._pathCurveToQT,
519 'A': self._pathCurveToA,
520 'Z': self._pathClose,
522 'm': self._pathMoveTo,
523 'l': self._pathLineTo,
524 'h': self._pathLineTo,
525 'v': self._pathLineTo,
526 'c': self._pathCurveToCS,
527 's': self._pathCurveToCS,
528 'q': self._pathCurveToQT,
529 't': self._pathCurveToQT,
530 'a': self._pathCurveToA,
531 'z': self._pathClose}
533 def _getCoordPair(self, relative, point):
535 Get next coordinate pair
538 x = self._data.nextCoord()
539 y = self._data.nextCoord()
541 if relative and point is not None:
542 x += point[0]
543 y += point[1]
545 return x, y
547 def _appendPoint(self, x, y, handle_left=None, handle_left_type='VECTOR',
548 handle_right=None, handle_right_type='VECTOR'):
550 Append point to spline
552 If there's no active spline, create one and set it's first point
553 to current point coordinate
556 if self._spline is None:
557 self._spline = {'points': [],
558 'closed': False}
560 self._splines.append(self._spline)
562 if len(self._spline['points']) > 0:
563 # Not sure about specifications, but Illustrator could create
564 # last point at the same position, as start point (which was
565 # reached by MoveTo command) to set needed handle coords.
566 # It's also could use last point at last position to make path
567 # filled.
569 first = self._spline['points'][0]
570 if check_points_equal((first['x'], first['y']), (x, y)):
571 if handle_left is not None:
572 first['handle_left'] = handle_left
573 first['handle_left_type'] = 'FREE'
575 if handle_left_type != 'VECTOR':
576 first['handle_left_type'] = handle_left_type
578 if self._data.eof() or self._data.lookupNext().lower() == 'm':
579 self._spline['closed'] = True
581 return
583 last = self._spline['points'][-1]
584 if last['handle_right_type'] == 'VECTOR' and handle_left_type == 'FREE':
585 last['handle_right'] = (last['x'], last['y'])
586 last['handle_right_type'] = 'FREE'
587 if last['handle_right_type'] == 'FREE' and handle_left_type == 'VECTOR':
588 handle_left = (x, y)
589 handle_left_type = 'FREE'
591 point = {'x': x,
592 'y': y,
594 'handle_left': handle_left,
595 'handle_left_type': handle_left_type,
597 'handle_right': handle_right,
598 'handle_right_type': handle_right_type}
600 self._spline['points'].append(point)
602 def _updateHandle(self, handle=None, handle_type=None):
604 Update right handle of previous point when adding new point to spline
607 point = self._spline['points'][-1]
609 if handle_type is not None:
610 point['handle_right_type'] = handle_type
612 if handle is not None:
613 point['handle_right'] = handle
615 def _pathMoveTo(self, code):
617 MoveTo path command
620 relative = code.islower()
621 x, y = self._getCoordPair(relative, self._point)
623 self._spline = None # Flag to start new spline
624 self._point = (x, y)
626 cur = self._data.cur()
627 while cur is not None and not cur.isalpha():
628 x, y = self._getCoordPair(relative, self._point)
630 if self._spline is None:
631 self._appendPoint(self._point[0], self._point[1])
633 self._appendPoint(x, y)
635 self._point = (x, y)
636 cur = self._data.cur()
638 self._handle = None
640 def _pathLineTo(self, code):
642 LineTo path command
645 c = code.lower()
647 cur = self._data.cur()
648 while cur is not None and not cur.isalpha():
649 if c == 'l':
650 x, y = self._getCoordPair(code == 'l', self._point)
651 elif c == 'h':
652 x = self._data.nextCoord()
653 y = self._point[1]
654 else:
655 x = self._point[0]
656 y = self._data.nextCoord()
658 if code == 'h':
659 x += self._point[0]
660 elif code == 'v':
661 y += self._point[1]
663 if self._spline is None:
664 self._appendPoint(self._point[0], self._point[1])
666 self._appendPoint(x, y)
668 self._point = (x, y)
669 cur = self._data.cur()
671 self._handle = None
673 def _pathCurveToCS(self, code):
675 Cubic BEZIER CurveTo path command
678 c = code.lower()
679 cur = self._data.cur()
680 while cur is not None and not cur.isalpha():
681 if c == 'c':
682 x1, y1 = self._getCoordPair(code.islower(), self._point)
683 x2, y2 = self._getCoordPair(code.islower(), self._point)
684 else:
685 if self._handle is not None:
686 x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
687 self._handle[0], self._handle[1])
688 else:
689 x1, y1 = self._point
691 x2, y2 = self._getCoordPair(code.islower(), self._point)
693 x, y = self._getCoordPair(code.islower(), self._point)
695 if self._spline is None:
696 self._appendPoint(self._point[0], self._point[1],
697 handle_left_type='FREE', handle_left=self._point,
698 handle_right_type='FREE', handle_right=(x1, y1))
699 else:
700 self._updateHandle(handle=(x1, y1), handle_type='FREE')
702 self._appendPoint(x, y,
703 handle_left_type='FREE', handle_left=(x2, y2),
704 handle_right_type='FREE', handle_right=(x, y))
706 self._point = (x, y)
707 self._handle = (x2, y2)
708 cur = self._data.cur()
710 def _pathCurveToQT(self, code):
712 Quadratic BEZIER CurveTo path command
715 c = code.lower()
716 cur = self._data.cur()
718 while cur is not None and not cur.isalpha():
719 if c == 'q':
720 x1, y1 = self._getCoordPair(code.islower(), self._point)
721 else:
722 if self._handle is not None:
723 x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
724 self._handle[0], self._handle[1])
725 else:
726 x1, y1 = self._point
728 x, y = self._getCoordPair(code.islower(), self._point)
730 if not check_points_equal((x, y), self._point):
731 if self._spline is None:
732 self._appendPoint(self._point[0], self._point[1],
733 handle_left_type='FREE', handle_left=self._point,
734 handle_right_type='FREE', handle_right=self._point)
736 self._appendPoint(x, y,
737 handle_left_type='FREE', handle_left=(x1, y1),
738 handle_right_type='FREE', handle_right=(x, y))
740 self._point = (x, y)
741 self._handle = (x1, y1)
742 cur = self._data.cur()
744 def _calcArc(self, rx, ry, ang, fa, fs, x, y):
746 Calc arc paths
748 Copied and adoptedfrom paths_svg2obj.py script for Blender 2.49
749 which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
752 cpx = self._point[0]
753 cpy = self._point[1]
754 rx = abs(rx)
755 ry = abs(ry)
756 px = abs((cos(ang) * (cpx - x) + sin(ang) * (cpy - y)) * 0.5) ** 2.0
757 py = abs((cos(ang) * (cpy - y) - sin(ang) * (cpx - x)) * 0.5) ** 2.0
758 rpx = rpy = 0.0
760 if abs(rx) > 0.0:
761 px = px / (rx ** 2.0)
763 if abs(ry) > 0.0:
764 rpy = py / (ry ** 2.0)
766 pl = rpx + rpy
767 if pl > 1.0:
768 pl = pl ** 0.5
769 rx *= pl
770 ry *= pl
772 carx = sarx = cary = sary = 0.0
774 if abs(rx) > 0.0:
775 carx = cos(ang) / rx
776 sarx = sin(ang) / rx
778 if abs(ry) > 0.0:
779 cary = cos(ang) / ry
780 sary = sin(ang) / ry
782 x0 = carx * cpx + sarx * cpy
783 y0 = -sary * cpx + cary * cpy
784 x1 = carx * x + sarx * y
785 y1 = -sary * x + cary * y
786 d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
788 if abs(d) > 0.0:
789 sq = 1.0 / d - 0.25
790 else:
791 sq = -0.25
793 if sq < 0.0:
794 sq = 0.0
796 sf = sq ** 0.5
797 if fs == fa:
798 sf = -sf
800 xc = 0.5 * (x0 + x1) - sf * (y1 - y0)
801 yc = 0.5 * (y0 + y1) + sf * (x1 - x0)
802 ang_0 = atan2(y0 - yc, x0 - xc)
803 ang_1 = atan2(y1 - yc, x1 - xc)
804 ang_arc = ang_1 - ang_0
806 if ang_arc < 0.0 and fs == 1:
807 ang_arc += 2.0 * pi
808 elif ang_arc > 0.0 and fs == 0:
809 ang_arc -= 2.0 * pi
811 n_segs = int(ceil(abs(ang_arc * 2.0 / (pi * 0.5 + 0.001))))
813 if self._spline is None:
814 self._appendPoint(cpx, cpy,
815 handle_left_type='FREE', handle_left=(cpx, cpy),
816 handle_right_type='FREE', handle_right=(cpx, cpy))
818 for i in range(n_segs):
819 ang0 = ang_0 + i * ang_arc / n_segs
820 ang1 = ang_0 + (i + 1) * ang_arc / n_segs
821 ang_demi = 0.25 * (ang1 - ang0)
822 t = 2.66666 * sin(ang_demi) * sin(ang_demi) / sin(ang_demi * 2.0)
823 x1 = xc + cos(ang0) - t * sin(ang0)
824 y1 = yc + sin(ang0) + t * cos(ang0)
825 x2 = xc + cos(ang1)
826 y2 = yc + sin(ang1)
827 x3 = x2 + t * sin(ang1)
828 y3 = y2 - t * cos(ang1)
830 coord1 = ((cos(ang) * rx) * x1 + (-sin(ang) * ry) * y1,
831 (sin(ang) * rx) * x1 + (cos(ang) * ry) * y1)
832 coord2 = ((cos(ang) * rx) * x3 + (-sin(ang) * ry) * y3,
833 (sin(ang) * rx) * x3 + (cos(ang) * ry) * y3)
834 coord3 = ((cos(ang) * rx) * x2 + (-sin(ang) * ry) * y2,
835 (sin(ang) * rx) * x2 + (cos(ang) * ry) * y2)
837 self._updateHandle(handle=coord1, handle_type='FREE')
839 self._appendPoint(coord3[0], coord3[1],
840 handle_left_type='FREE', handle_left=coord2,
841 handle_right_type='FREE', handle_right=coord3)
843 def _pathCurveToA(self, code):
845 Elliptical arc CurveTo path command
848 cur = self._data.cur()
850 while cur is not None and not cur.isalpha():
851 rx = float(self._data.next())
852 ry = float(self._data.next())
853 ang = float(self._data.next()) / 180 * pi
854 fa = float(self._data.next())
855 fs = float(self._data.next())
856 x, y = self._getCoordPair(code.islower(), self._point)
858 self._calcArc(rx, ry, ang, fa, fs, x, y)
860 self._point = (x, y)
861 self._handle = None
862 cur = self._data.cur()
864 def _pathClose(self, code):
866 Close path command
869 if self._spline:
870 self._spline['closed'] = True
872 cv = self._spline['points'][0]
873 self._point = (cv['x'], cv['y'])
875 def parse(self):
877 Execute parser
880 closed = False
882 while not self._data.eof():
883 code = self._data.next()
884 cmd = self._commands.get(code)
886 if cmd is None:
887 raise Exception('Unknown path command: {0}' . format(code))
889 if cmd in {'Z', 'z'}:
890 closed = True
891 else:
892 closed = False
894 cmd(code)
895 if self._use_fill and not closed:
896 self._pathClose('z')
898 def getSplines(self):
900 Get splines definitions
903 return self._splines
906 class SVGGeometry:
908 Abstract SVG geometry
911 __slots__ = ('_node', # XML node for geometry
912 '_context', # Global SVG context (holds matrices stack, i.e.)
913 '_creating') # Flag if geometry is already creating
914 # for this node
915 # need to detect cycles for USE node
917 def __init__(self, node, context):
919 Initialize SVG geometry
922 self._node = node
923 self._context = context
924 self._creating = False
926 if hasattr(node, 'getAttribute'):
927 defs = context['defines']
929 attr_id = node.getAttribute('id')
930 if attr_id and defs.get('#' + attr_id) is None:
931 defs['#' + attr_id] = self
933 className = node.getAttribute('class')
934 if className and defs.get(className) is None:
935 defs[className] = self
937 def _pushRect(self, rect):
939 Push display rectangle
942 self._context['rects'].append(rect)
943 self._context['rect'] = rect
945 def _popRect(self):
947 Pop display rectangle
950 self._context['rects'].pop()
951 self._context['rect'] = self._context['rects'][-1]
953 def _pushMatrix(self, matrix):
955 Push transformation matrix
958 self._context['transform'].append(matrix)
959 self._context['matrix'] = self._context['matrix'] @ matrix
961 def _popMatrix(self):
963 Pop transformation matrix
966 matrix = self._context['transform'].pop()
967 self._context['matrix'] = self._context['matrix'] @ matrix.inverted()
969 def _pushStyle(self, style):
971 Push style
974 self._context['styles'].append(style)
975 self._context['style'] = style
977 def _popStyle(self):
979 Pop style
982 self._context['styles'].pop()
983 self._context['style'] = self._context['styles'][-1]
985 def _transformCoord(self, point):
987 Transform SVG-file coords
990 v = Vector((point[0], point[1], 0.0))
992 return self._context['matrix'] @ v
994 def getNodeMatrix(self):
996 Get transformation matrix of node
999 return SVGMatrixFromNode(self._node, self._context)
1001 def parse(self):
1003 Parse XML node to memory
1006 pass
1008 def _doCreateGeom(self, instancing):
1010 Internal handler to create real geometries
1013 pass
1015 def getTransformMatrix(self):
1017 Get matrix created from "transform" attribute
1020 transform = self._node.getAttribute('transform')
1022 if transform:
1023 return SVGParseTransform(transform)
1025 return None
1027 def createGeom(self, instancing):
1029 Create real geometries
1032 if self._creating:
1033 return
1035 self._creating = True
1037 matrix = self.getTransformMatrix()
1038 if matrix is not None:
1039 self._pushMatrix(matrix)
1041 self._doCreateGeom(instancing)
1043 if matrix is not None:
1044 self._popMatrix()
1046 self._creating = False
1049 class SVGGeometryContainer(SVGGeometry):
1051 Container of SVG geometries
1054 __slots__ = ('_geometries', # List of chold geometries
1055 '_styles') # Styles, used for displaying
1057 def __init__(self, node, context):
1059 Initialize SVG geometry container
1062 super().__init__(node, context)
1064 self._geometries = []
1065 self._styles = SVGEmptyStyles
1067 def parse(self):
1069 Parse XML node to memory
1072 if type(self._node) is xml.dom.minidom.Element:
1073 self._styles = SVGParseStyles(self._node, self._context)
1075 self._pushStyle(self._styles)
1077 for node in self._node.childNodes:
1078 if type(node) is not xml.dom.minidom.Element:
1079 continue
1081 ob = parseAbstractNode(node, self._context)
1082 if ob is not None:
1083 self._geometries.append(ob)
1085 self._popStyle()
1087 def _doCreateGeom(self, instancing):
1089 Create real geometries
1092 for geom in self._geometries:
1093 geom.createGeom(instancing)
1095 def getGeometries(self):
1097 Get list of parsed geometries
1100 return self._geometries
1103 class SVGGeometryPATH(SVGGeometry):
1105 SVG path geometry
1108 __slots__ = ('_splines', # List of splines after parsing
1109 '_styles') # Styles, used for displaying
1111 def __init__(self, node, context):
1113 Initialize SVG path
1116 super().__init__(node, context)
1118 self._splines = []
1119 self._styles = SVGEmptyStyles
1121 def parse(self):
1123 Parse SVG path node
1126 d = self._node.getAttribute('d')
1128 self._styles = SVGParseStyles(self._node, self._context)
1130 pathParser = SVGPathParser(d, self._styles['useFill'])
1131 pathParser.parse()
1133 self._splines = pathParser.getSplines()
1135 def _doCreateGeom(self, instancing):
1137 Create real geometries
1140 ob = SVGCreateCurve(self._context)
1141 cu = ob.data
1143 id_names_from_node(self._node, ob)
1145 if self._styles['useFill']:
1146 cu.dimensions = '2D'
1147 cu.fill_mode = 'BOTH'
1148 cu.materials.append(self._styles['fill'])
1149 else:
1150 cu.dimensions = '3D'
1152 for spline in self._splines:
1153 act_spline = None
1155 if spline['closed'] and len(spline['points']) >= 2:
1156 first = spline['points'][0]
1157 last = spline['points'][-1]
1158 if ( first['handle_left_type'] == 'FREE' and
1159 last['handle_right_type'] == 'VECTOR'):
1160 last['handle_right_type'] = 'FREE'
1161 last['handle_right'] = (last['x'], last['y'])
1162 if ( last['handle_right_type'] == 'FREE' and
1163 first['handle_left_type'] == 'VECTOR'):
1164 first['handle_left_type'] = 'FREE'
1165 first['handle_left'] = (first['x'], first['y'])
1167 for point in spline['points']:
1168 co = self._transformCoord((point['x'], point['y']))
1170 if act_spline is None:
1171 cu.splines.new('BEZIER')
1173 act_spline = cu.splines[-1]
1174 act_spline.use_cyclic_u = spline['closed']
1175 else:
1176 act_spline.bezier_points.add(1)
1178 bezt = act_spline.bezier_points[-1]
1179 bezt.co = co
1181 bezt.handle_left_type = point['handle_left_type']
1182 if point['handle_left'] is not None:
1183 handle = point['handle_left']
1184 bezt.handle_left = self._transformCoord(handle)
1186 bezt.handle_right_type = point['handle_right_type']
1187 if point['handle_right'] is not None:
1188 handle = point['handle_right']
1189 bezt.handle_right = self._transformCoord(handle)
1191 SVGFinishCurve()
1194 class SVGGeometryDEFS(SVGGeometryContainer):
1196 Container for referenced elements
1199 def createGeom(self, instancing):
1201 Create real geometries
1204 pass
1207 class SVGGeometrySYMBOL(SVGGeometryContainer):
1209 Referenced element
1212 def _doCreateGeom(self, instancing):
1214 Create real geometries
1217 self._pushMatrix(self.getNodeMatrix())
1219 super()._doCreateGeom(False)
1221 self._popMatrix()
1223 def createGeom(self, instancing):
1225 Create real geometries
1228 if not instancing:
1229 return
1231 super().createGeom(instancing)
1234 class SVGGeometryG(SVGGeometryContainer):
1236 Geometry group
1239 pass
1242 class SVGGeometryUSE(SVGGeometry):
1244 User of referenced elements
1247 def _doCreateGeom(self, instancing):
1249 Create real geometries
1252 ref = self._node.getAttribute('xlink:href')
1253 geom = self._context['defines'].get(ref)
1255 if geom is not None:
1256 rect = SVGRectFromNode(self._node, self._context)
1257 self._pushRect(rect)
1259 self._pushMatrix(self.getNodeMatrix())
1261 geom.createGeom(True)
1263 self._popMatrix()
1265 self._popRect()
1268 class SVGGeometryRECT(SVGGeometry):
1270 SVG rectangle
1273 __slots__ = ('_rect', # coordinate and dimensions of rectangle
1274 '_radius', # Rounded corner radiuses
1275 '_styles') # Styles, used for displaying
1277 def __init__(self, node, context):
1279 Initialize new rectangle
1282 super().__init__(node, context)
1284 self._rect = ('0', '0', '0', '0')
1285 self._radius = ('0', '0')
1286 self._styles = SVGEmptyStyles
1288 def parse(self):
1290 Parse SVG rectangle node
1293 self._styles = SVGParseStyles(self._node, self._context)
1295 rect = []
1296 for attr in ['x', 'y', 'width', 'height']:
1297 val = self._node.getAttribute(attr)
1298 rect.append(val or '0')
1300 self._rect = (rect)
1302 rx = self._node.getAttribute('rx')
1303 ry = self._node.getAttribute('ry')
1305 self._radius = (rx, ry)
1307 def _appendCorner(self, spline, coord, firstTime, rounded):
1309 Append new corner to rectangle
1312 handle = None
1313 if len(coord) == 3:
1314 handle = self._transformCoord(coord[2])
1315 coord = (coord[0], coord[1])
1317 co = self._transformCoord(coord)
1319 if not firstTime:
1320 spline.bezier_points.add(1)
1322 bezt = spline.bezier_points[-1]
1323 bezt.co = co
1325 if rounded:
1326 if handle:
1327 bezt.handle_left_type = 'VECTOR'
1328 bezt.handle_right_type = 'FREE'
1330 bezt.handle_right = handle
1331 else:
1332 bezt.handle_left_type = 'FREE'
1333 bezt.handle_right_type = 'VECTOR'
1334 bezt.handle_left = co
1336 else:
1337 bezt.handle_left_type = 'VECTOR'
1338 bezt.handle_right_type = 'VECTOR'
1340 def _doCreateGeom(self, instancing):
1342 Create real geometries
1345 # Run-time parsing -- percents would be correct only if
1346 # parsing them now
1347 crect = self._context['rect']
1348 rect = []
1350 for i in range(4):
1351 rect.append(SVGParseCoord(self._rect[i], crect[i % 2]))
1353 r = self._radius
1354 rx = ry = 0.0
1356 if r[0] and r[1]:
1357 rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
1358 ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
1359 elif r[0]:
1360 rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
1361 ry = min(rx, rect[3] / 2)
1362 rx = ry = min(rx, ry)
1363 elif r[1]:
1364 ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
1365 rx = min(ry, rect[2] / 2)
1366 rx = ry = min(rx, ry)
1368 radius = (rx, ry)
1370 # Geometry creation
1371 ob = SVGCreateCurve(self._context)
1372 cu = ob.data
1374 id_names_from_node(self._node, ob)
1376 if self._styles['useFill']:
1377 cu.dimensions = '2D'
1378 cu.fill_mode = 'BOTH'
1379 cu.materials.append(self._styles['fill'])
1380 else:
1381 cu.dimensions = '3D'
1383 cu.splines.new('BEZIER')
1385 spline = cu.splines[-1]
1386 spline.use_cyclic_u = True
1388 x, y = rect[0], rect[1]
1389 w, h = rect[2], rect[3]
1390 rx, ry = radius[0], radius[1]
1391 rounded = False
1393 if rx or ry:
1395 # 0 _______ 1
1396 # / \
1397 # / \
1398 # 7 2
1399 # | |
1400 # | |
1401 # 6 3
1402 # \ /
1403 # \ /
1404 # 5 _______ 4
1407 # Optional third component -- right handle coord
1408 coords = [(x + rx, y),
1409 (x + w - rx, y, (x + w, y)),
1410 (x + w, y + ry),
1411 (x + w, y + h - ry, (x + w, y + h)),
1412 (x + w - rx, y + h),
1413 (x + rx, y + h, (x, y + h)),
1414 (x, y + h - ry),
1415 (x, y + ry, (x, y))]
1417 rounded = True
1418 else:
1419 coords = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
1421 firstTime = True
1422 for coord in coords:
1423 self._appendCorner(spline, coord, firstTime, rounded)
1424 firstTime = False
1426 SVGFinishCurve()
1429 class SVGGeometryELLIPSE(SVGGeometry):
1431 SVG ellipse
1434 __slots__ = ('_cx', # X-coordinate of center
1435 '_cy', # Y-coordinate of center
1436 '_rx', # X-axis radius of circle
1437 '_ry', # Y-axis radius of circle
1438 '_styles') # Styles, used for displaying
1440 def __init__(self, node, context):
1442 Initialize new ellipse
1445 super().__init__(node, context)
1447 self._cx = '0.0'
1448 self._cy = '0.0'
1449 self._rx = '0.0'
1450 self._ry = '0.0'
1451 self._styles = SVGEmptyStyles
1453 def parse(self):
1455 Parse SVG ellipse node
1458 self._styles = SVGParseStyles(self._node, self._context)
1460 self._cx = self._node.getAttribute('cx') or '0'
1461 self._cy = self._node.getAttribute('cy') or '0'
1462 self._rx = self._node.getAttribute('rx') or '0'
1463 self._ry = self._node.getAttribute('ry') or '0'
1465 def _doCreateGeom(self, instancing):
1467 Create real geometries
1470 # Run-time parsing -- percents would be correct only if
1471 # parsing them now
1472 crect = self._context['rect']
1474 cx = SVGParseCoord(self._cx, crect[0])
1475 cy = SVGParseCoord(self._cy, crect[1])
1476 rx = SVGParseCoord(self._rx, crect[0])
1477 ry = SVGParseCoord(self._ry, crect[1])
1479 if not rx or not ry:
1480 # Automaic handles will work incorrect in this case
1481 return
1483 # Create circle
1484 ob = SVGCreateCurve(self._context)
1485 cu = ob.data
1487 id_names_from_node(self._node, ob)
1489 if self._styles['useFill']:
1490 cu.dimensions = '2D'
1491 cu.fill_mode = 'BOTH'
1492 cu.materials.append(self._styles['fill'])
1493 else:
1494 cu.dimensions = '3D'
1496 coords = [((cx - rx, cy),
1497 (cx - rx, cy + ry * 0.552),
1498 (cx - rx, cy - ry * 0.552)),
1500 ((cx, cy - ry),
1501 (cx - rx * 0.552, cy - ry),
1502 (cx + rx * 0.552, cy - ry)),
1504 ((cx + rx, cy),
1505 (cx + rx, cy - ry * 0.552),
1506 (cx + rx, cy + ry * 0.552)),
1508 ((cx, cy + ry),
1509 (cx + rx * 0.552, cy + ry),
1510 (cx - rx * 0.552, cy + ry))]
1512 spline = None
1513 for coord in coords:
1514 co = self._transformCoord(coord[0])
1515 handle_left = self._transformCoord(coord[1])
1516 handle_right = self._transformCoord(coord[2])
1518 if spline is None:
1519 cu.splines.new('BEZIER')
1520 spline = cu.splines[-1]
1521 spline.use_cyclic_u = True
1522 else:
1523 spline.bezier_points.add(1)
1525 bezt = spline.bezier_points[-1]
1526 bezt.co = co
1527 bezt.handle_left_type = 'FREE'
1528 bezt.handle_right_type = 'FREE'
1529 bezt.handle_left = handle_left
1530 bezt.handle_right = handle_right
1532 SVGFinishCurve()
1535 class SVGGeometryCIRCLE(SVGGeometryELLIPSE):
1537 SVG circle
1540 def parse(self):
1542 Parse SVG circle node
1545 self._styles = SVGParseStyles(self._node, self._context)
1547 self._cx = self._node.getAttribute('cx') or '0'
1548 self._cy = self._node.getAttribute('cy') or '0'
1550 r = self._node.getAttribute('r') or '0'
1551 self._rx = self._ry = r
1554 class SVGGeometryLINE(SVGGeometry):
1556 SVG line
1559 __slots__ = ('_x1', # X-coordinate of beginning
1560 '_y1', # Y-coordinate of beginning
1561 '_x2', # X-coordinate of ending
1562 '_y2') # Y-coordinate of ending
1564 def __init__(self, node, context):
1566 Initialize new line
1569 super().__init__(node, context)
1571 self._x1 = '0.0'
1572 self._y1 = '0.0'
1573 self._x2 = '0.0'
1574 self._y2 = '0.0'
1576 def parse(self):
1578 Parse SVG line node
1581 self._x1 = self._node.getAttribute('x1') or '0'
1582 self._y1 = self._node.getAttribute('y1') or '0'
1583 self._x2 = self._node.getAttribute('x2') or '0'
1584 self._y2 = self._node.getAttribute('y2') or '0'
1586 def _doCreateGeom(self, instancing):
1588 Create real geometries
1591 # Run-time parsing -- percents would be correct only if
1592 # parsing them now
1593 crect = self._context['rect']
1595 x1 = SVGParseCoord(self._x1, crect[0])
1596 y1 = SVGParseCoord(self._y1, crect[1])
1597 x2 = SVGParseCoord(self._x2, crect[0])
1598 y2 = SVGParseCoord(self._y2, crect[1])
1600 # Create cline
1601 ob = SVGCreateCurve(self._context)
1602 cu = ob.data
1604 id_names_from_node(self._node, ob)
1606 coords = [(x1, y1), (x2, y2)]
1607 spline = None
1609 for coord in coords:
1610 co = self._transformCoord(coord)
1612 if spline is None:
1613 cu.splines.new('BEZIER')
1614 spline = cu.splines[-1]
1615 spline.use_cyclic_u = True
1616 else:
1617 spline.bezier_points.add(1)
1619 bezt = spline.bezier_points[-1]
1620 bezt.co = co
1621 bezt.handle_left_type = 'VECTOR'
1622 bezt.handle_right_type = 'VECTOR'
1624 SVGFinishCurve()
1627 class SVGGeometryPOLY(SVGGeometry):
1629 Abstract class for handling poly-geometries
1630 (polylines and polygons)
1633 __slots__ = ('_points', # Array of points for poly geometry
1634 '_styles', # Styles, used for displaying
1635 '_closed') # Should generated curve be closed?
1637 def __init__(self, node, context):
1639 Initialize new poly geometry
1642 super().__init__(node, context)
1644 self._points = []
1645 self._styles = SVGEmptyStyles
1646 self._closed = False
1648 def parse(self):
1650 Parse poly node
1653 self._styles = SVGParseStyles(self._node, self._context)
1655 points = parse_array_of_floats(self._node.getAttribute('points'))
1657 prev = None
1658 self._points = []
1660 for p in points:
1661 if prev is None:
1662 prev = p
1663 else:
1664 self._points.append((prev, p))
1665 prev = None
1667 def _doCreateGeom(self, instancing):
1669 Create real geometries
1672 ob = SVGCreateCurve(self._context)
1673 cu = ob.data
1675 id_names_from_node(self._node, ob)
1677 if self._closed and self._styles['useFill']:
1678 cu.dimensions = '2D'
1679 cu.fill_mode = 'BOTH'
1680 cu.materials.append(self._styles['fill'])
1681 else:
1682 cu.dimensions = '3D'
1684 spline = None
1686 for point in self._points:
1687 co = self._transformCoord(point)
1689 if spline is None:
1690 cu.splines.new('BEZIER')
1691 spline = cu.splines[-1]
1692 spline.use_cyclic_u = self._closed
1693 else:
1694 spline.bezier_points.add(1)
1696 bezt = spline.bezier_points[-1]
1697 bezt.co = co
1698 bezt.handle_left_type = 'VECTOR'
1699 bezt.handle_right_type = 'VECTOR'
1701 SVGFinishCurve()
1704 class SVGGeometryPOLYLINE(SVGGeometryPOLY):
1706 SVG polyline geometry
1709 pass
1712 class SVGGeometryPOLYGON(SVGGeometryPOLY):
1714 SVG polygon geometry
1717 def __init__(self, node, context):
1719 Initialize new polygon geometry
1722 super().__init__(node, context)
1724 self._closed = True
1727 class SVGGeometrySVG(SVGGeometryContainer):
1729 Main geometry holder
1732 def _doCreateGeom(self, instancing):
1734 Create real geometries
1737 rect = SVGRectFromNode(self._node, self._context)
1739 matrix = self.getNodeMatrix()
1741 # Better SVG compatibility: match svg-document units
1742 # with blender units
1744 viewbox = []
1745 unit = ''
1747 if self._node.getAttribute('height'):
1748 raw_height = self._node.getAttribute('height')
1749 token, last_char = read_float(raw_height)
1750 document_height = float(token)
1751 unit = raw_height[last_char:].strip()
1753 if self._node.getAttribute('viewBox'):
1754 viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
1756 if len(viewbox) == 4 and unit in ('cm', 'mm', 'in', 'pt', 'pc'):
1758 #convert units to BU:
1759 unitscale = units[unit] / 90 * 1000 / 39.3701
1761 #apply blender unit scale:
1762 unitscale = unitscale / bpy.context.scene.unit_settings.scale_length
1764 matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((1.0, 0.0, 0.0)))
1765 matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((0.0, 1.0, 0.0)))
1767 # match document origin with 3D space origin.
1768 if self._node.getAttribute('viewBox'):
1769 viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
1770 matrix = matrix @ matrix.Translation([0.0, - viewbox[1] - viewbox[3], 0.0])
1772 self._pushMatrix(matrix)
1773 self._pushRect(rect)
1775 super()._doCreateGeom(False)
1777 self._popRect()
1778 self._popMatrix()
1781 class SVGLoader(SVGGeometryContainer):
1783 SVG file loader
1786 def getTransformMatrix(self):
1788 Get matrix created from "transform" attribute
1791 # SVG document doesn't support transform specification
1792 # it can't even hold attributes
1794 return None
1796 def __init__(self, context, filepath, do_colormanage):
1798 Initialize SVG loader
1800 import os
1802 svg_name = os.path.basename(filepath)
1803 scene = context.scene
1804 collection = bpy.data.collections.new(name=svg_name)
1805 scene.collection.children.link(collection)
1807 node = xml.dom.minidom.parse(filepath)
1809 m = Matrix()
1810 m = m @ Matrix.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
1811 m = m @ Matrix.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
1813 rect = (0, 0)
1815 self._context = {'defines': {},
1816 'transform': [],
1817 'rects': [rect],
1818 'rect': rect,
1819 'matrix': m,
1820 'materials': {},
1821 'styles': [None],
1822 'style': None,
1823 'do_colormanage': do_colormanage,
1824 'collection': collection}
1826 super().__init__(node, self._context)
1829 svgGeometryClasses = {
1830 'svg': SVGGeometrySVG,
1831 'path': SVGGeometryPATH,
1832 'defs': SVGGeometryDEFS,
1833 'symbol': SVGGeometrySYMBOL,
1834 'use': SVGGeometryUSE,
1835 'rect': SVGGeometryRECT,
1836 'ellipse': SVGGeometryELLIPSE,
1837 'circle': SVGGeometryCIRCLE,
1838 'line': SVGGeometryLINE,
1839 'polyline': SVGGeometryPOLYLINE,
1840 'polygon': SVGGeometryPOLYGON,
1841 'g': SVGGeometryG}
1844 def parseAbstractNode(node, context):
1845 name = node.tagName.lower()
1847 if name.startswith('svg:'):
1848 name = name[4:]
1850 geomClass = svgGeometryClasses.get(name)
1852 if geomClass is not None:
1853 ob = geomClass(node, context)
1854 ob.parse()
1856 return ob
1858 return None
1861 def load_svg(context, filepath, do_colormanage):
1863 Load specified SVG file
1866 if bpy.ops.object.mode_set.poll():
1867 bpy.ops.object.mode_set(mode='OBJECT')
1869 loader = SVGLoader(context, filepath, do_colormanage)
1870 loader.parse()
1871 loader.createGeom(False)
1874 def load(operator, context, filepath=""):
1876 # error in code should raise exceptions but loading
1877 # non SVG files can give useful messages.
1878 do_colormanage = context.scene.display_settings.display_device != 'NONE'
1879 try:
1880 load_svg(context, filepath, do_colormanage)
1881 except (xml.parsers.expat.ExpatError, UnicodeEncodeError) as e:
1882 import traceback
1883 traceback.print_exc()
1885 operator.report({'WARNING'}, "Unable to parse XML, %s:%s for file %r" % (type(e).__name__, e, filepath))
1886 return {'CANCELLED'}
1888 return {'FINISHED'}