Sun position: remove unused prop in HDRI mode
[blender-addons.git] / io_curve_svg / import_svg.py
blobceb2442074b85c8c032968a527012f49389c25f3
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 import re
22 import xml.dom.minidom
23 from math import cos, sin, tan, atan2, pi, ceil
25 import bpy
26 from mathutils import Vector, Matrix
28 from . import svg_colors
29 from .svg_util import (units,
30 srgb_to_linearrgb,
31 check_points_equal,
32 parse_array_of_floats,
33 read_float)
35 #### Common utilities ####
37 SVGEmptyStyles = {'useFill': None,
38 'fill': None}
41 def SVGCreateCurve(context):
42 """
43 Create new curve object to hold splines in
44 """
46 cu = bpy.data.curves.new("Curve", 'CURVE')
47 obj = bpy.data.objects.new("Curve", cu)
49 context['collection'].objects.link(obj)
51 return obj
54 def SVGFinishCurve():
55 """
56 Finish curve creation
57 """
59 pass
62 def SVGFlipHandle(x, y, x1, y1):
63 """
64 Flip handle around base point
65 """
67 x = x + (x - x1)
68 y = y + (y - y1)
70 return x, y
73 def SVGParseCoord(coord, size):
74 """
75 Parse coordinate component to common basis
77 Needed to handle coordinates set in cm, mm, inches.
78 """
80 token, last_char = read_float(coord)
81 val = float(token)
82 unit = coord[last_char:].strip() # strip() in case there is a space
84 if unit == '%':
85 return float(size) / 100.0 * val
86 else:
87 return val * units[unit]
89 return val
92 def SVGRectFromNode(node, context):
93 """
94 Get display rectangle from node
95 """
97 w = context['rect'][0]
98 h = context['rect'][1]
100 if node.getAttribute('viewBox'):
101 viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
102 w = SVGParseCoord(viewBox[2], w)
103 h = SVGParseCoord(viewBox[3], h)
104 else:
105 if node.getAttribute('width'):
106 w = SVGParseCoord(node.getAttribute('width'), w)
108 if node.getAttribute('height'):
109 h = SVGParseCoord(node.getAttribute('height'), h)
111 return (w, h)
114 def SVGMatrixFromNode(node, context):
116 Get transformation matrix from given node
119 tagName = node.tagName.lower()
120 tags = ['svg:svg', 'svg:use', 'svg:symbol']
122 if tagName not in tags and 'svg:' + tagName not in tags:
123 return Matrix()
125 rect = context['rect']
126 has_user_coordinate = (len(context['rects']) > 1)
128 m = Matrix()
129 x = SVGParseCoord(node.getAttribute('x') or '0', rect[0])
130 y = SVGParseCoord(node.getAttribute('y') or '0', rect[1])
131 w = SVGParseCoord(node.getAttribute('width') or str(rect[0]), rect[0])
132 h = SVGParseCoord(node.getAttribute('height') or str(rect[1]), rect[1])
134 m = Matrix.Translation(Vector((x, y, 0.0)))
135 if has_user_coordinate:
136 if rect[0] != 0 and rect[1] != 0:
137 m = m @ Matrix.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0)))
138 m = m @ Matrix.Scale(h / rect[1], 4, Vector((0.0, 1.0, 0.0)))
140 if node.getAttribute('viewBox'):
141 viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
142 vx = SVGParseCoord(viewBox[0], w)
143 vy = SVGParseCoord(viewBox[1], h)
144 vw = SVGParseCoord(viewBox[2], w)
145 vh = SVGParseCoord(viewBox[3], h)
147 if vw == 0 or vh == 0:
148 return m
150 if has_user_coordinate or (w != 0 and h != 0):
151 sx = w / vw
152 sy = h / vh
153 scale = min(sx, sy)
154 else:
155 scale = 1.0
156 w = vw
157 h = vh
159 tx = (w - vw * scale) / 2
160 ty = (h - vh * scale) / 2
161 m = m @ Matrix.Translation(Vector((tx, ty, 0.0)))
163 m = m @ Matrix.Translation(Vector((-vx, -vy, 0.0)))
164 m = m @ Matrix.Scale(scale, 4, Vector((1.0, 0.0, 0.0)))
165 m = m @ Matrix.Scale(scale, 4, Vector((0.0, 1.0, 0.0)))
167 return m
170 def SVGParseTransform(transform):
172 Parse transform string and return transformation matrix
175 m = Matrix()
176 r = re.compile('\s*([A-z]+)\s*\((.*?)\)')
178 for match in r.finditer(transform):
179 func = match.group(1)
180 params = match.group(2)
181 params = params.replace(',', ' ').split()
183 proc = SVGTransforms.get(func)
184 if proc is None:
185 raise Exception('Unknown trasnform function: ' + func)
187 m = m @ proc(params)
189 return m
192 def SVGGetMaterial(color, context):
194 Get material for specified color
197 materials = context['materials']
198 rgb_re = re.compile('^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
200 if color in materials:
201 return materials[color]
203 diff = None
204 if color.startswith('#'):
205 color = color[1:]
207 if len(color) == 3:
208 color = color[0] * 2 + color[1] * 2 + color[2] * 2
210 diff = (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
211 elif color in svg_colors.SVGColors:
212 diff = svg_colors.SVGColors[color]
213 elif rgb_re.match(color):
214 c = rgb_re.findall(color)[0]
215 diff = (float(c[0]), float(c[1]), float(c[2]))
216 else:
217 return None
219 diffuse_color = ([x / 255.0 for x in diff])
221 if context['do_colormanage']:
222 diffuse_color[0] = srgb_to_linearrgb(diffuse_color[0])
223 diffuse_color[1] = srgb_to_linearrgb(diffuse_color[1])
224 diffuse_color[2] = srgb_to_linearrgb(diffuse_color[2])
226 mat = bpy.data.materials.new(name='SVGMat')
227 mat.diffuse_color = (*diffuse_color, 1.0)
229 materials[color] = mat
231 return mat
234 def SVGTransformTranslate(params):
236 translate SVG transform command
239 tx = float(params[0])
240 ty = float(params[1]) if len(params) > 1 else 0.0
242 return Matrix.Translation(Vector((tx, ty, 0.0)))
245 def SVGTransformMatrix(params):
247 matrix SVG transform command
250 a = float(params[0])
251 b = float(params[1])
252 c = float(params[2])
253 d = float(params[3])
254 e = float(params[4])
255 f = float(params[5])
257 return Matrix(((a, c, 0.0, e),
258 (b, d, 0.0, f),
259 (0, 0, 1.0, 0),
260 (0, 0, 0.0, 1)))
263 def SVGTransformScale(params):
265 scale SVG transform command
268 sx = float(params[0])
269 sy = float(params[1]) if len(params) > 1 else sx
271 m = Matrix()
273 m = m @ Matrix.Scale(sx, 4, Vector((1.0, 0.0, 0.0)))
274 m = m @ Matrix.Scale(sy, 4, Vector((0.0, 1.0, 0.0)))
276 return m
279 def SVGTransformSkewX(params):
281 skewX SVG transform command
284 ang = float(params[0]) * pi / 180.0
286 return Matrix(((1.0, 0.0, 0.0),
287 (tan(ang), 1.0, 0.0),
288 (0.0, 0.0, 1.0))).to_4x4()
291 def SVGTransformSkewY(params):
293 skewX SVG transform command
296 ang = float(params[0]) * pi / 180.0
298 return Matrix(((1.0, tan(ang), 0.0),
299 (0.0, 1.0, 0.0),
300 (0.0, 0.0, 1.0))).to_4x4()
303 def SVGTransformRotate(params):
305 skewX SVG transform command
308 ang = float(params[0]) * pi / 180.0
309 cx = cy = 0.0
311 if len(params) >= 3:
312 cx = float(params[1])
313 cy = float(params[2])
315 tm = Matrix.Translation(Vector((cx, cy, 0.0)))
316 rm = Matrix.Rotation(ang, 4, Vector((0.0, 0.0, 1.0)))
318 return tm @ rm @ tm.inverted()
320 SVGTransforms = {'translate': SVGTransformTranslate,
321 'scale': SVGTransformScale,
322 'skewX': SVGTransformSkewX,
323 'skewY': SVGTransformSkewY,
324 'matrix': SVGTransformMatrix,
325 'rotate': SVGTransformRotate}
328 def SVGParseStyles(node, context):
330 Parse node to get different styles for displaying geometries
331 (materials, filling flags, etc..)
334 styles = SVGEmptyStyles.copy()
336 style = node.getAttribute('style')
337 if style:
338 elems = style.split(';')
339 for elem in elems:
340 s = elem.split(':')
342 if len(s) != 2:
343 continue
345 name = s[0].strip().lower()
346 val = s[1].strip()
348 if name == 'fill':
349 val = val.lower()
350 if val == 'none':
351 styles['useFill'] = False
352 else:
353 styles['useFill'] = True
354 styles['fill'] = SVGGetMaterial(val, context)
356 if styles['useFill'] is None:
357 styles['useFill'] = True
358 styles['fill'] = SVGGetMaterial('#000', context)
360 return styles
362 if styles['useFill'] is None:
363 fill = node.getAttribute('fill')
364 if fill:
365 fill = fill.lower()
366 if fill == 'none':
367 styles['useFill'] = False
368 else:
369 styles['useFill'] = True
370 styles['fill'] = SVGGetMaterial(fill, context)
372 if styles['useFill'] is None and context['style']:
373 styles = context['style'].copy()
375 if styles['useFill'] is None:
376 styles['useFill'] = True
377 styles['fill'] = SVGGetMaterial('#000', context)
379 return styles
381 def id_names_from_node(node, ob):
382 if node.getAttribute('id'):
383 name = node.getAttribute('id')
384 ob.name = name
385 ob.data.name = name
387 #### SVG path helpers ####
390 class SVGPathData:
392 SVG Path data token supplier
395 __slots__ = ('_data', # List of tokens
396 '_index', # Index of current token in tokens list
397 '_len') # Length of tokens list
399 def __init__(self, d):
401 Initialize new path data supplier
403 d - the definition of the outline of a shape
406 spaces = ' ,\t'
407 commands = {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
408 tokens = []
410 i = 0
411 n = len(d)
412 while i < n:
413 c = d[i]
415 if c in spaces:
416 pass
417 elif c.lower() in commands:
418 tokens.append(c)
419 elif c in ['-', '.'] or c.isdigit():
420 token, last_char = read_float(d, i)
421 tokens.append(token)
423 # in most cases len(token) and (last_char - i) are the same
424 # but with whitespace or ',' prefix they are not.
426 i += (last_char - i) - 1
428 i += 1
430 self._data = tokens
431 self._index = 0
432 self._len = len(tokens)
434 def eof(self):
436 Check if end of data reached
439 return self._index >= self._len
441 def cur(self):
443 Return current token
446 if self.eof():
447 return None
449 return self._data[self._index]
451 def lookupNext(self):
453 get next token without moving pointer
456 if self.eof():
457 return None
459 return self._data[self._index]
461 def next(self):
463 Return current token and go to next one
466 if self.eof():
467 return None
469 token = self._data[self._index]
470 self._index += 1
472 return token
474 def nextCoord(self):
476 Return coordinate created from current token and move to next token
479 token = self.next()
481 if token is None:
482 return None
484 return float(token)
487 class SVGPathParser:
489 Parser of SVG path data
492 __slots__ = ('_data', # Path data supplird
493 '_point', # Current point coorfinate
494 '_handle', # Last handle coordinate
495 '_splines', # List of all splies created during parsing
496 '_spline', # Currently handling spline
497 '_commands', # Hash of all supported path commands
498 '_use_fill', # Splines would be filled, so expected to be closed
501 def __init__(self, d, use_fill):
503 Initialize path parser
505 d - the definition of the outline of a shape
508 self._data = SVGPathData(d)
509 self._point = None # Current point
510 self._handle = None # Last handle
511 self._splines = [] # List of splines in path
512 self._spline = None # Current spline
513 self._use_fill = use_fill
515 self._commands = {'M': self._pathMoveTo,
516 'L': self._pathLineTo,
517 'H': self._pathLineTo,
518 'V': self._pathLineTo,
519 'C': self._pathCurveToCS,
520 'S': self._pathCurveToCS,
521 'Q': self._pathCurveToQT,
522 'T': self._pathCurveToQT,
523 'A': self._pathCurveToA,
524 'Z': self._pathClose,
526 'm': self._pathMoveTo,
527 'l': self._pathLineTo,
528 'h': self._pathLineTo,
529 'v': self._pathLineTo,
530 'c': self._pathCurveToCS,
531 's': self._pathCurveToCS,
532 'q': self._pathCurveToQT,
533 't': self._pathCurveToQT,
534 'a': self._pathCurveToA,
535 'z': self._pathClose}
537 def _getCoordPair(self, relative, point):
539 Get next coordinate pair
542 x = self._data.nextCoord()
543 y = self._data.nextCoord()
545 if relative and point is not None:
546 x += point[0]
547 y += point[1]
549 return x, y
551 def _appendPoint(self, x, y, handle_left=None, handle_left_type='VECTOR',
552 handle_right=None, handle_right_type='VECTOR'):
554 Append point to spline
556 If there's no active spline, create one and set it's first point
557 to current point coordinate
560 if self._spline is None:
561 self._spline = {'points': [],
562 'closed': False}
564 self._splines.append(self._spline)
566 if len(self._spline['points']) > 0:
567 # Not sure about specifications, but Illustrator could create
568 # last point at the same position, as start point (which was
569 # reached by MoveTo command) to set needed handle coords.
570 # It's also could use last point at last position to make path
571 # filled.
573 first = self._spline['points'][0]
574 if check_points_equal((first['x'], first['y']), (x, y)):
575 if handle_left is not None:
576 first['handle_left'] = handle_left
577 first['handle_left_type'] = 'FREE'
579 if handle_left_type != 'VECTOR':
580 first['handle_left_type'] = handle_left_type
582 if self._data.eof() or self._data.lookupNext().lower() == 'm':
583 self._spline['closed'] = True
585 return
587 last = self._spline['points'][-1]
588 if last['handle_right_type'] == 'VECTOR' and handle_left_type == 'FREE':
589 last['handle_right'] = (last['x'], last['y'])
590 last['handle_right_type'] = 'FREE'
591 if last['handle_right_type'] == 'FREE' and handle_left_type == 'VECTOR':
592 handle_left = (x, y)
593 handle_left_type = 'FREE'
595 point = {'x': x,
596 'y': y,
598 'handle_left': handle_left,
599 'handle_left_type': handle_left_type,
601 'handle_right': handle_right,
602 'handle_right_type': handle_right_type}
604 self._spline['points'].append(point)
606 def _updateHandle(self, handle=None, handle_type=None):
608 Update right handle of previous point when adding new point to spline
611 point = self._spline['points'][-1]
613 if handle_type is not None:
614 point['handle_right_type'] = handle_type
616 if handle is not None:
617 point['handle_right'] = handle
619 def _pathMoveTo(self, code):
621 MoveTo path command
624 relative = code.islower()
625 x, y = self._getCoordPair(relative, self._point)
627 self._spline = None # Flag to start new spline
628 self._point = (x, y)
630 cur = self._data.cur()
631 while cur is not None and not cur.isalpha():
632 x, y = self._getCoordPair(relative, self._point)
634 if self._spline is None:
635 self._appendPoint(self._point[0], self._point[1])
637 self._appendPoint(x, y)
639 self._point = (x, y)
640 cur = self._data.cur()
642 self._handle = None
644 def _pathLineTo(self, code):
646 LineTo path command
649 c = code.lower()
651 cur = self._data.cur()
652 while cur is not None and not cur.isalpha():
653 if c == 'l':
654 x, y = self._getCoordPair(code == 'l', self._point)
655 elif c == 'h':
656 x = self._data.nextCoord()
657 y = self._point[1]
658 else:
659 x = self._point[0]
660 y = self._data.nextCoord()
662 if code == 'h':
663 x += self._point[0]
664 elif code == 'v':
665 y += self._point[1]
667 if self._spline is None:
668 self._appendPoint(self._point[0], self._point[1])
670 self._appendPoint(x, y)
672 self._point = (x, y)
673 cur = self._data.cur()
675 self._handle = None
677 def _pathCurveToCS(self, code):
679 Cubic BEZIER CurveTo path command
682 c = code.lower()
683 cur = self._data.cur()
684 while cur is not None and not cur.isalpha():
685 if c == 'c':
686 x1, y1 = self._getCoordPair(code.islower(), self._point)
687 x2, y2 = self._getCoordPair(code.islower(), self._point)
688 else:
689 if self._handle is not None:
690 x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
691 self._handle[0], self._handle[1])
692 else:
693 x1, y1 = self._point
695 x2, y2 = self._getCoordPair(code.islower(), self._point)
697 x, y = self._getCoordPair(code.islower(), self._point)
699 if self._spline is None:
700 self._appendPoint(self._point[0], self._point[1],
701 handle_left_type='FREE', handle_left=self._point,
702 handle_right_type='FREE', handle_right=(x1, y1))
703 else:
704 self._updateHandle(handle=(x1, y1), handle_type='FREE')
706 self._appendPoint(x, y,
707 handle_left_type='FREE', handle_left=(x2, y2),
708 handle_right_type='FREE', handle_right=(x, y))
710 self._point = (x, y)
711 self._handle = (x2, y2)
712 cur = self._data.cur()
714 def _pathCurveToQT(self, code):
716 Quadratic BEZIER CurveTo path command
719 c = code.lower()
720 cur = self._data.cur()
722 while cur is not None and not cur.isalpha():
723 if c == 'q':
724 x1, y1 = self._getCoordPair(code.islower(), self._point)
725 else:
726 if self._handle is not None:
727 x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
728 self._handle[0], self._handle[1])
729 else:
730 x1, y1 = self._point
732 x, y = self._getCoordPair(code.islower(), self._point)
734 if not check_points_equal((x, y), self._point):
735 if self._spline is None:
736 self._appendPoint(self._point[0], self._point[1],
737 handle_left_type='FREE', handle_left=self._point,
738 handle_right_type='FREE', handle_right=self._point)
740 self._appendPoint(x, y,
741 handle_left_type='FREE', handle_left=(x1, y1),
742 handle_right_type='FREE', handle_right=(x, y))
744 self._point = (x, y)
745 self._handle = (x1, y1)
746 cur = self._data.cur()
748 def _calcArc(self, rx, ry, ang, fa, fs, x, y):
750 Calc arc paths
752 Copied and adoptedfrom paths_svg2obj.py script for Blender 2.49
753 which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
756 cpx = self._point[0]
757 cpy = self._point[1]
758 rx = abs(rx)
759 ry = abs(ry)
760 px = abs((cos(ang) * (cpx - x) + sin(ang) * (cpy - y)) * 0.5) ** 2.0
761 py = abs((cos(ang) * (cpy - y) - sin(ang) * (cpx - x)) * 0.5) ** 2.0
762 rpx = rpy = 0.0
764 if abs(rx) > 0.0:
765 px = px / (rx ** 2.0)
767 if abs(ry) > 0.0:
768 rpy = py / (ry ** 2.0)
770 pl = rpx + rpy
771 if pl > 1.0:
772 pl = pl ** 0.5
773 rx *= pl
774 ry *= pl
776 carx = sarx = cary = sary = 0.0
778 if abs(rx) > 0.0:
779 carx = cos(ang) / rx
780 sarx = sin(ang) / rx
782 if abs(ry) > 0.0:
783 cary = cos(ang) / ry
784 sary = sin(ang) / ry
786 x0 = carx * cpx + sarx * cpy
787 y0 = -sary * cpx + cary * cpy
788 x1 = carx * x + sarx * y
789 y1 = -sary * x + cary * y
790 d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
792 if abs(d) > 0.0:
793 sq = 1.0 / d - 0.25
794 else:
795 sq = -0.25
797 if sq < 0.0:
798 sq = 0.0
800 sf = sq ** 0.5
801 if fs == fa:
802 sf = -sf
804 xc = 0.5 * (x0 + x1) - sf * (y1 - y0)
805 yc = 0.5 * (y0 + y1) + sf * (x1 - x0)
806 ang_0 = atan2(y0 - yc, x0 - xc)
807 ang_1 = atan2(y1 - yc, x1 - xc)
808 ang_arc = ang_1 - ang_0
810 if ang_arc < 0.0 and fs == 1:
811 ang_arc += 2.0 * pi
812 elif ang_arc > 0.0 and fs == 0:
813 ang_arc -= 2.0 * pi
815 n_segs = int(ceil(abs(ang_arc * 2.0 / (pi * 0.5 + 0.001))))
817 if self._spline is None:
818 self._appendPoint(cpx, cpy,
819 handle_left_type='FREE', handle_left=(cpx, cpy),
820 handle_right_type='FREE', handle_right=(cpx, cpy))
822 for i in range(n_segs):
823 ang0 = ang_0 + i * ang_arc / n_segs
824 ang1 = ang_0 + (i + 1) * ang_arc / n_segs
825 ang_demi = 0.25 * (ang1 - ang0)
826 t = 2.66666 * sin(ang_demi) * sin(ang_demi) / sin(ang_demi * 2.0)
827 x1 = xc + cos(ang0) - t * sin(ang0)
828 y1 = yc + sin(ang0) + t * cos(ang0)
829 x2 = xc + cos(ang1)
830 y2 = yc + sin(ang1)
831 x3 = x2 + t * sin(ang1)
832 y3 = y2 - t * cos(ang1)
834 coord1 = ((cos(ang) * rx) * x1 + (-sin(ang) * ry) * y1,
835 (sin(ang) * rx) * x1 + (cos(ang) * ry) * y1)
836 coord2 = ((cos(ang) * rx) * x3 + (-sin(ang) * ry) * y3,
837 (sin(ang) * rx) * x3 + (cos(ang) * ry) * y3)
838 coord3 = ((cos(ang) * rx) * x2 + (-sin(ang) * ry) * y2,
839 (sin(ang) * rx) * x2 + (cos(ang) * ry) * y2)
841 self._updateHandle(handle=coord1, handle_type='FREE')
843 self._appendPoint(coord3[0], coord3[1],
844 handle_left_type='FREE', handle_left=coord2,
845 handle_right_type='FREE', handle_right=coord3)
847 def _pathCurveToA(self, code):
849 Elliptical arc CurveTo path command
852 cur = self._data.cur()
854 while cur is not None and not cur.isalpha():
855 rx = float(self._data.next())
856 ry = float(self._data.next())
857 ang = float(self._data.next()) / 180 * pi
858 fa = float(self._data.next())
859 fs = float(self._data.next())
860 x, y = self._getCoordPair(code.islower(), self._point)
862 self._calcArc(rx, ry, ang, fa, fs, x, y)
864 self._point = (x, y)
865 self._handle = None
866 cur = self._data.cur()
868 def _pathClose(self, code):
870 Close path command
873 if self._spline:
874 self._spline['closed'] = True
876 cv = self._spline['points'][0]
877 self._point = (cv['x'], cv['y'])
879 def parse(self):
881 Execute parser
884 closed = False
886 while not self._data.eof():
887 code = self._data.next()
888 cmd = self._commands.get(code)
890 if cmd is None:
891 raise Exception('Unknown path command: {0}' . format(code))
893 if cmd in {'Z', 'z'}:
894 closed = True
895 else:
896 closed = False
898 cmd(code)
899 if self._use_fill and not closed:
900 self._pathClose('z')
902 def getSplines(self):
904 Get splines definitions
907 return self._splines
910 class SVGGeometry:
912 Abstract SVG geometry
915 __slots__ = ('_node', # XML node for geometry
916 '_context', # Global SVG context (holds matrices stack, i.e.)
917 '_creating') # Flag if geometry is already creating
918 # for this node
919 # need to detect cycles for USE node
921 def __init__(self, node, context):
923 Initialize SVG geometry
926 self._node = node
927 self._context = context
928 self._creating = False
930 if hasattr(node, 'getAttribute'):
931 defs = context['defines']
933 attr_id = node.getAttribute('id')
934 if attr_id and defs.get('#' + attr_id) is None:
935 defs['#' + attr_id] = self
937 className = node.getAttribute('class')
938 if className and defs.get(className) is None:
939 defs[className] = self
941 def _pushRect(self, rect):
943 Push display rectangle
946 self._context['rects'].append(rect)
947 self._context['rect'] = rect
949 def _popRect(self):
951 Pop display rectangle
954 self._context['rects'].pop()
955 self._context['rect'] = self._context['rects'][-1]
957 def _pushMatrix(self, matrix):
959 Push transformation matrix
962 self._context['transform'].append(matrix)
963 self._context['matrix'] = self._context['matrix'] @ matrix
965 def _popMatrix(self):
967 Pop transformation matrix
970 matrix = self._context['transform'].pop()
971 self._context['matrix'] = self._context['matrix'] @ matrix.inverted()
973 def _pushStyle(self, style):
975 Push style
978 self._context['styles'].append(style)
979 self._context['style'] = style
981 def _popStyle(self):
983 Pop style
986 self._context['styles'].pop()
987 self._context['style'] = self._context['styles'][-1]
989 def _transformCoord(self, point):
991 Transform SVG-file coords
994 v = Vector((point[0], point[1], 0.0))
996 return self._context['matrix'] @ v
998 def getNodeMatrix(self):
1000 Get transformation matrix of node
1003 return SVGMatrixFromNode(self._node, self._context)
1005 def parse(self):
1007 Parse XML node to memory
1010 pass
1012 def _doCreateGeom(self, instancing):
1014 Internal handler to create real geometries
1017 pass
1019 def getTransformMatrix(self):
1021 Get matrix created from "transform" attribute
1024 transform = self._node.getAttribute('transform')
1026 if transform:
1027 return SVGParseTransform(transform)
1029 return None
1031 def createGeom(self, instancing):
1033 Create real geometries
1036 if self._creating:
1037 return
1039 self._creating = True
1041 matrix = self.getTransformMatrix()
1042 if matrix is not None:
1043 self._pushMatrix(matrix)
1045 self._doCreateGeom(instancing)
1047 if matrix is not None:
1048 self._popMatrix()
1050 self._creating = False
1053 class SVGGeometryContainer(SVGGeometry):
1055 Container of SVG geometries
1058 __slots__ = ('_geometries', # List of chold geometries
1059 '_styles') # Styles, used for displaying
1061 def __init__(self, node, context):
1063 Initialize SVG geometry container
1066 super().__init__(node, context)
1068 self._geometries = []
1069 self._styles = SVGEmptyStyles
1071 def parse(self):
1073 Parse XML node to memory
1076 if type(self._node) is xml.dom.minidom.Element:
1077 self._styles = SVGParseStyles(self._node, self._context)
1079 self._pushStyle(self._styles)
1081 for node in self._node.childNodes:
1082 if type(node) is not xml.dom.minidom.Element:
1083 continue
1085 ob = parseAbstractNode(node, self._context)
1086 if ob is not None:
1087 self._geometries.append(ob)
1089 self._popStyle()
1091 def _doCreateGeom(self, instancing):
1093 Create real geometries
1096 for geom in self._geometries:
1097 geom.createGeom(instancing)
1099 def getGeometries(self):
1101 Get list of parsed geometries
1104 return self._geometries
1107 class SVGGeometryPATH(SVGGeometry):
1109 SVG path geometry
1112 __slots__ = ('_splines', # List of splines after parsing
1113 '_styles') # Styles, used for displaying
1115 def __init__(self, node, context):
1117 Initialize SVG path
1120 super().__init__(node, context)
1122 self._splines = []
1123 self._styles = SVGEmptyStyles
1125 def parse(self):
1127 Parse SVG path node
1130 d = self._node.getAttribute('d')
1132 self._styles = SVGParseStyles(self._node, self._context)
1134 pathParser = SVGPathParser(d, self._styles['useFill'])
1135 pathParser.parse()
1137 self._splines = pathParser.getSplines()
1139 def _doCreateGeom(self, instancing):
1141 Create real geometries
1144 ob = SVGCreateCurve(self._context)
1145 cu = ob.data
1147 id_names_from_node(self._node, ob)
1149 if self._styles['useFill']:
1150 cu.dimensions = '2D'
1151 cu.fill_mode = 'BOTH'
1152 cu.materials.append(self._styles['fill'])
1153 else:
1154 cu.dimensions = '3D'
1156 for spline in self._splines:
1157 act_spline = None
1159 if spline['closed'] and len(spline['points']) >= 2:
1160 first = spline['points'][0]
1161 last = spline['points'][-1]
1162 if ( first['handle_left_type'] == 'FREE' and
1163 last['handle_right_type'] == 'VECTOR'):
1164 last['handle_right_type'] = 'FREE'
1165 last['handle_right'] = (last['x'], last['y'])
1166 if ( last['handle_right_type'] == 'FREE' and
1167 first['handle_left_type'] == 'VECTOR'):
1168 first['handle_left_type'] = 'FREE'
1169 first['handle_left'] = (first['x'], first['y'])
1171 for point in spline['points']:
1172 co = self._transformCoord((point['x'], point['y']))
1174 if act_spline is None:
1175 cu.splines.new('BEZIER')
1177 act_spline = cu.splines[-1]
1178 act_spline.use_cyclic_u = spline['closed']
1179 else:
1180 act_spline.bezier_points.add(1)
1182 bezt = act_spline.bezier_points[-1]
1183 bezt.co = co
1185 bezt.handle_left_type = point['handle_left_type']
1186 if point['handle_left'] is not None:
1187 handle = point['handle_left']
1188 bezt.handle_left = self._transformCoord(handle)
1190 bezt.handle_right_type = point['handle_right_type']
1191 if point['handle_right'] is not None:
1192 handle = point['handle_right']
1193 bezt.handle_right = self._transformCoord(handle)
1195 SVGFinishCurve()
1198 class SVGGeometryDEFS(SVGGeometryContainer):
1200 Container for referenced elements
1203 def createGeom(self, instancing):
1205 Create real geometries
1208 pass
1211 class SVGGeometrySYMBOL(SVGGeometryContainer):
1213 Referenced element
1216 def _doCreateGeom(self, instancing):
1218 Create real geometries
1221 self._pushMatrix(self.getNodeMatrix())
1223 super()._doCreateGeom(False)
1225 self._popMatrix()
1227 def createGeom(self, instancing):
1229 Create real geometries
1232 if not instancing:
1233 return
1235 super().createGeom(instancing)
1238 class SVGGeometryG(SVGGeometryContainer):
1240 Geometry group
1243 pass
1246 class SVGGeometryUSE(SVGGeometry):
1248 User of referenced elements
1251 def _doCreateGeom(self, instancing):
1253 Create real geometries
1256 ref = self._node.getAttribute('xlink:href')
1257 geom = self._context['defines'].get(ref)
1259 if geom is not None:
1260 rect = SVGRectFromNode(self._node, self._context)
1261 self._pushRect(rect)
1263 self._pushMatrix(self.getNodeMatrix())
1265 geom.createGeom(True)
1267 self._popMatrix()
1269 self._popRect()
1272 class SVGGeometryRECT(SVGGeometry):
1274 SVG rectangle
1277 __slots__ = ('_rect', # coordinate and dimensions of rectangle
1278 '_radius', # Rounded corner radiuses
1279 '_styles') # Styles, used for displaying
1281 def __init__(self, node, context):
1283 Initialize new rectangle
1286 super().__init__(node, context)
1288 self._rect = ('0', '0', '0', '0')
1289 self._radius = ('0', '0')
1290 self._styles = SVGEmptyStyles
1292 def parse(self):
1294 Parse SVG rectangle node
1297 self._styles = SVGParseStyles(self._node, self._context)
1299 rect = []
1300 for attr in ['x', 'y', 'width', 'height']:
1301 val = self._node.getAttribute(attr)
1302 rect.append(val or '0')
1304 self._rect = (rect)
1306 rx = self._node.getAttribute('rx')
1307 ry = self._node.getAttribute('ry')
1309 self._radius = (rx, ry)
1311 def _appendCorner(self, spline, coord, firstTime, rounded):
1313 Append new corner to rectangle
1316 handle = None
1317 if len(coord) == 3:
1318 handle = self._transformCoord(coord[2])
1319 coord = (coord[0], coord[1])
1321 co = self._transformCoord(coord)
1323 if not firstTime:
1324 spline.bezier_points.add(1)
1326 bezt = spline.bezier_points[-1]
1327 bezt.co = co
1329 if rounded:
1330 if handle:
1331 bezt.handle_left_type = 'VECTOR'
1332 bezt.handle_right_type = 'FREE'
1334 bezt.handle_right = handle
1335 else:
1336 bezt.handle_left_type = 'FREE'
1337 bezt.handle_right_type = 'VECTOR'
1338 bezt.handle_left = co
1340 else:
1341 bezt.handle_left_type = 'VECTOR'
1342 bezt.handle_right_type = 'VECTOR'
1344 def _doCreateGeom(self, instancing):
1346 Create real geometries
1349 # Run-time parsing -- percents would be correct only if
1350 # parsing them now
1351 crect = self._context['rect']
1352 rect = []
1354 for i in range(4):
1355 rect.append(SVGParseCoord(self._rect[i], crect[i % 2]))
1357 r = self._radius
1358 rx = ry = 0.0
1360 if r[0] and r[1]:
1361 rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
1362 ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
1363 elif r[0]:
1364 rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
1365 ry = min(rx, rect[3] / 2)
1366 rx = ry = min(rx, ry)
1367 elif r[1]:
1368 ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
1369 rx = min(ry, rect[2] / 2)
1370 rx = ry = min(rx, ry)
1372 radius = (rx, ry)
1374 # Geometry creation
1375 ob = SVGCreateCurve(self._context)
1376 cu = ob.data
1378 if self._styles['useFill']:
1379 cu.dimensions = '2D'
1380 cu.fill_mode = 'BOTH'
1381 cu.materials.append(self._styles['fill'])
1382 else:
1383 cu.dimensions = '3D'
1385 cu.splines.new('BEZIER')
1387 spline = cu.splines[-1]
1388 spline.use_cyclic_u = True
1390 x, y = rect[0], rect[1]
1391 w, h = rect[2], rect[3]
1392 rx, ry = radius[0], radius[1]
1393 rounded = False
1395 if rx or ry:
1397 # 0 _______ 1
1398 # / \
1399 # / \
1400 # 7 2
1401 # | |
1402 # | |
1403 # 6 3
1404 # \ /
1405 # \ /
1406 # 5 _______ 4
1409 # Optional third component -- right handle coord
1410 coords = [(x + rx, y),
1411 (x + w - rx, y, (x + w, y)),
1412 (x + w, y + ry),
1413 (x + w, y + h - ry, (x + w, y + h)),
1414 (x + w - rx, y + h),
1415 (x + rx, y + h, (x, y + h)),
1416 (x, y + h - ry),
1417 (x, y + ry, (x, y))]
1419 rounded = True
1420 else:
1421 coords = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
1423 firstTime = True
1424 for coord in coords:
1425 self._appendCorner(spline, coord, firstTime, rounded)
1426 firstTime = False
1428 SVGFinishCurve()
1431 class SVGGeometryELLIPSE(SVGGeometry):
1433 SVG ellipse
1436 __slots__ = ('_cx', # X-coordinate of center
1437 '_cy', # Y-coordinate of center
1438 '_rx', # X-axis radius of circle
1439 '_ry', # Y-axis radius of circle
1440 '_styles') # Styles, used for displaying
1442 def __init__(self, node, context):
1444 Initialize new ellipse
1447 super().__init__(node, context)
1449 self._cx = '0.0'
1450 self._cy = '0.0'
1451 self._rx = '0.0'
1452 self._ry = '0.0'
1453 self._styles = SVGEmptyStyles
1455 def parse(self):
1457 Parse SVG ellipse node
1460 self._styles = SVGParseStyles(self._node, self._context)
1462 self._cx = self._node.getAttribute('cx') or '0'
1463 self._cy = self._node.getAttribute('cy') or '0'
1464 self._rx = self._node.getAttribute('rx') or '0'
1465 self._ry = self._node.getAttribute('ry') or '0'
1467 def _doCreateGeom(self, instancing):
1469 Create real geometries
1472 # Run-time parsing -- percents would be correct only if
1473 # parsing them now
1474 crect = self._context['rect']
1476 cx = SVGParseCoord(self._cx, crect[0])
1477 cy = SVGParseCoord(self._cy, crect[1])
1478 rx = SVGParseCoord(self._rx, crect[0])
1479 ry = SVGParseCoord(self._ry, crect[1])
1481 if not rx or not ry:
1482 # Automaic handles will work incorrect in this case
1483 return
1485 # Create circle
1486 ob = SVGCreateCurve(self._context)
1487 cu = ob.data
1489 id_names_from_node(self._node, ob)
1491 if self._styles['useFill']:
1492 cu.dimensions = '2D'
1493 cu.fill_mode = 'BOTH'
1494 cu.materials.append(self._styles['fill'])
1495 else:
1496 cu.dimensions = '3D'
1498 coords = [((cx - rx, cy),
1499 (cx - rx, cy + ry * 0.552),
1500 (cx - rx, cy - ry * 0.552)),
1502 ((cx, cy - ry),
1503 (cx - rx * 0.552, cy - ry),
1504 (cx + rx * 0.552, cy - ry)),
1506 ((cx + rx, cy),
1507 (cx + rx, cy - ry * 0.552),
1508 (cx + rx, cy + ry * 0.552)),
1510 ((cx, cy + ry),
1511 (cx + rx * 0.552, cy + ry),
1512 (cx - rx * 0.552, cy + ry))]
1514 spline = None
1515 for coord in coords:
1516 co = self._transformCoord(coord[0])
1517 handle_left = self._transformCoord(coord[1])
1518 handle_right = self._transformCoord(coord[2])
1520 if spline is None:
1521 cu.splines.new('BEZIER')
1522 spline = cu.splines[-1]
1523 spline.use_cyclic_u = True
1524 else:
1525 spline.bezier_points.add(1)
1527 bezt = spline.bezier_points[-1]
1528 bezt.co = co
1529 bezt.handle_left_type = 'FREE'
1530 bezt.handle_right_type = 'FREE'
1531 bezt.handle_left = handle_left
1532 bezt.handle_right = handle_right
1534 SVGFinishCurve()
1537 class SVGGeometryCIRCLE(SVGGeometryELLIPSE):
1539 SVG circle
1542 def parse(self):
1544 Parse SVG circle node
1547 self._styles = SVGParseStyles(self._node, self._context)
1549 self._cx = self._node.getAttribute('cx') or '0'
1550 self._cy = self._node.getAttribute('cy') or '0'
1552 r = self._node.getAttribute('r') or '0'
1553 self._rx = self._ry = r
1556 class SVGGeometryLINE(SVGGeometry):
1558 SVG line
1561 __slots__ = ('_x1', # X-coordinate of beginning
1562 '_y1', # Y-coordinate of beginning
1563 '_x2', # X-coordinate of ending
1564 '_y2') # Y-coordinate of ending
1566 def __init__(self, node, context):
1568 Initialize new line
1571 super().__init__(node, context)
1573 self._x1 = '0.0'
1574 self._y1 = '0.0'
1575 self._x2 = '0.0'
1576 self._y2 = '0.0'
1578 def parse(self):
1580 Parse SVG line node
1583 self._x1 = self._node.getAttribute('x1') or '0'
1584 self._y1 = self._node.getAttribute('y1') or '0'
1585 self._x2 = self._node.getAttribute('x2') or '0'
1586 self._y2 = self._node.getAttribute('y2') or '0'
1588 def _doCreateGeom(self, instancing):
1590 Create real geometries
1593 # Run-time parsing -- percents would be correct only if
1594 # parsing them now
1595 crect = self._context['rect']
1597 x1 = SVGParseCoord(self._x1, crect[0])
1598 y1 = SVGParseCoord(self._y1, crect[1])
1599 x2 = SVGParseCoord(self._x2, crect[0])
1600 y2 = SVGParseCoord(self._y2, crect[1])
1602 # Create cline
1603 ob = SVGCreateCurve(self._context)
1604 cu = ob.data
1606 id_names_from_node(self._node, ob)
1608 coords = [(x1, y1), (x2, y2)]
1609 spline = None
1611 for coord in coords:
1612 co = self._transformCoord(coord)
1614 if spline is None:
1615 cu.splines.new('BEZIER')
1616 spline = cu.splines[-1]
1617 spline.use_cyclic_u = True
1618 else:
1619 spline.bezier_points.add(1)
1621 bezt = spline.bezier_points[-1]
1622 bezt.co = co
1623 bezt.handle_left_type = 'VECTOR'
1624 bezt.handle_right_type = 'VECTOR'
1626 SVGFinishCurve()
1629 class SVGGeometryPOLY(SVGGeometry):
1631 Abstract class for handling poly-geometries
1632 (polylines and polygons)
1635 __slots__ = ('_points', # Array of points for poly geometry
1636 '_styles', # Styles, used for displaying
1637 '_closed') # Should generated curve be closed?
1639 def __init__(self, node, context):
1641 Initialize new poly geometry
1644 super().__init__(node, context)
1646 self._points = []
1647 self._styles = SVGEmptyStyles
1648 self._closed = False
1650 def parse(self):
1652 Parse poly node
1655 self._styles = SVGParseStyles(self._node, self._context)
1657 points = parse_array_of_floats(self._node.getAttribute('points'))
1659 prev = None
1660 self._points = []
1662 for p in points:
1663 if prev is None:
1664 prev = p
1665 else:
1666 self._points.append((prev, p))
1667 prev = None
1669 def _doCreateGeom(self, instancing):
1671 Create real geometries
1674 ob = SVGCreateCurve(self._context)
1675 cu = ob.data
1677 id_names_from_node(self._node, ob)
1679 if self._closed and self._styles['useFill']:
1680 cu.dimensions = '2D'
1681 cu.fill_mode = 'BOTH'
1682 cu.materials.append(self._styles['fill'])
1683 else:
1684 cu.dimensions = '3D'
1686 spline = None
1688 for point in self._points:
1689 co = self._transformCoord(point)
1691 if spline is None:
1692 cu.splines.new('BEZIER')
1693 spline = cu.splines[-1]
1694 spline.use_cyclic_u = self._closed
1695 else:
1696 spline.bezier_points.add(1)
1698 bezt = spline.bezier_points[-1]
1699 bezt.co = co
1700 bezt.handle_left_type = 'VECTOR'
1701 bezt.handle_right_type = 'VECTOR'
1703 SVGFinishCurve()
1706 class SVGGeometryPOLYLINE(SVGGeometryPOLY):
1708 SVG polyline geometry
1711 pass
1714 class SVGGeometryPOLYGON(SVGGeometryPOLY):
1716 SVG polygon geometry
1719 def __init__(self, node, context):
1721 Initialize new polygon geometry
1724 super().__init__(node, context)
1726 self._closed = True
1729 class SVGGeometrySVG(SVGGeometryContainer):
1731 Main geometry holder
1734 def _doCreateGeom(self, instancing):
1736 Create real geometries
1739 rect = SVGRectFromNode(self._node, self._context)
1741 matrix = self.getNodeMatrix()
1743 # Better SVG compatibility: match svg-document units
1744 # with blender units
1746 viewbox = []
1747 unit = ''
1749 if self._node.getAttribute('height'):
1750 raw_height = self._node.getAttribute('height')
1751 token, last_char = read_float(raw_height)
1752 document_height = float(token)
1753 unit = raw_height[last_char:].strip()
1755 if self._node.getAttribute('viewBox'):
1756 viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
1758 if len(viewbox) == 4 and unit in ('cm', 'mm', 'in', 'pt', 'pc'):
1760 #convert units to BU:
1761 unitscale = units[unit] / 90 * 1000 / 39.3701
1763 #apply blender unit scale:
1764 unitscale = unitscale / bpy.context.scene.unit_settings.scale_length
1766 matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((1.0, 0.0, 0.0)))
1767 matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((0.0, 1.0, 0.0)))
1769 # match document origin with 3D space origin.
1770 if self._node.getAttribute('viewBox'):
1771 viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
1772 matrix = matrix @ matrix.Translation([0.0, - viewbox[1] - viewbox[3], 0.0])
1774 self._pushMatrix(matrix)
1775 self._pushRect(rect)
1777 super()._doCreateGeom(False)
1779 self._popRect()
1780 self._popMatrix()
1783 class SVGLoader(SVGGeometryContainer):
1785 SVG file loader
1788 def getTransformMatrix(self):
1790 Get matrix created from "transform" attribute
1793 # SVG document doesn't support transform specification
1794 # it can't even hold attributes
1796 return None
1798 def __init__(self, context, filepath, do_colormanage):
1800 Initialize SVG loader
1802 import os
1804 svg_name = os.path.basename(filepath)
1805 scene = context.scene
1806 collection = bpy.data.collections.new(name=svg_name)
1807 scene.collection.children.link(collection)
1809 node = xml.dom.minidom.parse(filepath)
1811 m = Matrix()
1812 m = m @ Matrix.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
1813 m = m @ Matrix.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
1815 rect = (0, 0)
1817 self._context = {'defines': {},
1818 'transform': [],
1819 'rects': [rect],
1820 'rect': rect,
1821 'matrix': m,
1822 'materials': {},
1823 'styles': [None],
1824 'style': None,
1825 'do_colormanage': do_colormanage,
1826 'collection': collection}
1828 super().__init__(node, self._context)
1831 svgGeometryClasses = {
1832 'svg': SVGGeometrySVG,
1833 'path': SVGGeometryPATH,
1834 'defs': SVGGeometryDEFS,
1835 'symbol': SVGGeometrySYMBOL,
1836 'use': SVGGeometryUSE,
1837 'rect': SVGGeometryRECT,
1838 'ellipse': SVGGeometryELLIPSE,
1839 'circle': SVGGeometryCIRCLE,
1840 'line': SVGGeometryLINE,
1841 'polyline': SVGGeometryPOLYLINE,
1842 'polygon': SVGGeometryPOLYGON,
1843 'g': SVGGeometryG}
1846 def parseAbstractNode(node, context):
1847 name = node.tagName.lower()
1849 if name.startswith('svg:'):
1850 name = name[4:]
1852 geomClass = svgGeometryClasses.get(name)
1854 if geomClass is not None:
1855 ob = geomClass(node, context)
1856 ob.parse()
1858 return ob
1860 return None
1863 def load_svg(context, filepath, do_colormanage):
1865 Load specified SVG file
1868 if bpy.ops.object.mode_set.poll():
1869 bpy.ops.object.mode_set(mode='OBJECT')
1871 loader = SVGLoader(context, filepath, do_colormanage)
1872 loader.parse()
1873 loader.createGeom(False)
1876 def load(operator, context, filepath=""):
1878 # error in code should raise exceptions but loading
1879 # non SVG files can give useful messages.
1880 do_colormanage = context.scene.display_settings.display_device != 'NONE'
1881 try:
1882 load_svg(context, filepath, do_colormanage)
1883 except (xml.parsers.expat.ExpatError, UnicodeEncodeError) as e:
1884 import traceback
1885 traceback.print_exc()
1887 operator.report({'WARNING'}, "Unable to parse XML, %s:%s for file %r" % (type(e).__name__, e, filepath))
1888 return {'CANCELLED'}
1890 return {'FINISHED'}