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
30 #### Common utilities ####
32 # TODO: "em" and "ex" aren't actually supported
42 "INVALID": 1.0, # some DocBook files contain this
45 SVGEmptyStyles
= {'useFill': None,
48 def srgb_to_linearrgb(c
):
50 return 0.0 if c
< 0.0 else c
* (1.0 / 12.92)
52 return pow((c
+ 0.055) * (1.0 / 1.055), 2.4)
54 def check_points_equal(point_a
, point_b
):
55 return (abs(point_a
[0] - point_b
[0]) < 1e-6 and
56 abs(point_a
[1] - point_b
[1]) < 1e-6)
59 def SVGParseFloat(s
, i
=0):
61 Parse first float value from string
63 Returns value as string
70 # Skip leading whitespace characters
71 while i
< n
and (s
[i
].isspace() or s
[i
] == ','):
86 while i
< n
and s
[i
].isdigit():
91 if i
< n
and s
[i
] == '.':
96 while i
< n
and s
[i
].isdigit():
99 elif s
[i
].isspace() or s
[i
] == ',':
100 # Inkscape sometimes uses weird float format with missed
101 # fractional part after dot. Suppose zero fractional part
105 raise Exception('Invalid float value near ' + s
[start
:start
+ 10])
108 if i
< n
and (s
[i
] == 'e' or s
[i
] == 'E'):
111 if s
[i
] == '+' or s
[i
] == '-':
116 while i
< n
and s
[i
].isdigit():
120 raise Exception('Invalid float value near ' + s
[start
:start
+ 10])
125 def SVGCreateCurve():
127 Create new curve object to hold splines in
130 cu
= bpy
.data
.curves
.new("Curve", 'CURVE')
131 obj
= bpy
.data
.objects
.new("Curve", cu
)
132 bpy
.context
.scene
.objects
.link(obj
)
137 def SVGFinishCurve():
139 Finish curve creation
145 def SVGFlipHandle(x
, y
, x1
, y1
):
147 Flip handle around base point
156 def SVGParseCoord(coord
, size
):
158 Parse coordinate component to common basis
160 Needed to handle coordinates set in cm, mm, iches..
163 token
, last_char
= SVGParseFloat(coord
)
165 unit
= coord
[last_char
:].strip() # strip() in case there is a space
168 return float(size
) / 100.0 * val
170 return val
* SVGUnits
[unit
]
175 def SVGRectFromNode(node
, context
):
177 Get display rectangle from node
180 w
= context
['rect'][0]
181 h
= context
['rect'][1]
183 if node
.getAttribute('viewBox'):
184 viewBox
= node
.getAttribute('viewBox').replace(',', ' ').split()
185 w
= SVGParseCoord(viewBox
[2], w
)
186 h
= SVGParseCoord(viewBox
[3], h
)
188 if node
.getAttribute('width'):
189 w
= SVGParseCoord(node
.getAttribute('width'), w
)
191 if node
.getAttribute('height'):
192 h
= SVGParseCoord(node
.getAttribute('height'), h
)
197 def SVGMatrixFromNode(node
, context
):
199 Get transformation matrix from given node
202 tagName
= node
.tagName
.lower()
203 tags
= ['svg:svg', 'svg:use', 'svg:symbol']
205 if tagName
not in tags
and 'svg:' + tagName
not in tags
:
208 rect
= context
['rect']
209 has_user_coordinate
= (len(context
['rects']) > 1)
212 x
= SVGParseCoord(node
.getAttribute('x') or '0', rect
[0])
213 y
= SVGParseCoord(node
.getAttribute('y') or '0', rect
[1])
214 w
= SVGParseCoord(node
.getAttribute('width') or str(rect
[0]), rect
[0])
215 h
= SVGParseCoord(node
.getAttribute('height') or str(rect
[1]), rect
[1])
217 m
= Matrix
.Translation(Vector((x
, y
, 0.0)))
218 if has_user_coordinate
:
219 if rect
[0] != 0 and rect
[1] != 0:
220 m
= m
* Matrix
.Scale(w
/ rect
[0], 4, Vector((1.0, 0.0, 0.0)))
221 m
= m
* Matrix
.Scale(h
/ rect
[1], 4, Vector((0.0, 1.0, 0.0)))
223 if node
.getAttribute('viewBox'):
224 viewBox
= node
.getAttribute('viewBox').replace(',', ' ').split()
225 vx
= SVGParseCoord(viewBox
[0], w
)
226 vy
= SVGParseCoord(viewBox
[1], h
)
227 vw
= SVGParseCoord(viewBox
[2], w
)
228 vh
= SVGParseCoord(viewBox
[3], h
)
230 if vw
== 0 or vh
== 0:
233 if has_user_coordinate
or (w
!= 0 and h
!= 0):
242 tx
= (w
- vw
* scale
) / 2
243 ty
= (h
- vh
* scale
) / 2
244 m
= m
* Matrix
.Translation(Vector((tx
, ty
, 0.0)))
246 m
= m
* Matrix
.Translation(Vector((-vx
, -vy
, 0.0)))
247 m
= m
* Matrix
.Scale(scale
, 4, Vector((1.0, 0.0, 0.0)))
248 m
= m
* Matrix
.Scale(scale
, 4, Vector((0.0, 1.0, 0.0)))
253 def SVGParseTransform(transform
):
255 Parse transform string and return transformation matrix
259 r
= re
.compile('\s*([A-z]+)\s*\((.*?)\)')
261 for match
in r
.finditer(transform
):
262 func
= match
.group(1)
263 params
= match
.group(2)
264 params
= params
.replace(',', ' ').split()
266 proc
= SVGTransforms
.get(func
)
268 raise Exception('Unknown trasnform function: ' + func
)
275 def SVGGetMaterial(color
, context
):
277 Get material for specified color
280 materials
= context
['materials']
281 rgb_re
= re
.compile('^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
283 if color
in materials
:
284 return materials
[color
]
287 if color
.startswith('#'):
291 color
= color
[0] * 2 + color
[1] * 2 + color
[2] * 2
293 diff
= (int(color
[0:2], 16), int(color
[2:4], 16), int(color
[4:6], 16))
294 elif color
in svg_colors
.SVGColors
:
295 diff
= svg_colors
.SVGColors
[color
]
296 elif rgb_re
.match(color
):
297 c
= rgb_re
.findall(color
)[0]
298 diff
= (float(c
[0]), float(c
[1]), float(c
[2]))
302 diffuse_color
= ([x
/ 255.0 for x
in diff
])
304 if context
['do_colormanage']:
305 diffuse_color
[0] = srgb_to_linearrgb(diffuse_color
[0])
306 diffuse_color
[1] = srgb_to_linearrgb(diffuse_color
[1])
307 diffuse_color
[2] = srgb_to_linearrgb(diffuse_color
[2])
309 mat
= bpy
.data
.materials
.new(name
='SVGMat')
310 mat
.diffuse_color
= diffuse_color
311 mat
.diffuse_intensity
= 1.0
313 materials
[color
] = mat
318 def SVGTransformTranslate(params
):
320 translate SVG transform command
323 tx
= float(params
[0])
324 ty
= float(params
[1]) if len(params
) > 1 else 0.0
326 return Matrix
.Translation(Vector((tx
, ty
, 0.0)))
329 def SVGTransformMatrix(params
):
331 matrix SVG transform command
341 return Matrix(((a
, c
, 0.0, e
),
347 def SVGTransformScale(params
):
349 scale SVG transform command
352 sx
= float(params
[0])
353 sy
= float(params
[1]) if len(params
) > 1 else sx
357 m
= m
* Matrix
.Scale(sx
, 4, Vector((1.0, 0.0, 0.0)))
358 m
= m
* Matrix
.Scale(sy
, 4, Vector((0.0, 1.0, 0.0)))
363 def SVGTransformSkewX(params
):
365 skewX SVG transform command
368 ang
= float(params
[0]) * pi
/ 180.0
370 return Matrix(((1.0, 0.0, 0.0),
371 (tan(ang
), 1.0, 0.0),
372 (0.0, 0.0, 1.0))).to_4x4()
375 def SVGTransformSkewY(params
):
377 skewX SVG transform command
380 ang
= float(params
[0]) * pi
/ 180.0
382 return Matrix(((1.0, tan(ang
), 0.0),
384 (0.0, 0.0, 1.0))).to_4x4()
387 def SVGTransformRotate(params
):
389 skewX SVG transform command
392 ang
= float(params
[0]) * pi
/ 180.0
396 cx
= float(params
[1])
397 cy
= float(params
[2])
399 tm
= Matrix
.Translation(Vector((cx
, cy
, 0.0)))
400 rm
= Matrix
.Rotation(ang
, 4, Vector((0.0, 0.0, 1.0)))
402 return tm
* rm
* tm
.inverted()
404 SVGTransforms
= {'translate': SVGTransformTranslate
,
405 'scale': SVGTransformScale
,
406 'skewX': SVGTransformSkewX
,
407 'skewY': SVGTransformSkewY
,
408 'matrix': SVGTransformMatrix
,
409 'rotate': SVGTransformRotate
}
412 def SVGParseStyles(node
, context
):
414 Parse node to get different styles for displaying geometries
415 (materials, filling flags, etc..)
418 styles
= SVGEmptyStyles
.copy()
420 style
= node
.getAttribute('style')
422 elems
= style
.split(';')
429 name
= s
[0].strip().lower()
435 styles
['useFill'] = False
437 styles
['useFill'] = True
438 styles
['fill'] = SVGGetMaterial(val
, context
)
440 if styles
['useFill'] is None:
441 styles
['useFill'] = True
442 styles
['fill'] = SVGGetMaterial('#000', context
)
446 if styles
['useFill'] is None:
447 fill
= node
.getAttribute('fill')
451 styles
['useFill'] = False
453 styles
['useFill'] = True
454 styles
['fill'] = SVGGetMaterial(fill
, context
)
456 if styles
['useFill'] is None and context
['style']:
457 styles
= context
['style'].copy()
459 if styles
['useFill'] is None:
460 styles
['useFill'] = True
461 styles
['fill'] = SVGGetMaterial('#000', context
)
465 #### SVG path helpers ####
470 SVG Path data token supplier
473 __slots__
= ('_data', # List of tokens
474 '_index', # Index of current token in tokens list
475 '_len') # Length of tokens list
477 def __init__(self
, d
):
479 Initialize new path data supplier
481 d - the definition of the outline of a shape
485 commands
= {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
495 elif c
.lower() in commands
:
497 elif c
in ['-', '.'] or c
.isdigit():
498 token
, last_char
= SVGParseFloat(d
, i
)
501 # in most cases len(token) and (last_char - i) are the same
502 # but with whitespace or ',' prefix they are not.
504 i
+= (last_char
- i
) - 1
510 self
._len
= len(tokens
)
514 Check if end of data reached
517 return self
._index
>= self
._len
527 return self
._data
[self
._index
]
529 def lookupNext(self
):
531 get next token without moving pointer
537 return self
._data
[self
._index
]
541 Return current token and go to next one
547 token
= self
._data
[self
._index
]
554 Return coordinate created from current token and move to next token
567 Parser of SVG path data
570 __slots__
= ('_data', # Path data supplird
571 '_point', # Current point coorfinate
572 '_handle', # Last handle coordinate
573 '_splines', # List of all splies created during parsing
574 '_spline', # Currently handling spline
575 '_commands', # Hash of all supported path commands
576 '_use_fill', # Splines would be filled, so expected to be closed
579 def __init__(self
, d
, use_fill
):
581 Initialize path parser
583 d - the definition of the outline of a shape
586 self
._data
= SVGPathData(d
)
587 self
._point
= None # Current point
588 self
._handle
= None # Last handle
589 self
._splines
= [] # List of splines in path
590 self
._spline
= None # Current spline
591 self
._use
_fill
= use_fill
593 self
._commands
= {'M': self
._pathMoveTo
,
594 'L': self
._pathLineTo
,
595 'H': self
._pathLineTo
,
596 'V': self
._pathLineTo
,
597 'C': self
._pathCurveToCS
,
598 'S': self
._pathCurveToCS
,
599 'Q': self
._pathCurveToQT
,
600 'T': self
._pathCurveToQT
,
601 'A': self
._pathCurveToA
,
602 'Z': self
._pathClose
,
604 'm': self
._pathMoveTo
,
605 'l': self
._pathLineTo
,
606 'h': self
._pathLineTo
,
607 'v': self
._pathLineTo
,
608 'c': self
._pathCurveToCS
,
609 's': self
._pathCurveToCS
,
610 'q': self
._pathCurveToQT
,
611 't': self
._pathCurveToQT
,
612 'a': self
._pathCurveToA
,
613 'z': self
._pathClose
}
615 def _getCoordPair(self
, relative
, point
):
617 Get next coordinate pair
620 x
= self
._data
.nextCoord()
621 y
= self
._data
.nextCoord()
623 if relative
and point
is not None:
629 def _appendPoint(self
, x
, y
, handle_left
=None, handle_left_type
='VECTOR',
630 handle_right
=None, handle_right_type
='VECTOR'):
632 Append point to spline
634 If there's no active spline, create one and set it's first point
635 to current point coordinate
638 if self
._spline
is None:
639 self
._spline
= {'points': [],
642 self
._splines
.append(self
._spline
)
644 if len(self
._spline
['points']) > 0:
645 # Not sure about specifications, but Illustrator could create
646 # last point at the same position, as start point (which was
647 # reached by MoveTo command) to set needed handle coords.
648 # It's also could use last point at last position to make path
651 first
= self
._spline
['points'][0]
652 if check_points_equal((first
['x'], first
['y']), (x
, y
)):
653 if handle_left
is not None:
654 first
['handle_left'] = handle_left
655 first
['handle_left_type'] = 'FREE'
657 if handle_left_type
!= 'VECTOR':
658 first
['handle_left_type'] = handle_left_type
660 if self
._data
.eof() or self
._data
.lookupNext().lower() == 'm':
661 self
._spline
['closed'] = True
665 last
= self
._spline
['points'][-1]
666 if last
['handle_right_type'] == 'VECTOR' and handle_left_type
== 'FREE':
667 last
['handle_right'] = (last
['x'], last
['y'])
668 last
['handle_right_type'] = 'FREE'
669 if last
['handle_right_type'] == 'FREE' and handle_left_type
== 'VECTOR':
671 handle_left_type
= 'FREE'
676 'handle_left': handle_left
,
677 'handle_left_type': handle_left_type
,
679 'handle_right': handle_right
,
680 'handle_right_type': handle_right_type
}
682 self
._spline
['points'].append(point
)
684 def _updateHandle(self
, handle
=None, handle_type
=None):
686 Update right handle of previous point when adding new point to spline
689 point
= self
._spline
['points'][-1]
691 if handle_type
is not None:
692 point
['handle_right_type'] = handle_type
694 if handle
is not None:
695 point
['handle_right'] = handle
697 def _pathMoveTo(self
, code
):
702 relative
= code
.islower()
703 x
, y
= self
._getCoordPair
(relative
, self
._point
)
705 self
._spline
= None # Flag to start new spline
708 cur
= self
._data
.cur()
709 while cur
is not None and not cur
.isalpha():
710 x
, y
= self
._getCoordPair
(relative
, self
._point
)
712 if self
._spline
is None:
713 self
._appendPoint
(self
._point
[0], self
._point
[1])
715 self
._appendPoint
(x
, y
)
718 cur
= self
._data
.cur()
722 def _pathLineTo(self
, code
):
729 cur
= self
._data
.cur()
730 while cur
is not None and not cur
.isalpha():
732 x
, y
= self
._getCoordPair
(code
== 'l', self
._point
)
734 x
= self
._data
.nextCoord()
738 y
= self
._data
.nextCoord()
745 if self
._spline
is None:
746 self
._appendPoint
(self
._point
[0], self
._point
[1])
748 self
._appendPoint
(x
, y
)
751 cur
= self
._data
.cur()
755 def _pathCurveToCS(self
, code
):
757 Cubic BEZIER CurveTo path command
761 cur
= self
._data
.cur()
762 while cur
is not None and not cur
.isalpha():
764 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
765 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
767 if self
._handle
is not None:
768 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
769 self
._handle
[0], self
._handle
[1])
773 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
775 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
777 if self
._spline
is None:
778 self
._appendPoint
(self
._point
[0], self
._point
[1],
779 handle_left_type
='FREE', handle_left
=self
._point
,
780 handle_right_type
='FREE', handle_right
=(x1
, y1
))
782 self
._updateHandle
(handle
=(x1
, y1
), handle_type
='FREE')
784 self
._appendPoint
(x
, y
,
785 handle_left_type
='FREE', handle_left
=(x2
, y2
),
786 handle_right_type
='FREE', handle_right
=(x
, y
))
789 self
._handle
= (x2
, y2
)
790 cur
= self
._data
.cur()
792 def _pathCurveToQT(self
, code
):
794 Quadratic BEZIER CurveTo path command
798 cur
= self
._data
.cur()
800 while cur
is not None and not cur
.isalpha():
802 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
804 if self
._handle
is not None:
805 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
806 self
._handle
[0], self
._handle
[1])
810 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
812 if not check_points_equal((x
, y
), self
._point
):
813 if self
._spline
is None:
814 self
._appendPoint
(self
._point
[0], self
._point
[1],
815 handle_left_type
='FREE', handle_left
=self
._point
,
816 handle_right_type
='FREE', handle_right
=self
._point
)
818 self
._appendPoint
(x
, y
,
819 handle_left_type
='FREE', handle_left
=(x1
, y1
),
820 handle_right_type
='FREE', handle_right
=(x
, y
))
823 self
._handle
= (x1
, y1
)
824 cur
= self
._data
.cur()
826 def _calcArc(self
, rx
, ry
, ang
, fa
, fs
, x
, y
):
830 Copied and adoptedfrom paths_svg2obj.py script for Blender 2.49
831 which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
838 px
= abs((cos(ang
) * (cpx
- x
) + sin(ang
) * (cpy
- y
)) * 0.5) ** 2.0
839 py
= abs((cos(ang
) * (cpy
- y
) - sin(ang
) * (cpx
- x
)) * 0.5) ** 2.0
843 px
= px
/ (rx
** 2.0)
846 rpy
= py
/ (ry
** 2.0)
854 carx
= sarx
= cary
= sary
= 0.0
864 x0
= carx
* cpx
+ sarx
* cpy
865 y0
= -sary
* cpx
+ cary
* cpy
866 x1
= carx
* x
+ sarx
* y
867 y1
= -sary
* x
+ cary
* y
868 d
= (x1
- x0
) * (x1
- x0
) + (y1
- y0
) * (y1
- y0
)
882 xc
= 0.5 * (x0
+ x1
) - sf
* (y1
- y0
)
883 yc
= 0.5 * (y0
+ y1
) + sf
* (x1
- x0
)
884 ang_0
= atan2(y0
- yc
, x0
- xc
)
885 ang_1
= atan2(y1
- yc
, x1
- xc
)
886 ang_arc
= ang_1
- ang_0
888 if ang_arc
< 0.0 and fs
== 1:
890 elif ang_arc
> 0.0 and fs
== 0:
893 n_segs
= int(ceil(abs(ang_arc
* 2.0 / (pi
* 0.5 + 0.001))))
895 if self
._spline
is None:
896 self
._appendPoint
(cpx
, cpy
,
897 handle_left_type
='FREE', handle_left
=(cpx
, cpy
),
898 handle_right_type
='FREE', handle_right
=(cpx
, cpy
))
900 for i
in range(n_segs
):
901 ang0
= ang_0
+ i
* ang_arc
/ n_segs
902 ang1
= ang_0
+ (i
+ 1) * ang_arc
/ n_segs
903 ang_demi
= 0.25 * (ang1
- ang0
)
904 t
= 2.66666 * sin(ang_demi
) * sin(ang_demi
) / sin(ang_demi
* 2.0)
905 x1
= xc
+ cos(ang0
) - t
* sin(ang0
)
906 y1
= yc
+ sin(ang0
) + t
* cos(ang0
)
909 x3
= x2
+ t
* sin(ang1
)
910 y3
= y2
- t
* cos(ang1
)
912 coord1
= ((cos(ang
) * rx
) * x1
+ (-sin(ang
) * ry
) * y1
,
913 (sin(ang
) * rx
) * x1
+ (cos(ang
) * ry
) * y1
)
914 coord2
= ((cos(ang
) * rx
) * x3
+ (-sin(ang
) * ry
) * y3
,
915 (sin(ang
) * rx
) * x3
+ (cos(ang
) * ry
) * y3
)
916 coord3
= ((cos(ang
) * rx
) * x2
+ (-sin(ang
) * ry
) * y2
,
917 (sin(ang
) * rx
) * x2
+ (cos(ang
) * ry
) * y2
)
919 self
._updateHandle
(handle
=coord1
, handle_type
='FREE')
921 self
._appendPoint
(coord3
[0], coord3
[1],
922 handle_left_type
='FREE', handle_left
=coord2
,
923 handle_right_type
='FREE', handle_right
=coord3
)
925 def _pathCurveToA(self
, code
):
927 Elliptical arc CurveTo path command
930 cur
= self
._data
.cur()
932 while cur
is not None and not cur
.isalpha():
933 rx
= float(self
._data
.next())
934 ry
= float(self
._data
.next())
935 ang
= float(self
._data
.next()) / 180 * pi
936 fa
= float(self
._data
.next())
937 fs
= float(self
._data
.next())
938 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
940 self
._calcArc
(rx
, ry
, ang
, fa
, fs
, x
, y
)
944 cur
= self
._data
.cur()
946 def _pathClose(self
, code
):
952 self
._spline
['closed'] = True
954 cv
= self
._spline
['points'][0]
955 self
._point
= (cv
['x'], cv
['y'])
964 while not self
._data
.eof():
965 code
= self
._data
.next()
966 cmd
= self
._commands
.get(code
)
969 raise Exception('Unknown path command: {0}' . format(code
))
971 if cmd
in {'Z', 'z'}:
977 if self
._use
_fill
and not closed
:
980 def getSplines(self
):
982 Get splines definitions
990 Abstract SVG geometry
993 __slots__
= ('_node', # XML node for geometry
994 '_context', # Global SVG context (holds matrices stack, i.e.)
995 '_creating') # Flag if geometry is already creating
997 # need to detect cycles for USE node
999 def __init__(self
, node
, context
):
1001 Initialize SVG geometry
1005 self
._context
= context
1006 self
._creating
= False
1008 if hasattr(node
, 'getAttribute'):
1009 defs
= context
['defines']
1011 attr_id
= node
.getAttribute('id')
1012 if attr_id
and defs
.get('#' + attr_id
) is None:
1013 defs
['#' + attr_id
] = self
1015 className
= node
.getAttribute('class')
1016 if className
and defs
.get(className
) is None:
1017 defs
[className
] = self
1019 def _pushRect(self
, rect
):
1021 Push display rectangle
1024 self
._context
['rects'].append(rect
)
1025 self
._context
['rect'] = rect
1029 Pop display rectangle
1032 self
._context
['rects'].pop()
1033 self
._context
['rect'] = self
._context
['rects'][-1]
1035 def _pushMatrix(self
, matrix
):
1037 Push transformation matrix
1040 self
._context
['transform'].append(matrix
)
1041 self
._context
['matrix'] = self
._context
['matrix'] * matrix
1043 def _popMatrix(self
):
1045 Pop transformation matrix
1048 matrix
= self
._context
['transform'].pop()
1049 self
._context
['matrix'] = self
._context
['matrix'] * matrix
.inverted()
1051 def _pushStyle(self
, style
):
1056 self
._context
['styles'].append(style
)
1057 self
._context
['style'] = style
1059 def _popStyle(self
):
1064 self
._context
['styles'].pop()
1065 self
._context
['style'] = self
._context
['styles'][-1]
1067 def _transformCoord(self
, point
):
1069 Transform SVG-file coords
1072 v
= Vector((point
[0], point
[1], 0.0))
1074 return self
._context
['matrix'] * v
1076 def getNodeMatrix(self
):
1078 Get transformation matrix of node
1081 return SVGMatrixFromNode(self
._node
, self
._context
)
1085 Parse XML node to memory
1090 def _doCreateGeom(self
, instancing
):
1092 Internal handler to create real geometries
1097 def getTransformMatrix(self
):
1099 Get matrix created from "transform" attribute
1102 transform
= self
._node
.getAttribute('transform')
1105 return SVGParseTransform(transform
)
1109 def createGeom(self
, instancing
):
1111 Create real geometries
1117 self
._creating
= True
1119 matrix
= self
.getTransformMatrix()
1120 if matrix
is not None:
1121 self
._pushMatrix
(matrix
)
1123 self
._doCreateGeom
(instancing
)
1125 if matrix
is not None:
1128 self
._creating
= False
1131 class SVGGeometryContainer(SVGGeometry
):
1133 Container of SVG geometries
1136 __slots__
= ('_geometries', # List of chold geometries
1137 '_styles') # Styles, used for displaying
1139 def __init__(self
, node
, context
):
1141 Initialize SVG geometry container
1144 super().__init
__(node
, context
)
1146 self
._geometries
= []
1147 self
._styles
= SVGEmptyStyles
1151 Parse XML node to memory
1154 if type(self
._node
) is xml
.dom
.minidom
.Element
:
1155 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1157 self
._pushStyle
(self
._styles
)
1159 for node
in self
._node
.childNodes
:
1160 if type(node
) is not xml
.dom
.minidom
.Element
:
1163 ob
= parseAbstractNode(node
, self
._context
)
1165 self
._geometries
.append(ob
)
1169 def _doCreateGeom(self
, instancing
):
1171 Create real geometries
1174 for geom
in self
._geometries
:
1175 geom
.createGeom(instancing
)
1177 def getGeometries(self
):
1179 Get list of parsed geometries
1182 return self
._geometries
1185 class SVGGeometryPATH(SVGGeometry
):
1190 __slots__
= ('_splines', # List of splines after parsing
1191 '_styles') # Styles, used for displaying
1193 def __init__(self
, node
, context
):
1198 super().__init
__(node
, context
)
1201 self
._styles
= SVGEmptyStyles
1208 d
= self
._node
.getAttribute('d')
1210 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1212 pathParser
= SVGPathParser(d
, self
._styles
['useFill'])
1215 self
._splines
= pathParser
.getSplines()
1217 def _doCreateGeom(self
, instancing
):
1219 Create real geometries
1222 ob
= SVGCreateCurve()
1225 if self
._node
.getAttribute('id'):
1226 cu
.name
= self
._node
.getAttribute('id')
1228 if self
._styles
['useFill']:
1229 cu
.dimensions
= '2D'
1230 cu
.materials
.append(self
._styles
['fill'])
1232 cu
.dimensions
= '3D'
1234 for spline
in self
._splines
:
1237 if spline
['closed'] and len(spline
['points']) >= 2:
1238 first
= spline
['points'][0]
1239 last
= spline
['points'][-1]
1240 if ( first
['handle_left_type'] == 'FREE' and
1241 last
['handle_right_type'] == 'VECTOR'):
1242 last
['handle_right_type'] = 'FREE'
1243 last
['handle_right'] = (last
['x'], last
['y'])
1244 if ( last
['handle_right_type'] == 'FREE' and
1245 first
['handle_left_type'] == 'VECTOR'):
1246 first
['handle_left_type'] = 'FREE'
1247 first
['handle_left'] = (first
['x'], first
['y'])
1249 for point
in spline
['points']:
1250 co
= self
._transformCoord
((point
['x'], point
['y']))
1252 if act_spline
is None:
1253 cu
.splines
.new('BEZIER')
1255 act_spline
= cu
.splines
[-1]
1256 act_spline
.use_cyclic_u
= spline
['closed']
1258 act_spline
.bezier_points
.add()
1260 bezt
= act_spline
.bezier_points
[-1]
1263 bezt
.handle_left_type
= point
['handle_left_type']
1264 if point
['handle_left'] is not None:
1265 handle
= point
['handle_left']
1266 bezt
.handle_left
= self
._transformCoord
(handle
)
1268 bezt
.handle_right_type
= point
['handle_right_type']
1269 if point
['handle_right'] is not None:
1270 handle
= point
['handle_right']
1271 bezt
.handle_right
= self
._transformCoord
(handle
)
1276 class SVGGeometryDEFS(SVGGeometryContainer
):
1278 Container for referenced elements
1281 def createGeom(self
, instancing
):
1283 Create real geometries
1289 class SVGGeometrySYMBOL(SVGGeometryContainer
):
1294 def _doCreateGeom(self
, instancing
):
1296 Create real geometries
1299 self
._pushMatrix
(self
.getNodeMatrix())
1301 super()._doCreateGeom
(False)
1305 def createGeom(self
, instancing
):
1307 Create real geometries
1313 super().createGeom(instancing
)
1316 class SVGGeometryG(SVGGeometryContainer
):
1324 class SVGGeometryUSE(SVGGeometry
):
1326 User of referenced elements
1329 def _doCreateGeom(self
, instancing
):
1331 Create real geometries
1334 ref
= self
._node
.getAttribute('xlink:href')
1335 geom
= self
._context
['defines'].get(ref
)
1337 if geom
is not None:
1338 rect
= SVGRectFromNode(self
._node
, self
._context
)
1339 self
._pushRect
(rect
)
1341 self
._pushMatrix
(self
.getNodeMatrix())
1343 geom
.createGeom(True)
1350 class SVGGeometryRECT(SVGGeometry
):
1355 __slots__
= ('_rect', # coordinate and dimensions of rectangle
1356 '_radius', # Rounded corner radiuses
1357 '_styles') # Styles, used for displaying
1359 def __init__(self
, node
, context
):
1361 Initialize new rectangle
1364 super().__init
__(node
, context
)
1366 self
._rect
= ('0', '0', '0', '0')
1367 self
._radius
= ('0', '0')
1368 self
._styles
= SVGEmptyStyles
1372 Parse SVG rectangle node
1375 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1378 for attr
in ['x', 'y', 'width', 'height']:
1379 val
= self
._node
.getAttribute(attr
)
1380 rect
.append(val
or '0')
1384 rx
= self
._node
.getAttribute('rx')
1385 ry
= self
._node
.getAttribute('ry')
1387 self
._radius
= (rx
, ry
)
1389 def _appendCorner(self
, spline
, coord
, firstTime
, rounded
):
1391 Append new corner to rectangle
1396 handle
= self
._transformCoord
(coord
[2])
1397 coord
= (coord
[0], coord
[1])
1399 co
= self
._transformCoord
(coord
)
1402 spline
.bezier_points
.add()
1404 bezt
= spline
.bezier_points
[-1]
1409 bezt
.handle_left_type
= 'VECTOR'
1410 bezt
.handle_right_type
= 'FREE'
1412 bezt
.handle_right
= handle
1414 bezt
.handle_left_type
= 'FREE'
1415 bezt
.handle_right_type
= 'VECTOR'
1416 bezt
.handle_left
= co
1419 bezt
.handle_left_type
= 'VECTOR'
1420 bezt
.handle_right_type
= 'VECTOR'
1422 def _doCreateGeom(self
, instancing
):
1424 Create real geometries
1427 # Run-time parsing -- percents would be correct only if
1429 crect
= self
._context
['rect']
1433 rect
.append(SVGParseCoord(self
._rect
[i
], crect
[i
% 2]))
1439 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1440 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1442 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1443 ry
= min(rx
, rect
[3] / 2)
1444 rx
= ry
= min(rx
, ry
)
1446 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1447 rx
= min(ry
, rect
[2] / 2)
1448 rx
= ry
= min(rx
, ry
)
1453 ob
= SVGCreateCurve()
1456 if self
._styles
['useFill']:
1457 cu
.dimensions
= '2D'
1458 cu
.materials
.append(self
._styles
['fill'])
1460 cu
.dimensions
= '3D'
1462 cu
.splines
.new('BEZIER')
1464 spline
= cu
.splines
[-1]
1465 spline
.use_cyclic_u
= True
1467 x
, y
= rect
[0], rect
[1]
1468 w
, h
= rect
[2], rect
[3]
1469 rx
, ry
= radius
[0], radius
[1]
1486 # Optional third component -- right handle coord
1487 coords
= [(x
+ rx
, y
),
1488 (x
+ w
- rx
, y
, (x
+ w
, y
)),
1490 (x
+ w
, y
+ h
- ry
, (x
+ w
, y
+ h
)),
1491 (x
+ w
- rx
, y
+ h
),
1492 (x
+ rx
, y
+ h
, (x
, y
+ h
)),
1494 (x
, y
+ ry
, (x
, y
))]
1498 coords
= [(x
, y
), (x
+ w
, y
), (x
+ w
, y
+ h
), (x
, y
+ h
)]
1501 for coord
in coords
:
1502 self
._appendCorner
(spline
, coord
, firstTime
, rounded
)
1508 class SVGGeometryELLIPSE(SVGGeometry
):
1513 __slots__
= ('_cx', # X-coordinate of center
1514 '_cy', # Y-coordinate of center
1515 '_rx', # X-axis radius of circle
1516 '_ry', # Y-axis radius of circle
1517 '_styles') # Styles, used for displaying
1519 def __init__(self
, node
, context
):
1521 Initialize new ellipse
1524 super().__init
__(node
, context
)
1530 self
._styles
= SVGEmptyStyles
1534 Parse SVG ellipse node
1537 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1539 self
._cx
= self
._node
.getAttribute('cx') or '0'
1540 self
._cy
= self
._node
.getAttribute('cy') or '0'
1541 self
._rx
= self
._node
.getAttribute('rx') or '0'
1542 self
._ry
= self
._node
.getAttribute('ry') or '0'
1544 def _doCreateGeom(self
, instancing
):
1546 Create real geometries
1549 # Run-time parsing -- percents would be correct only if
1551 crect
= self
._context
['rect']
1553 cx
= SVGParseCoord(self
._cx
, crect
[0])
1554 cy
= SVGParseCoord(self
._cy
, crect
[1])
1555 rx
= SVGParseCoord(self
._rx
, crect
[0])
1556 ry
= SVGParseCoord(self
._ry
, crect
[1])
1558 if not rx
or not ry
:
1559 # Automaic handles will work incorrect in this case
1563 ob
= SVGCreateCurve()
1566 if self
._node
.getAttribute('id'):
1567 cu
.name
= self
._node
.getAttribute('id')
1569 if self
._styles
['useFill']:
1570 cu
.dimensions
= '2D'
1571 cu
.materials
.append(self
._styles
['fill'])
1573 cu
.dimensions
= '3D'
1575 coords
= [((cx
- rx
, cy
),
1576 (cx
- rx
, cy
+ ry
* 0.552),
1577 (cx
- rx
, cy
- ry
* 0.552)),
1580 (cx
- rx
* 0.552, cy
- ry
),
1581 (cx
+ rx
* 0.552, cy
- ry
)),
1584 (cx
+ rx
, cy
- ry
* 0.552),
1585 (cx
+ rx
, cy
+ ry
* 0.552)),
1588 (cx
+ rx
* 0.552, cy
+ ry
),
1589 (cx
- rx
* 0.552, cy
+ ry
))]
1592 for coord
in coords
:
1593 co
= self
._transformCoord
(coord
[0])
1594 handle_left
= self
._transformCoord
(coord
[1])
1595 handle_right
= self
._transformCoord
(coord
[2])
1598 cu
.splines
.new('BEZIER')
1599 spline
= cu
.splines
[-1]
1600 spline
.use_cyclic_u
= True
1602 spline
.bezier_points
.add()
1604 bezt
= spline
.bezier_points
[-1]
1606 bezt
.handle_left_type
= 'FREE'
1607 bezt
.handle_right_type
= 'FREE'
1608 bezt
.handle_left
= handle_left
1609 bezt
.handle_right
= handle_right
1614 class SVGGeometryCIRCLE(SVGGeometryELLIPSE
):
1621 Parse SVG circle node
1624 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1626 self
._cx
= self
._node
.getAttribute('cx') or '0'
1627 self
._cy
= self
._node
.getAttribute('cy') or '0'
1629 r
= self
._node
.getAttribute('r') or '0'
1630 self
._rx
= self
._ry
= r
1633 class SVGGeometryLINE(SVGGeometry
):
1638 __slots__
= ('_x1', # X-coordinate of beginning
1639 '_y1', # Y-coordinate of beginning
1640 '_x2', # X-coordinate of ending
1641 '_y2') # Y-coordinate of ending
1643 def __init__(self
, node
, context
):
1648 super().__init
__(node
, context
)
1660 self
._x
1 = self
._node
.getAttribute('x1') or '0'
1661 self
._y
1 = self
._node
.getAttribute('y1') or '0'
1662 self
._x
2 = self
._node
.getAttribute('x2') or '0'
1663 self
._y
2 = self
._node
.getAttribute('y2') or '0'
1665 def _doCreateGeom(self
, instancing
):
1667 Create real geometries
1670 # Run-time parsing -- percents would be correct only if
1672 crect
= self
._context
['rect']
1674 x1
= SVGParseCoord(self
._x
1, crect
[0])
1675 y1
= SVGParseCoord(self
._y
1, crect
[1])
1676 x2
= SVGParseCoord(self
._x
2, crect
[0])
1677 y2
= SVGParseCoord(self
._y
2, crect
[1])
1680 ob
= SVGCreateCurve()
1683 coords
= [(x1
, y1
), (x2
, y2
)]
1686 for coord
in coords
:
1687 co
= self
._transformCoord
(coord
)
1690 cu
.splines
.new('BEZIER')
1691 spline
= cu
.splines
[-1]
1692 spline
.use_cyclic_u
= True
1694 spline
.bezier_points
.add()
1696 bezt
= spline
.bezier_points
[-1]
1698 bezt
.handle_left_type
= 'VECTOR'
1699 bezt
.handle_right_type
= 'VECTOR'
1704 class SVGGeometryPOLY(SVGGeometry
):
1706 Abstract class for handling poly-geometries
1707 (polylines and polygons)
1710 __slots__
= ('_points', # Array of points for poly geometry
1711 '_styles', # Styles, used for displaying
1712 '_closed') # Should generated curve be closed?
1714 def __init__(self
, node
, context
):
1716 Initialize new poly geometry
1719 super().__init
__(node
, context
)
1722 self
._styles
= SVGEmptyStyles
1723 self
._closed
= False
1730 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1732 points
= self
._node
.getAttribute('points')
1733 points
= points
.replace(',', ' ').replace('-', ' -')
1734 points
= points
.split()
1743 self
._points
.append((float(prev
), float(p
)))
1746 def _doCreateGeom(self
, instancing
):
1748 Create real geometries
1751 ob
= SVGCreateCurve()
1754 if self
._closed
and self
._styles
['useFill']:
1755 cu
.dimensions
= '2D'
1756 cu
.materials
.append(self
._styles
['fill'])
1758 cu
.dimensions
= '3D'
1762 for point
in self
._points
:
1763 co
= self
._transformCoord
(point
)
1766 cu
.splines
.new('BEZIER')
1767 spline
= cu
.splines
[-1]
1768 spline
.use_cyclic_u
= self
._closed
1770 spline
.bezier_points
.add()
1772 bezt
= spline
.bezier_points
[-1]
1774 bezt
.handle_left_type
= 'VECTOR'
1775 bezt
.handle_right_type
= 'VECTOR'
1780 class SVGGeometryPOLYLINE(SVGGeometryPOLY
):
1782 SVG polyline geometry
1788 class SVGGeometryPOLYGON(SVGGeometryPOLY
):
1790 SVG polygon geometry
1793 def __init__(self
, node
, context
):
1795 Initialize new polygon geometry
1798 super().__init
__(node
, context
)
1803 class SVGGeometrySVG(SVGGeometryContainer
):
1805 Main geometry holder
1808 def _doCreateGeom(self
, instancing
):
1810 Create real geometries
1813 rect
= SVGRectFromNode(self
._node
, self
._context
)
1815 matrix
= self
.getNodeMatrix()
1817 # Better Inkscape compatibility: match document origin with
1819 if self
._node
.getAttribute('inkscape:version'):
1820 raw_height
= self
._node
.getAttribute('height')
1821 document_height
= SVGParseCoord(raw_height
, 1.0)
1822 matrix
= matrix
* Matrix
.Translation([0.0, -document_height
, 0.0])
1824 self
._pushMatrix
(matrix
)
1825 self
._pushRect
(rect
)
1827 super()._doCreateGeom
(False)
1833 class SVGLoader(SVGGeometryContainer
):
1838 def getTransformMatrix(self
):
1840 Get matrix created from "transform" attribute
1843 # SVG document doesn't support transform specification
1844 # it can't even hold attributes
1848 def __init__(self
, filepath
, do_colormanage
):
1850 Initialize SVG loader
1853 node
= xml
.dom
.minidom
.parse(filepath
)
1856 m
= m
* Matrix
.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
1857 m
= m
* Matrix
.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
1861 self
._context
= {'defines': {},
1869 'do_colormanage': do_colormanage
}
1871 super().__init
__(node
, self
._context
)
1874 svgGeometryClasses
= {
1875 'svg': SVGGeometrySVG
,
1876 'path': SVGGeometryPATH
,
1877 'defs': SVGGeometryDEFS
,
1878 'symbol': SVGGeometrySYMBOL
,
1879 'use': SVGGeometryUSE
,
1880 'rect': SVGGeometryRECT
,
1881 'ellipse': SVGGeometryELLIPSE
,
1882 'circle': SVGGeometryCIRCLE
,
1883 'line': SVGGeometryLINE
,
1884 'polyline': SVGGeometryPOLYLINE
,
1885 'polygon': SVGGeometryPOLYGON
,
1889 def parseAbstractNode(node
, context
):
1890 name
= node
.tagName
.lower()
1892 if name
.startswith('svg:'):
1895 geomClass
= svgGeometryClasses
.get(name
)
1897 if geomClass
is not None:
1898 ob
= geomClass(node
, context
)
1906 def load_svg(filepath
, do_colormanage
):
1908 Load specified SVG file
1911 if bpy
.ops
.object.mode_set
.poll():
1912 bpy
.ops
.object.mode_set(mode
='OBJECT')
1914 loader
= SVGLoader(filepath
, do_colormanage
)
1916 loader
.createGeom(False)
1919 def load(operator
, context
, filepath
=""):
1921 # error in code should raise exceptions but loading
1922 # non SVG files can give useful messages.
1923 do_colormanage
= context
.scene
.display_settings
.display_device
!= 'NONE'
1925 load_svg(filepath
, do_colormanage
)
1926 except (xml
.parsers
.expat
.ExpatError
, UnicodeEncodeError) as e
:
1928 traceback
.print_exc()
1930 operator
.report({'WARNING'}, "Unable to parse XML, %s:%s for file %r" % (type(e
).__name
__, e
, filepath
))
1931 return {'CANCELLED'}