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 #####
22 import xml
.dom
.minidom
23 from math
import cos
, sin
, tan
, atan2
, pi
, ceil
26 from mathutils
import Vector
, Matrix
28 from . import svg_colors
29 from .svg_util
import (units
,
32 parse_array_of_floats
,
35 #### Common utilities ####
37 SVGEmptyStyles
= {'useFill': None,
41 def SVGCreateCurve(context
):
43 Create new curve object to hold splines in
46 cu
= bpy
.data
.curves
.new("Curve", 'CURVE')
47 obj
= bpy
.data
.objects
.new("Curve", cu
)
49 context
['collection'].objects
.link(obj
)
62 def SVGFlipHandle(x
, y
, x1
, y1
):
64 Flip handle around base point
73 def SVGParseCoord(coord
, size
):
75 Parse coordinate component to common basis
77 Needed to handle coordinates set in cm, mm, inches.
80 token
, last_char
= read_float(coord
)
82 unit
= coord
[last_char
:].strip() # strip() in case there is a space
85 return float(size
) / 100.0 * val
87 return val
* units
[unit
]
92 def SVGRectFromNode(node
, context
):
94 Get display rectangle from node
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
)
105 if node
.getAttribute('width'):
106 w
= SVGParseCoord(node
.getAttribute('width'), w
)
108 if node
.getAttribute('height'):
109 h
= SVGParseCoord(node
.getAttribute('height'), 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
:
125 rect
= context
['rect']
126 has_user_coordinate
= (len(context
['rects']) > 1)
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:
150 if has_user_coordinate
or (w
!= 0 and h
!= 0):
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)))
170 def SVGParseTransform(transform
):
172 Parse transform string and return transformation 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
)
185 raise Exception('Unknown trasnform function: ' + func
)
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
]
204 if color
.startswith('#'):
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]))
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
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
257 return Matrix(((a
, c
, 0.0, e
),
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
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)))
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),
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
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')
338 elems
= style
.split(';')
345 name
= s
[0].strip().lower()
351 styles
['useFill'] = False
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
)
362 if styles
['useFill'] is None:
363 fill
= node
.getAttribute('fill')
367 styles
['useFill'] = False
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
)
381 def id_names_from_node(node
, ob
):
382 if node
.getAttribute('id'):
383 name
= node
.getAttribute('id')
387 #### SVG path helpers ####
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
407 commands
= {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
417 elif c
.lower() in commands
:
419 elif c
in ['-', '.'] or c
.isdigit():
420 token
, last_char
= read_float(d
, i
)
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
432 self
._len
= len(tokens
)
436 Check if end of data reached
439 return self
._index
>= self
._len
449 return self
._data
[self
._index
]
451 def lookupNext(self
):
453 get next token without moving pointer
459 return self
._data
[self
._index
]
463 Return current token and go to next one
469 token
= self
._data
[self
._index
]
476 Return coordinate created from current token and move to next token
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:
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': [],
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
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
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':
593 handle_left_type
= 'FREE'
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
):
624 relative
= code
.islower()
625 x
, y
= self
._getCoordPair
(relative
, self
._point
)
627 self
._spline
= None # Flag to start new spline
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
)
640 cur
= self
._data
.cur()
644 def _pathLineTo(self
, code
):
651 cur
= self
._data
.cur()
652 while cur
is not None and not cur
.isalpha():
654 x
, y
= self
._getCoordPair
(code
== 'l', self
._point
)
656 x
= self
._data
.nextCoord()
660 y
= self
._data
.nextCoord()
667 if self
._spline
is None:
668 self
._appendPoint
(self
._point
[0], self
._point
[1])
670 self
._appendPoint
(x
, y
)
673 cur
= self
._data
.cur()
677 def _pathCurveToCS(self
, code
):
679 Cubic BEZIER CurveTo path command
683 cur
= self
._data
.cur()
684 while cur
is not None and not cur
.isalpha():
686 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
687 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
689 if self
._handle
is not None:
690 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
691 self
._handle
[0], self
._handle
[1])
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
))
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
))
711 self
._handle
= (x2
, y2
)
712 cur
= self
._data
.cur()
714 def _pathCurveToQT(self
, code
):
716 Quadratic BEZIER CurveTo path command
720 cur
= self
._data
.cur()
722 while cur
is not None and not cur
.isalpha():
724 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
726 if self
._handle
is not None:
727 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
728 self
._handle
[0], self
._handle
[1])
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
))
745 self
._handle
= (x1
, y1
)
746 cur
= self
._data
.cur()
748 def _calcArc(self
, rx
, ry
, ang
, fa
, fs
, x
, y
):
752 Copied and adoptedfrom paths_svg2obj.py script for Blender 2.49
753 which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
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
765 px
= px
/ (rx
** 2.0)
768 rpy
= py
/ (ry
** 2.0)
776 carx
= sarx
= cary
= sary
= 0.0
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
)
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:
812 elif ang_arc
> 0.0 and fs
== 0:
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
)
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
)
866 cur
= self
._data
.cur()
868 def _pathClose(self
, code
):
874 self
._spline
['closed'] = True
876 cv
= self
._spline
['points'][0]
877 self
._point
= (cv
['x'], cv
['y'])
886 while not self
._data
.eof():
887 code
= self
._data
.next()
888 cmd
= self
._commands
.get(code
)
891 raise Exception('Unknown path command: {0}' . format(code
))
893 if cmd
in {'Z', 'z'}:
899 if self
._use
_fill
and not closed
:
902 def getSplines(self
):
904 Get splines definitions
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
919 # need to detect cycles for USE node
921 def __init__(self
, node
, context
):
923 Initialize SVG geometry
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
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
):
978 self
._context
['styles'].append(style
)
979 self
._context
['style'] = 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
)
1007 Parse XML node to memory
1012 def _doCreateGeom(self
, instancing
):
1014 Internal handler to create real geometries
1019 def getTransformMatrix(self
):
1021 Get matrix created from "transform" attribute
1024 transform
= self
._node
.getAttribute('transform')
1027 return SVGParseTransform(transform
)
1031 def createGeom(self
, instancing
):
1033 Create real geometries
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:
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
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
:
1085 ob
= parseAbstractNode(node
, self
._context
)
1087 self
._geometries
.append(ob
)
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
):
1112 __slots__
= ('_splines', # List of splines after parsing
1113 '_styles') # Styles, used for displaying
1115 def __init__(self
, node
, context
):
1120 super().__init
__(node
, context
)
1123 self
._styles
= SVGEmptyStyles
1130 d
= self
._node
.getAttribute('d')
1132 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1134 pathParser
= SVGPathParser(d
, self
._styles
['useFill'])
1137 self
._splines
= pathParser
.getSplines()
1139 def _doCreateGeom(self
, instancing
):
1141 Create real geometries
1144 ob
= SVGCreateCurve(self
._context
)
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'])
1154 cu
.dimensions
= '3D'
1156 for spline
in self
._splines
:
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']
1180 act_spline
.bezier_points
.add(1)
1182 bezt
= act_spline
.bezier_points
[-1]
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
)
1198 class SVGGeometryDEFS(SVGGeometryContainer
):
1200 Container for referenced elements
1203 def createGeom(self
, instancing
):
1205 Create real geometries
1211 class SVGGeometrySYMBOL(SVGGeometryContainer
):
1216 def _doCreateGeom(self
, instancing
):
1218 Create real geometries
1221 self
._pushMatrix
(self
.getNodeMatrix())
1223 super()._doCreateGeom
(False)
1227 def createGeom(self
, instancing
):
1229 Create real geometries
1235 super().createGeom(instancing
)
1238 class SVGGeometryG(SVGGeometryContainer
):
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)
1272 class SVGGeometryRECT(SVGGeometry
):
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
1294 Parse SVG rectangle node
1297 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1300 for attr
in ['x', 'y', 'width', 'height']:
1301 val
= self
._node
.getAttribute(attr
)
1302 rect
.append(val
or '0')
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
1318 handle
= self
._transformCoord
(coord
[2])
1319 coord
= (coord
[0], coord
[1])
1321 co
= self
._transformCoord
(coord
)
1324 spline
.bezier_points
.add(1)
1326 bezt
= spline
.bezier_points
[-1]
1331 bezt
.handle_left_type
= 'VECTOR'
1332 bezt
.handle_right_type
= 'FREE'
1334 bezt
.handle_right
= handle
1336 bezt
.handle_left_type
= 'FREE'
1337 bezt
.handle_right_type
= 'VECTOR'
1338 bezt
.handle_left
= co
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
1351 crect
= self
._context
['rect']
1355 rect
.append(SVGParseCoord(self
._rect
[i
], crect
[i
% 2]))
1361 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1362 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1364 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1365 ry
= min(rx
, rect
[3] / 2)
1366 rx
= ry
= min(rx
, ry
)
1368 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1369 rx
= min(ry
, rect
[2] / 2)
1370 rx
= ry
= min(rx
, ry
)
1375 ob
= SVGCreateCurve(self
._context
)
1378 if self
._styles
['useFill']:
1379 cu
.dimensions
= '2D'
1380 cu
.fill_mode
= 'BOTH'
1381 cu
.materials
.append(self
._styles
['fill'])
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]
1409 # Optional third component -- right handle coord
1410 coords
= [(x
+ rx
, y
),
1411 (x
+ w
- rx
, y
, (x
+ w
, y
)),
1413 (x
+ w
, y
+ h
- ry
, (x
+ w
, y
+ h
)),
1414 (x
+ w
- rx
, y
+ h
),
1415 (x
+ rx
, y
+ h
, (x
, y
+ h
)),
1417 (x
, y
+ ry
, (x
, y
))]
1421 coords
= [(x
, y
), (x
+ w
, y
), (x
+ w
, y
+ h
), (x
, y
+ h
)]
1424 for coord
in coords
:
1425 self
._appendCorner
(spline
, coord
, firstTime
, rounded
)
1431 class SVGGeometryELLIPSE(SVGGeometry
):
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
)
1453 self
._styles
= SVGEmptyStyles
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
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
1486 ob
= SVGCreateCurve(self
._context
)
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'])
1496 cu
.dimensions
= '3D'
1498 coords
= [((cx
- rx
, cy
),
1499 (cx
- rx
, cy
+ ry
* 0.552),
1500 (cx
- rx
, cy
- ry
* 0.552)),
1503 (cx
- rx
* 0.552, cy
- ry
),
1504 (cx
+ rx
* 0.552, cy
- ry
)),
1507 (cx
+ rx
, cy
- ry
* 0.552),
1508 (cx
+ rx
, cy
+ ry
* 0.552)),
1511 (cx
+ rx
* 0.552, cy
+ ry
),
1512 (cx
- rx
* 0.552, cy
+ ry
))]
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])
1521 cu
.splines
.new('BEZIER')
1522 spline
= cu
.splines
[-1]
1523 spline
.use_cyclic_u
= True
1525 spline
.bezier_points
.add(1)
1527 bezt
= spline
.bezier_points
[-1]
1529 bezt
.handle_left_type
= 'FREE'
1530 bezt
.handle_right_type
= 'FREE'
1531 bezt
.handle_left
= handle_left
1532 bezt
.handle_right
= handle_right
1537 class SVGGeometryCIRCLE(SVGGeometryELLIPSE
):
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
):
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
):
1571 super().__init
__(node
, context
)
1583 self
._x
1 = self
._node
.getAttribute('x1') or '0'
1584 self
._y
1 = self
._node
.getAttribute('y1') or '0'
1585 self
._x
2 = self
._node
.getAttribute('x2') or '0'
1586 self
._y
2 = 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
1595 crect
= self
._context
['rect']
1597 x1
= SVGParseCoord(self
._x
1, crect
[0])
1598 y1
= SVGParseCoord(self
._y
1, crect
[1])
1599 x2
= SVGParseCoord(self
._x
2, crect
[0])
1600 y2
= SVGParseCoord(self
._y
2, crect
[1])
1603 ob
= SVGCreateCurve(self
._context
)
1606 id_names_from_node(self
._node
, ob
)
1608 coords
= [(x1
, y1
), (x2
, y2
)]
1611 for coord
in coords
:
1612 co
= self
._transformCoord
(coord
)
1615 cu
.splines
.new('BEZIER')
1616 spline
= cu
.splines
[-1]
1617 spline
.use_cyclic_u
= True
1619 spline
.bezier_points
.add(1)
1621 bezt
= spline
.bezier_points
[-1]
1623 bezt
.handle_left_type
= 'VECTOR'
1624 bezt
.handle_right_type
= 'VECTOR'
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
)
1647 self
._styles
= SVGEmptyStyles
1648 self
._closed
= False
1655 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1657 points
= parse_array_of_floats(self
._node
.getAttribute('points'))
1666 self
._points
.append((prev
, p
))
1669 def _doCreateGeom(self
, instancing
):
1671 Create real geometries
1674 ob
= SVGCreateCurve(self
._context
)
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'])
1684 cu
.dimensions
= '3D'
1688 for point
in self
._points
:
1689 co
= self
._transformCoord
(point
)
1692 cu
.splines
.new('BEZIER')
1693 spline
= cu
.splines
[-1]
1694 spline
.use_cyclic_u
= self
._closed
1696 spline
.bezier_points
.add(1)
1698 bezt
= spline
.bezier_points
[-1]
1700 bezt
.handle_left_type
= 'VECTOR'
1701 bezt
.handle_right_type
= 'VECTOR'
1706 class SVGGeometryPOLYLINE(SVGGeometryPOLY
):
1708 SVG polyline geometry
1714 class SVGGeometryPOLYGON(SVGGeometryPOLY
):
1716 SVG polygon geometry
1719 def __init__(self
, node
, context
):
1721 Initialize new polygon geometry
1724 super().__init
__(node
, context
)
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
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)
1783 class SVGLoader(SVGGeometryContainer
):
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
1798 def __init__(self
, context
, filepath
, do_colormanage
):
1800 Initialize SVG loader
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
)
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)))
1817 self
._context
= {'defines': {},
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
,
1846 def parseAbstractNode(node
, context
):
1847 name
= node
.tagName
.lower()
1849 if name
.startswith('svg:'):
1852 geomClass
= svgGeometryClasses
.get(name
)
1854 if geomClass
is not None:
1855 ob
= geomClass(node
, context
)
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
)
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'
1882 load_svg(context
, filepath
, do_colormanage
)
1883 except (xml
.parsers
.expat
.ExpatError
, UnicodeEncodeError) as e
:
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'}