1 # SPDX-License-Identifier: GPL-2.0-or-later
7 from math
import cos
, sin
, tan
, atan2
, pi
, ceil
10 from mathutils
import Vector
, Matrix
12 from . import svg_colors
13 from .svg_util
import (units
,
16 parse_array_of_floats
,
19 #### Common utilities ####
21 SVGEmptyStyles
= {'useFill': None,
25 def SVGCreateCurve(context
):
27 Create new curve object to hold splines in
30 cu
= bpy
.data
.curves
.new("Curve", 'CURVE')
31 obj
= bpy
.data
.objects
.new("Curve", cu
)
33 context
['collection'].objects
.link(obj
)
46 def SVGFlipHandle(x
, y
, x1
, y1
):
48 Flip handle around base point
57 def SVGParseCoord(coord
, size
):
59 Parse coordinate component to common basis
61 Needed to handle coordinates set in cm, mm, inches.
64 token
, last_char
= read_float(coord
)
66 unit
= coord
[last_char
:].strip() # strip() in case there is a space
69 return float(size
) / 100.0 * val
70 return val
* units
[unit
]
73 def SVGRectFromNode(node
, context
):
75 Get display rectangle from node
78 w
= context
['rect'][0]
79 h
= context
['rect'][1]
81 if node
.getAttribute('viewBox'):
82 viewBox
= node
.getAttribute('viewBox').replace(',', ' ').split()
83 w
= SVGParseCoord(viewBox
[2], w
)
84 h
= SVGParseCoord(viewBox
[3], h
)
86 if node
.getAttribute('width'):
87 w
= SVGParseCoord(node
.getAttribute('width'), w
)
89 if node
.getAttribute('height'):
90 h
= SVGParseCoord(node
.getAttribute('height'), h
)
95 def SVGMatrixFromNode(node
, context
):
97 Get transformation matrix from given node
100 tagName
= node
.tagName
.lower()
101 tags
= ['svg:svg', 'svg:use', 'svg:symbol']
103 if tagName
not in tags
and 'svg:' + tagName
not in tags
:
106 rect
= context
['rect']
107 has_user_coordinate
= (len(context
['rects']) > 1)
110 x
= SVGParseCoord(node
.getAttribute('x') or '0', rect
[0])
111 y
= SVGParseCoord(node
.getAttribute('y') or '0', rect
[1])
112 w
= SVGParseCoord(node
.getAttribute('width') or str(rect
[0]), rect
[0])
113 h
= SVGParseCoord(node
.getAttribute('height') or str(rect
[1]), rect
[1])
115 m
= Matrix
.Translation(Vector((x
, y
, 0.0)))
116 if has_user_coordinate
:
117 if rect
[0] != 0 and rect
[1] != 0:
118 m
= m
@ Matrix
.Scale(w
/ rect
[0], 4, Vector((1.0, 0.0, 0.0)))
119 m
= m
@ Matrix
.Scale(h
/ rect
[1], 4, Vector((0.0, 1.0, 0.0)))
121 if node
.getAttribute('viewBox'):
122 viewBox
= node
.getAttribute('viewBox').replace(',', ' ').split()
123 vx
= SVGParseCoord(viewBox
[0], w
)
124 vy
= SVGParseCoord(viewBox
[1], h
)
125 vw
= SVGParseCoord(viewBox
[2], w
)
126 vh
= SVGParseCoord(viewBox
[3], h
)
128 if vw
== 0 or vh
== 0:
131 if has_user_coordinate
or (w
!= 0 and h
!= 0):
140 tx
= (w
- vw
* scale
) / 2
141 ty
= (h
- vh
* scale
) / 2
142 m
= m
@ Matrix
.Translation(Vector((tx
, ty
, 0.0)))
144 m
= m
@ Matrix
.Translation(Vector((-vx
, -vy
, 0.0)))
145 m
= m
@ Matrix
.Scale(scale
, 4, Vector((1.0, 0.0, 0.0)))
146 m
= m
@ Matrix
.Scale(scale
, 4, Vector((0.0, 1.0, 0.0)))
151 def SVGParseTransform(transform
):
153 Parse transform string and return transformation matrix
157 r
= re
.compile(r
'\s*([A-z]+)\s*\((.*?)\)')
159 for match
in r
.finditer(transform
):
160 func
= match
.group(1)
161 params
= match
.group(2)
162 params
= params
.replace(',', ' ').split()
164 proc
= SVGTransforms
.get(func
)
166 raise Exception('Unknown trasnform function: ' + func
)
173 def SVGGetMaterial(color
, context
):
175 Get material for specified color
178 materials
= context
['materials']
179 rgb_re
= re
.compile(r
'^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
181 if color
in materials
:
182 return materials
[color
]
185 if color
.startswith('#'):
189 color
= color
[0] * 2 + color
[1] * 2 + color
[2] * 2
191 diff
= (int(color
[0:2], 16), int(color
[2:4], 16), int(color
[4:6], 16))
192 elif color
in svg_colors
.SVGColors
:
193 diff
= svg_colors
.SVGColors
[color
]
194 elif rgb_re
.match(color
):
195 c
= rgb_re
.findall(color
)[0]
196 diff
= (float(c
[0]), float(c
[1]), float(c
[2]))
200 diffuse_color
= ([x
/ 255.0 for x
in diff
])
202 if context
['do_colormanage']:
203 diffuse_color
[0] = srgb_to_linearrgb(diffuse_color
[0])
204 diffuse_color
[1] = srgb_to_linearrgb(diffuse_color
[1])
205 diffuse_color
[2] = srgb_to_linearrgb(diffuse_color
[2])
207 mat
= bpy
.data
.materials
.new(name
='SVGMat')
208 mat
.diffuse_color
= (*diffuse_color
, 1.0)
210 materials
[color
] = mat
215 def SVGTransformTranslate(params
):
217 translate SVG transform command
220 tx
= float(params
[0])
221 ty
= float(params
[1]) if len(params
) > 1 else 0.0
223 return Matrix
.Translation(Vector((tx
, ty
, 0.0)))
226 def SVGTransformMatrix(params
):
228 matrix SVG transform command
238 return Matrix(((a
, c
, 0.0, e
),
244 def SVGTransformScale(params
):
246 scale SVG transform command
249 sx
= float(params
[0])
250 sy
= float(params
[1]) if len(params
) > 1 else sx
254 m
= m
@ Matrix
.Scale(sx
, 4, Vector((1.0, 0.0, 0.0)))
255 m
= m
@ Matrix
.Scale(sy
, 4, Vector((0.0, 1.0, 0.0)))
260 def SVGTransformSkewY(params
):
262 skewY SVG transform command
265 ang
= float(params
[0]) * pi
/ 180.0
267 return Matrix(((1.0, 0.0, 0.0),
268 (tan(ang
), 1.0, 0.0),
269 (0.0, 0.0, 1.0))).to_4x4()
272 def SVGTransformSkewX(params
):
274 skewX SVG transform command
277 ang
= float(params
[0]) * pi
/ 180.0
279 return Matrix(((1.0, tan(ang
), 0.0),
281 (0.0, 0.0, 1.0))).to_4x4()
284 def SVGTransformRotate(params
):
286 skewX SVG transform command
289 ang
= float(params
[0]) * pi
/ 180.0
293 cx
= float(params
[1])
294 cy
= float(params
[2])
296 tm
= Matrix
.Translation(Vector((cx
, cy
, 0.0)))
297 rm
= Matrix
.Rotation(ang
, 4, Vector((0.0, 0.0, 1.0)))
299 return tm
@ rm
@ tm
.inverted()
301 SVGTransforms
= {'translate': SVGTransformTranslate
,
302 'scale': SVGTransformScale
,
303 'skewX': SVGTransformSkewX
,
304 'skewY': SVGTransformSkewY
,
305 'matrix': SVGTransformMatrix
,
306 'rotate': SVGTransformRotate
}
309 def SVGParseStyles(node
, context
):
311 Parse node to get different styles for displaying geometries
312 (materials, filling flags, etc..)
315 styles
= SVGEmptyStyles
.copy()
317 style
= node
.getAttribute('style')
319 elems
= style
.split(';')
326 name
= s
[0].strip().lower()
332 styles
['useFill'] = False
334 styles
['useFill'] = True
335 styles
['fill'] = SVGGetMaterial(val
, context
)
337 if styles
['useFill'] is None:
338 styles
['useFill'] = True
339 styles
['fill'] = SVGGetMaterial('#000', context
)
343 if styles
['useFill'] is None:
344 fill
= node
.getAttribute('fill')
348 styles
['useFill'] = False
350 styles
['useFill'] = True
351 styles
['fill'] = SVGGetMaterial(fill
, context
)
353 if styles
['useFill'] is None and context
['style']:
354 styles
= context
['style'].copy()
356 if styles
['useFill'] is None:
357 styles
['useFill'] = True
358 styles
['fill'] = SVGGetMaterial('#000', context
)
362 def id_names_from_node(node
, ob
):
363 if node
.getAttribute('id'):
364 name
= node
.getAttribute('id')
368 #### SVG path helpers ####
373 SVG Path data token supplier
376 __slots__
= ('_data', # List of tokens
377 '_index', # Index of current token in tokens list
378 '_len') # Length of tokens list
380 def __init__(self
, d
):
382 Initialize new path data supplier
384 d - the definition of the outline of a shape
388 commands
= {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
399 elif c
.lower() in commands
:
403 elif c
in ['-', '.'] or c
.isdigit():
404 # Special case for 'a/A' commands.
405 # Arguments 4 and 5 are either 0 or 1 and might not
406 # be separated from the next argument with space or comma.
407 if current_command
.lower() == 'a':
408 if arg_index
% 7 in [4,5]:
412 token
, last_char
= read_float(d
, i
)
414 token
, last_char
= read_float(d
, i
)
419 # in most cases len(token) and (last_char - i) are the same
420 # but with whitespace or ',' prefix they are not.
422 i
+= (last_char
- i
) - 1
428 self
._len
= len(tokens
)
432 Check if end of data reached
435 return self
._index
>= self
._len
445 return self
._data
[self
._index
]
447 def lookupNext(self
):
449 get next token without moving pointer
455 return self
._data
[self
._index
]
459 Return current token and go to next one
465 token
= self
._data
[self
._index
]
472 Return coordinate created from current token and move to next token
485 Parser of SVG path data
488 __slots__
= ('_data', # Path data supplird
489 '_point', # Current point coorfinate
490 '_handle', # Last handle coordinate
491 '_splines', # List of all splies created during parsing
492 '_spline', # Currently handling spline
493 '_commands', # Hash of all supported path commands
494 '_use_fill', # Splines would be filled, so expected to be closed
497 def __init__(self
, d
, use_fill
):
499 Initialize path parser
501 d - the definition of the outline of a shape
504 self
._data
= SVGPathData(d
)
505 self
._point
= None # Current point
506 self
._handle
= None # Last handle
507 self
._splines
= [] # List of splines in path
508 self
._spline
= None # Current spline
509 self
._use
_fill
= use_fill
511 self
._commands
= {'M': self
._pathMoveTo
,
512 'L': self
._pathLineTo
,
513 'H': self
._pathLineTo
,
514 'V': self
._pathLineTo
,
515 'C': self
._pathCurveToCS
,
516 'S': self
._pathCurveToCS
,
517 'Q': self
._pathCurveToQT
,
518 'T': self
._pathCurveToQT
,
519 'A': self
._pathCurveToA
,
520 'Z': self
._pathClose
,
522 'm': self
._pathMoveTo
,
523 'l': self
._pathLineTo
,
524 'h': self
._pathLineTo
,
525 'v': self
._pathLineTo
,
526 'c': self
._pathCurveToCS
,
527 's': self
._pathCurveToCS
,
528 'q': self
._pathCurveToQT
,
529 't': self
._pathCurveToQT
,
530 'a': self
._pathCurveToA
,
531 'z': self
._pathClose
}
533 def _getCoordPair(self
, relative
, point
):
535 Get next coordinate pair
538 x
= self
._data
.nextCoord()
539 y
= self
._data
.nextCoord()
541 if relative
and point
is not None:
547 def _appendPoint(self
, x
, y
, handle_left
=None, handle_left_type
='VECTOR',
548 handle_right
=None, handle_right_type
='VECTOR'):
550 Append point to spline
552 If there's no active spline, create one and set it's first point
553 to current point coordinate
556 if self
._spline
is None:
557 self
._spline
= {'points': [],
560 self
._splines
.append(self
._spline
)
562 if len(self
._spline
['points']) > 0:
563 # Not sure about specifications, but Illustrator could create
564 # last point at the same position, as start point (which was
565 # reached by MoveTo command) to set needed handle coords.
566 # It's also could use last point at last position to make path
569 first
= self
._spline
['points'][0]
570 if check_points_equal((first
['x'], first
['y']), (x
, y
)):
571 if handle_left
is not None:
572 first
['handle_left'] = handle_left
573 first
['handle_left_type'] = 'FREE'
575 if handle_left_type
!= 'VECTOR':
576 first
['handle_left_type'] = handle_left_type
578 if self
._data
.eof() or self
._data
.lookupNext().lower() == 'm':
579 self
._spline
['closed'] = True
583 last
= self
._spline
['points'][-1]
584 if last
['handle_right_type'] == 'VECTOR' and handle_left_type
== 'FREE':
585 last
['handle_right'] = (last
['x'], last
['y'])
586 last
['handle_right_type'] = 'FREE'
587 if last
['handle_right_type'] == 'FREE' and handle_left_type
== 'VECTOR':
589 handle_left_type
= 'FREE'
594 'handle_left': handle_left
,
595 'handle_left_type': handle_left_type
,
597 'handle_right': handle_right
,
598 'handle_right_type': handle_right_type
}
600 self
._spline
['points'].append(point
)
602 def _updateHandle(self
, handle
=None, handle_type
=None):
604 Update right handle of previous point when adding new point to spline
607 point
= self
._spline
['points'][-1]
609 if handle_type
is not None:
610 point
['handle_right_type'] = handle_type
612 if handle
is not None:
613 point
['handle_right'] = handle
615 def _pathMoveTo(self
, code
):
620 relative
= code
.islower()
621 x
, y
= self
._getCoordPair
(relative
, self
._point
)
623 self
._spline
= None # Flag to start new spline
626 cur
= self
._data
.cur()
627 while cur
is not None and not cur
.isalpha():
628 x
, y
= self
._getCoordPair
(relative
, self
._point
)
630 if self
._spline
is None:
631 self
._appendPoint
(self
._point
[0], self
._point
[1])
633 self
._appendPoint
(x
, y
)
636 cur
= self
._data
.cur()
640 def _pathLineTo(self
, code
):
647 cur
= self
._data
.cur()
648 while cur
is not None and not cur
.isalpha():
650 x
, y
= self
._getCoordPair
(code
== 'l', self
._point
)
652 x
= self
._data
.nextCoord()
656 y
= self
._data
.nextCoord()
663 if self
._spline
is None:
664 self
._appendPoint
(self
._point
[0], self
._point
[1])
666 self
._appendPoint
(x
, y
)
669 cur
= self
._data
.cur()
673 def _pathCurveToCS(self
, code
):
675 Cubic BEZIER CurveTo path command
679 cur
= self
._data
.cur()
680 while cur
is not None and not cur
.isalpha():
682 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
683 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
685 if self
._handle
is not None:
686 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
687 self
._handle
[0], self
._handle
[1])
691 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
693 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
695 if self
._spline
is None:
696 self
._appendPoint
(self
._point
[0], self
._point
[1],
697 handle_left_type
='FREE', handle_left
=self
._point
,
698 handle_right_type
='FREE', handle_right
=(x1
, y1
))
700 self
._updateHandle
(handle
=(x1
, y1
), handle_type
='FREE')
702 self
._appendPoint
(x
, y
,
703 handle_left_type
='FREE', handle_left
=(x2
, y2
),
704 handle_right_type
='FREE', handle_right
=(x
, y
))
707 self
._handle
= (x2
, y2
)
708 cur
= self
._data
.cur()
710 def _pathCurveToQT(self
, code
):
712 Quadratic BEZIER CurveTo path command
716 cur
= self
._data
.cur()
718 while cur
is not None and not cur
.isalpha():
720 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
722 if self
._handle
is not None:
723 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
724 self
._handle
[0], self
._handle
[1])
728 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
730 if not check_points_equal((x
, y
), self
._point
):
731 if self
._spline
is None:
732 self
._appendPoint
(self
._point
[0], self
._point
[1],
733 handle_left_type
='FREE', handle_left
=self
._point
,
734 handle_right_type
='FREE', handle_right
=self
._point
)
736 self
._appendPoint
(x
, y
,
737 handle_left_type
='FREE', handle_left
=(x1
, y1
),
738 handle_right_type
='FREE', handle_right
=(x
, y
))
741 self
._handle
= (x1
, y1
)
742 cur
= self
._data
.cur()
744 def _calcArc(self
, rx
, ry
, ang
, fa
, fs
, x
, y
):
748 Copied and adoptedfrom paths_svg2obj.py script for Blender 2.49
749 which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
756 px
= abs((cos(ang
) * (cpx
- x
) + sin(ang
) * (cpy
- y
)) * 0.5) ** 2.0
757 py
= abs((cos(ang
) * (cpy
- y
) - sin(ang
) * (cpx
- x
)) * 0.5) ** 2.0
761 px
= px
/ (rx
** 2.0)
764 rpy
= py
/ (ry
** 2.0)
772 carx
= sarx
= cary
= sary
= 0.0
782 x0
= carx
* cpx
+ sarx
* cpy
783 y0
= -sary
* cpx
+ cary
* cpy
784 x1
= carx
* x
+ sarx
* y
785 y1
= -sary
* x
+ cary
* y
786 d
= (x1
- x0
) * (x1
- x0
) + (y1
- y0
) * (y1
- y0
)
800 xc
= 0.5 * (x0
+ x1
) - sf
* (y1
- y0
)
801 yc
= 0.5 * (y0
+ y1
) + sf
* (x1
- x0
)
802 ang_0
= atan2(y0
- yc
, x0
- xc
)
803 ang_1
= atan2(y1
- yc
, x1
- xc
)
804 ang_arc
= ang_1
- ang_0
806 if ang_arc
< 0.0 and fs
== 1:
808 elif ang_arc
> 0.0 and fs
== 0:
811 n_segs
= int(ceil(abs(ang_arc
* 2.0 / (pi
* 0.5 + 0.001))))
813 if self
._spline
is None:
814 self
._appendPoint
(cpx
, cpy
,
815 handle_left_type
='FREE', handle_left
=(cpx
, cpy
),
816 handle_right_type
='FREE', handle_right
=(cpx
, cpy
))
818 for i
in range(n_segs
):
819 ang0
= ang_0
+ i
* ang_arc
/ n_segs
820 ang1
= ang_0
+ (i
+ 1) * ang_arc
/ n_segs
821 ang_demi
= 0.25 * (ang1
- ang0
)
822 t
= 2.66666 * sin(ang_demi
) * sin(ang_demi
) / sin(ang_demi
* 2.0)
823 x1
= xc
+ cos(ang0
) - t
* sin(ang0
)
824 y1
= yc
+ sin(ang0
) + t
* cos(ang0
)
827 x3
= x2
+ t
* sin(ang1
)
828 y3
= y2
- t
* cos(ang1
)
830 coord1
= ((cos(ang
) * rx
) * x1
+ (-sin(ang
) * ry
) * y1
,
831 (sin(ang
) * rx
) * x1
+ (cos(ang
) * ry
) * y1
)
832 coord2
= ((cos(ang
) * rx
) * x3
+ (-sin(ang
) * ry
) * y3
,
833 (sin(ang
) * rx
) * x3
+ (cos(ang
) * ry
) * y3
)
834 coord3
= ((cos(ang
) * rx
) * x2
+ (-sin(ang
) * ry
) * y2
,
835 (sin(ang
) * rx
) * x2
+ (cos(ang
) * ry
) * y2
)
837 self
._updateHandle
(handle
=coord1
, handle_type
='FREE')
839 self
._appendPoint
(coord3
[0], coord3
[1],
840 handle_left_type
='FREE', handle_left
=coord2
,
841 handle_right_type
='FREE', handle_right
=coord3
)
843 def _pathCurveToA(self
, code
):
845 Elliptical arc CurveTo path command
848 cur
= self
._data
.cur()
850 while cur
is not None and not cur
.isalpha():
851 rx
= float(self
._data
.next())
852 ry
= float(self
._data
.next())
853 ang
= float(self
._data
.next()) / 180 * pi
854 fa
= float(self
._data
.next())
855 fs
= float(self
._data
.next())
856 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
858 self
._calcArc
(rx
, ry
, ang
, fa
, fs
, x
, y
)
862 cur
= self
._data
.cur()
864 def _pathClose(self
, code
):
870 self
._spline
['closed'] = True
872 cv
= self
._spline
['points'][0]
873 self
._point
= (cv
['x'], cv
['y'])
882 while not self
._data
.eof():
883 code
= self
._data
.next()
884 cmd
= self
._commands
.get(code
)
887 raise Exception('Unknown path command: {0}' . format(code
))
889 if cmd
in {'Z', 'z'}:
895 if self
._use
_fill
and not closed
:
898 def getSplines(self
):
900 Get splines definitions
908 Abstract SVG geometry
911 __slots__
= ('_node', # XML node for geometry
912 '_context', # Global SVG context (holds matrices stack, i.e.)
913 '_creating') # Flag if geometry is already creating
915 # need to detect cycles for USE node
917 def __init__(self
, node
, context
):
919 Initialize SVG geometry
923 self
._context
= context
924 self
._creating
= False
926 if hasattr(node
, 'getAttribute'):
927 defs
= context
['defines']
929 attr_id
= node
.getAttribute('id')
930 if attr_id
and defs
.get('#' + attr_id
) is None:
931 defs
['#' + attr_id
] = self
933 className
= node
.getAttribute('class')
934 if className
and defs
.get(className
) is None:
935 defs
[className
] = self
937 def _pushRect(self
, rect
):
939 Push display rectangle
942 self
._context
['rects'].append(rect
)
943 self
._context
['rect'] = rect
947 Pop display rectangle
950 self
._context
['rects'].pop()
951 self
._context
['rect'] = self
._context
['rects'][-1]
953 def _pushMatrix(self
, matrix
):
955 Push transformation matrix
958 self
._context
['transform'].append(matrix
)
959 self
._context
['matrix'] = self
._context
['matrix'] @ matrix
961 def _popMatrix(self
):
963 Pop transformation matrix
966 matrix
= self
._context
['transform'].pop()
967 self
._context
['matrix'] = self
._context
['matrix'] @ matrix
.inverted()
969 def _pushStyle(self
, style
):
974 self
._context
['styles'].append(style
)
975 self
._context
['style'] = style
982 self
._context
['styles'].pop()
983 self
._context
['style'] = self
._context
['styles'][-1]
985 def _transformCoord(self
, point
):
987 Transform SVG-file coords
990 v
= Vector((point
[0], point
[1], 0.0))
992 return self
._context
['matrix'] @ v
994 def getNodeMatrix(self
):
996 Get transformation matrix of node
999 return SVGMatrixFromNode(self
._node
, self
._context
)
1003 Parse XML node to memory
1008 def _doCreateGeom(self
, instancing
):
1010 Internal handler to create real geometries
1015 def getTransformMatrix(self
):
1017 Get matrix created from "transform" attribute
1020 transform
= self
._node
.getAttribute('transform')
1023 return SVGParseTransform(transform
)
1027 def createGeom(self
, instancing
):
1029 Create real geometries
1035 self
._creating
= True
1037 matrix
= self
.getTransformMatrix()
1038 if matrix
is not None:
1039 self
._pushMatrix
(matrix
)
1041 self
._doCreateGeom
(instancing
)
1043 if matrix
is not None:
1046 self
._creating
= False
1049 class SVGGeometryContainer(SVGGeometry
):
1051 Container of SVG geometries
1054 __slots__
= ('_geometries', # List of chold geometries
1055 '_styles') # Styles, used for displaying
1057 def __init__(self
, node
, context
):
1059 Initialize SVG geometry container
1062 super().__init
__(node
, context
)
1064 self
._geometries
= []
1065 self
._styles
= SVGEmptyStyles
1069 Parse XML node to memory
1072 if type(self
._node
) is xml
.dom
.minidom
.Element
:
1073 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1075 self
._pushStyle
(self
._styles
)
1077 for node
in self
._node
.childNodes
:
1078 if type(node
) is not xml
.dom
.minidom
.Element
:
1081 ob
= parseAbstractNode(node
, self
._context
)
1083 self
._geometries
.append(ob
)
1087 def _doCreateGeom(self
, instancing
):
1089 Create real geometries
1092 for geom
in self
._geometries
:
1093 geom
.createGeom(instancing
)
1095 def getGeometries(self
):
1097 Get list of parsed geometries
1100 return self
._geometries
1103 class SVGGeometryPATH(SVGGeometry
):
1108 __slots__
= ('_splines', # List of splines after parsing
1109 '_styles') # Styles, used for displaying
1111 def __init__(self
, node
, context
):
1116 super().__init
__(node
, context
)
1119 self
._styles
= SVGEmptyStyles
1126 d
= self
._node
.getAttribute('d')
1128 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1130 pathParser
= SVGPathParser(d
, self
._styles
['useFill'])
1133 self
._splines
= pathParser
.getSplines()
1135 def _doCreateGeom(self
, instancing
):
1137 Create real geometries
1140 ob
= SVGCreateCurve(self
._context
)
1143 id_names_from_node(self
._node
, ob
)
1145 if self
._styles
['useFill']:
1146 cu
.dimensions
= '2D'
1147 cu
.fill_mode
= 'BOTH'
1148 cu
.materials
.append(self
._styles
['fill'])
1150 cu
.dimensions
= '3D'
1152 for spline
in self
._splines
:
1155 if spline
['closed'] and len(spline
['points']) >= 2:
1156 first
= spline
['points'][0]
1157 last
= spline
['points'][-1]
1158 if ( first
['handle_left_type'] == 'FREE' and
1159 last
['handle_right_type'] == 'VECTOR'):
1160 last
['handle_right_type'] = 'FREE'
1161 last
['handle_right'] = (last
['x'], last
['y'])
1162 if ( last
['handle_right_type'] == 'FREE' and
1163 first
['handle_left_type'] == 'VECTOR'):
1164 first
['handle_left_type'] = 'FREE'
1165 first
['handle_left'] = (first
['x'], first
['y'])
1167 for point
in spline
['points']:
1168 co
= self
._transformCoord
((point
['x'], point
['y']))
1170 if act_spline
is None:
1171 cu
.splines
.new('BEZIER')
1173 act_spline
= cu
.splines
[-1]
1174 act_spline
.use_cyclic_u
= spline
['closed']
1176 act_spline
.bezier_points
.add(1)
1178 bezt
= act_spline
.bezier_points
[-1]
1181 bezt
.handle_left_type
= point
['handle_left_type']
1182 if point
['handle_left'] is not None:
1183 handle
= point
['handle_left']
1184 bezt
.handle_left
= self
._transformCoord
(handle
)
1186 bezt
.handle_right_type
= point
['handle_right_type']
1187 if point
['handle_right'] is not None:
1188 handle
= point
['handle_right']
1189 bezt
.handle_right
= self
._transformCoord
(handle
)
1194 class SVGGeometryDEFS(SVGGeometryContainer
):
1196 Container for referenced elements
1199 def createGeom(self
, instancing
):
1201 Create real geometries
1207 class SVGGeometrySYMBOL(SVGGeometryContainer
):
1212 def _doCreateGeom(self
, instancing
):
1214 Create real geometries
1217 self
._pushMatrix
(self
.getNodeMatrix())
1219 super()._doCreateGeom
(False)
1223 def createGeom(self
, instancing
):
1225 Create real geometries
1231 super().createGeom(instancing
)
1234 class SVGGeometryG(SVGGeometryContainer
):
1242 class SVGGeometryUSE(SVGGeometry
):
1244 User of referenced elements
1247 def _doCreateGeom(self
, instancing
):
1249 Create real geometries
1252 ref
= self
._node
.getAttribute('xlink:href')
1253 geom
= self
._context
['defines'].get(ref
)
1255 if geom
is not None:
1256 rect
= SVGRectFromNode(self
._node
, self
._context
)
1257 self
._pushRect
(rect
)
1259 self
._pushMatrix
(self
.getNodeMatrix())
1261 geom
.createGeom(True)
1268 class SVGGeometryRECT(SVGGeometry
):
1273 __slots__
= ('_rect', # coordinate and dimensions of rectangle
1274 '_radius', # Rounded corner radiuses
1275 '_styles') # Styles, used for displaying
1277 def __init__(self
, node
, context
):
1279 Initialize new rectangle
1282 super().__init
__(node
, context
)
1284 self
._rect
= ('0', '0', '0', '0')
1285 self
._radius
= ('0', '0')
1286 self
._styles
= SVGEmptyStyles
1290 Parse SVG rectangle node
1293 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1296 for attr
in ['x', 'y', 'width', 'height']:
1297 val
= self
._node
.getAttribute(attr
)
1298 rect
.append(val
or '0')
1302 rx
= self
._node
.getAttribute('rx')
1303 ry
= self
._node
.getAttribute('ry')
1305 self
._radius
= (rx
, ry
)
1307 def _appendCorner(self
, spline
, coord
, firstTime
, rounded
):
1309 Append new corner to rectangle
1314 handle
= self
._transformCoord
(coord
[2])
1315 coord
= (coord
[0], coord
[1])
1317 co
= self
._transformCoord
(coord
)
1320 spline
.bezier_points
.add(1)
1322 bezt
= spline
.bezier_points
[-1]
1327 bezt
.handle_left_type
= 'VECTOR'
1328 bezt
.handle_right_type
= 'FREE'
1330 bezt
.handle_right
= handle
1332 bezt
.handle_left_type
= 'FREE'
1333 bezt
.handle_right_type
= 'VECTOR'
1334 bezt
.handle_left
= co
1337 bezt
.handle_left_type
= 'VECTOR'
1338 bezt
.handle_right_type
= 'VECTOR'
1340 def _doCreateGeom(self
, instancing
):
1342 Create real geometries
1345 # Run-time parsing -- percents would be correct only if
1347 crect
= self
._context
['rect']
1351 rect
.append(SVGParseCoord(self
._rect
[i
], crect
[i
% 2]))
1357 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1358 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1360 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1361 ry
= min(rx
, rect
[3] / 2)
1362 rx
= ry
= min(rx
, ry
)
1364 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1365 rx
= min(ry
, rect
[2] / 2)
1366 rx
= ry
= min(rx
, ry
)
1371 ob
= SVGCreateCurve(self
._context
)
1374 id_names_from_node(self
._node
, ob
)
1376 if self
._styles
['useFill']:
1377 cu
.dimensions
= '2D'
1378 cu
.fill_mode
= 'BOTH'
1379 cu
.materials
.append(self
._styles
['fill'])
1381 cu
.dimensions
= '3D'
1383 cu
.splines
.new('BEZIER')
1385 spline
= cu
.splines
[-1]
1386 spline
.use_cyclic_u
= True
1388 x
, y
= rect
[0], rect
[1]
1389 w
, h
= rect
[2], rect
[3]
1390 rx
, ry
= radius
[0], radius
[1]
1407 # Optional third component -- right handle coord
1408 coords
= [(x
+ rx
, y
),
1409 (x
+ w
- rx
, y
, (x
+ w
, y
)),
1411 (x
+ w
, y
+ h
- ry
, (x
+ w
, y
+ h
)),
1412 (x
+ w
- rx
, y
+ h
),
1413 (x
+ rx
, y
+ h
, (x
, y
+ h
)),
1415 (x
, y
+ ry
, (x
, y
))]
1419 coords
= [(x
, y
), (x
+ w
, y
), (x
+ w
, y
+ h
), (x
, y
+ h
)]
1422 for coord
in coords
:
1423 self
._appendCorner
(spline
, coord
, firstTime
, rounded
)
1429 class SVGGeometryELLIPSE(SVGGeometry
):
1434 __slots__
= ('_cx', # X-coordinate of center
1435 '_cy', # Y-coordinate of center
1436 '_rx', # X-axis radius of circle
1437 '_ry', # Y-axis radius of circle
1438 '_styles') # Styles, used for displaying
1440 def __init__(self
, node
, context
):
1442 Initialize new ellipse
1445 super().__init
__(node
, context
)
1451 self
._styles
= SVGEmptyStyles
1455 Parse SVG ellipse node
1458 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1460 self
._cx
= self
._node
.getAttribute('cx') or '0'
1461 self
._cy
= self
._node
.getAttribute('cy') or '0'
1462 self
._rx
= self
._node
.getAttribute('rx') or '0'
1463 self
._ry
= self
._node
.getAttribute('ry') or '0'
1465 def _doCreateGeom(self
, instancing
):
1467 Create real geometries
1470 # Run-time parsing -- percents would be correct only if
1472 crect
= self
._context
['rect']
1474 cx
= SVGParseCoord(self
._cx
, crect
[0])
1475 cy
= SVGParseCoord(self
._cy
, crect
[1])
1476 rx
= SVGParseCoord(self
._rx
, crect
[0])
1477 ry
= SVGParseCoord(self
._ry
, crect
[1])
1479 if not rx
or not ry
:
1480 # Automaic handles will work incorrect in this case
1484 ob
= SVGCreateCurve(self
._context
)
1487 id_names_from_node(self
._node
, ob
)
1489 if self
._styles
['useFill']:
1490 cu
.dimensions
= '2D'
1491 cu
.fill_mode
= 'BOTH'
1492 cu
.materials
.append(self
._styles
['fill'])
1494 cu
.dimensions
= '3D'
1496 coords
= [((cx
- rx
, cy
),
1497 (cx
- rx
, cy
+ ry
* 0.552),
1498 (cx
- rx
, cy
- ry
* 0.552)),
1501 (cx
- rx
* 0.552, cy
- ry
),
1502 (cx
+ rx
* 0.552, cy
- ry
)),
1505 (cx
+ rx
, cy
- ry
* 0.552),
1506 (cx
+ rx
, cy
+ ry
* 0.552)),
1509 (cx
+ rx
* 0.552, cy
+ ry
),
1510 (cx
- rx
* 0.552, cy
+ ry
))]
1513 for coord
in coords
:
1514 co
= self
._transformCoord
(coord
[0])
1515 handle_left
= self
._transformCoord
(coord
[1])
1516 handle_right
= self
._transformCoord
(coord
[2])
1519 cu
.splines
.new('BEZIER')
1520 spline
= cu
.splines
[-1]
1521 spline
.use_cyclic_u
= True
1523 spline
.bezier_points
.add(1)
1525 bezt
= spline
.bezier_points
[-1]
1527 bezt
.handle_left_type
= 'FREE'
1528 bezt
.handle_right_type
= 'FREE'
1529 bezt
.handle_left
= handle_left
1530 bezt
.handle_right
= handle_right
1535 class SVGGeometryCIRCLE(SVGGeometryELLIPSE
):
1542 Parse SVG circle node
1545 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1547 self
._cx
= self
._node
.getAttribute('cx') or '0'
1548 self
._cy
= self
._node
.getAttribute('cy') or '0'
1550 r
= self
._node
.getAttribute('r') or '0'
1551 self
._rx
= self
._ry
= r
1554 class SVGGeometryLINE(SVGGeometry
):
1559 __slots__
= ('_x1', # X-coordinate of beginning
1560 '_y1', # Y-coordinate of beginning
1561 '_x2', # X-coordinate of ending
1562 '_y2') # Y-coordinate of ending
1564 def __init__(self
, node
, context
):
1569 super().__init
__(node
, context
)
1581 self
._x
1 = self
._node
.getAttribute('x1') or '0'
1582 self
._y
1 = self
._node
.getAttribute('y1') or '0'
1583 self
._x
2 = self
._node
.getAttribute('x2') or '0'
1584 self
._y
2 = self
._node
.getAttribute('y2') or '0'
1586 def _doCreateGeom(self
, instancing
):
1588 Create real geometries
1591 # Run-time parsing -- percents would be correct only if
1593 crect
= self
._context
['rect']
1595 x1
= SVGParseCoord(self
._x
1, crect
[0])
1596 y1
= SVGParseCoord(self
._y
1, crect
[1])
1597 x2
= SVGParseCoord(self
._x
2, crect
[0])
1598 y2
= SVGParseCoord(self
._y
2, crect
[1])
1601 ob
= SVGCreateCurve(self
._context
)
1604 id_names_from_node(self
._node
, ob
)
1606 coords
= [(x1
, y1
), (x2
, y2
)]
1609 for coord
in coords
:
1610 co
= self
._transformCoord
(coord
)
1613 cu
.splines
.new('BEZIER')
1614 spline
= cu
.splines
[-1]
1615 spline
.use_cyclic_u
= True
1617 spline
.bezier_points
.add(1)
1619 bezt
= spline
.bezier_points
[-1]
1621 bezt
.handle_left_type
= 'VECTOR'
1622 bezt
.handle_right_type
= 'VECTOR'
1627 class SVGGeometryPOLY(SVGGeometry
):
1629 Abstract class for handling poly-geometries
1630 (polylines and polygons)
1633 __slots__
= ('_points', # Array of points for poly geometry
1634 '_styles', # Styles, used for displaying
1635 '_closed') # Should generated curve be closed?
1637 def __init__(self
, node
, context
):
1639 Initialize new poly geometry
1642 super().__init
__(node
, context
)
1645 self
._styles
= SVGEmptyStyles
1646 self
._closed
= False
1653 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1655 points
= parse_array_of_floats(self
._node
.getAttribute('points'))
1664 self
._points
.append((prev
, p
))
1667 def _doCreateGeom(self
, instancing
):
1669 Create real geometries
1672 ob
= SVGCreateCurve(self
._context
)
1675 id_names_from_node(self
._node
, ob
)
1677 if self
._closed
and self
._styles
['useFill']:
1678 cu
.dimensions
= '2D'
1679 cu
.fill_mode
= 'BOTH'
1680 cu
.materials
.append(self
._styles
['fill'])
1682 cu
.dimensions
= '3D'
1686 for point
in self
._points
:
1687 co
= self
._transformCoord
(point
)
1690 cu
.splines
.new('BEZIER')
1691 spline
= cu
.splines
[-1]
1692 spline
.use_cyclic_u
= self
._closed
1694 spline
.bezier_points
.add(1)
1696 bezt
= spline
.bezier_points
[-1]
1698 bezt
.handle_left_type
= 'VECTOR'
1699 bezt
.handle_right_type
= 'VECTOR'
1704 class SVGGeometryPOLYLINE(SVGGeometryPOLY
):
1706 SVG polyline geometry
1712 class SVGGeometryPOLYGON(SVGGeometryPOLY
):
1714 SVG polygon geometry
1717 def __init__(self
, node
, context
):
1719 Initialize new polygon geometry
1722 super().__init
__(node
, context
)
1727 class SVGGeometrySVG(SVGGeometryContainer
):
1729 Main geometry holder
1732 def _doCreateGeom(self
, instancing
):
1734 Create real geometries
1737 rect
= SVGRectFromNode(self
._node
, self
._context
)
1739 matrix
= self
.getNodeMatrix()
1741 # Better SVG compatibility: match svg-document units
1742 # with blender units
1747 if self
._node
.getAttribute('height'):
1748 raw_height
= self
._node
.getAttribute('height')
1749 token
, last_char
= read_float(raw_height
)
1750 document_height
= float(token
)
1751 unit
= raw_height
[last_char
:].strip()
1753 if self
._node
.getAttribute('viewBox'):
1754 viewbox
= parse_array_of_floats(self
._node
.getAttribute('viewBox'))
1756 if len(viewbox
) == 4 and unit
in ('cm', 'mm', 'in', 'pt', 'pc'):
1758 #convert units to BU:
1759 unitscale
= units
[unit
] / 90 * 1000 / 39.3701
1761 #apply blender unit scale:
1762 unitscale
= unitscale
/ bpy
.context
.scene
.unit_settings
.scale_length
1764 matrix
= matrix
@ Matrix
.Scale(unitscale
, 4, Vector((1.0, 0.0, 0.0)))
1765 matrix
= matrix
@ Matrix
.Scale(unitscale
, 4, Vector((0.0, 1.0, 0.0)))
1767 # match document origin with 3D space origin.
1768 if self
._node
.getAttribute('viewBox'):
1769 viewbox
= parse_array_of_floats(self
._node
.getAttribute('viewBox'))
1770 matrix
= matrix
@ matrix
.Translation([0.0, - viewbox
[1] - viewbox
[3], 0.0])
1772 self
._pushMatrix
(matrix
)
1773 self
._pushRect
(rect
)
1775 super()._doCreateGeom
(False)
1781 class SVGLoader(SVGGeometryContainer
):
1786 def getTransformMatrix(self
):
1788 Get matrix created from "transform" attribute
1791 # SVG document doesn't support transform specification
1792 # it can't even hold attributes
1796 def __init__(self
, context
, filepath
, do_colormanage
):
1798 Initialize SVG loader
1802 svg_name
= os
.path
.basename(filepath
)
1803 scene
= context
.scene
1804 collection
= bpy
.data
.collections
.new(name
=svg_name
)
1805 scene
.collection
.children
.link(collection
)
1807 node
= xml
.dom
.minidom
.parse(filepath
)
1810 m
= m
@ Matrix
.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
1811 m
= m
@ Matrix
.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
1815 self
._context
= {'defines': {},
1823 'do_colormanage': do_colormanage
,
1824 'collection': collection
}
1826 super().__init
__(node
, self
._context
)
1829 svgGeometryClasses
= {
1830 'svg': SVGGeometrySVG
,
1831 'path': SVGGeometryPATH
,
1832 'defs': SVGGeometryDEFS
,
1833 'symbol': SVGGeometrySYMBOL
,
1834 'use': SVGGeometryUSE
,
1835 'rect': SVGGeometryRECT
,
1836 'ellipse': SVGGeometryELLIPSE
,
1837 'circle': SVGGeometryCIRCLE
,
1838 'line': SVGGeometryLINE
,
1839 'polyline': SVGGeometryPOLYLINE
,
1840 'polygon': SVGGeometryPOLYGON
,
1844 def parseAbstractNode(node
, context
):
1845 name
= node
.tagName
.lower()
1847 if name
.startswith('svg:'):
1850 geomClass
= svgGeometryClasses
.get(name
)
1852 if geomClass
is not None:
1853 ob
= geomClass(node
, context
)
1861 def load_svg(context
, filepath
, do_colormanage
):
1863 Load specified SVG file
1866 if bpy
.ops
.object.mode_set
.poll():
1867 bpy
.ops
.object.mode_set(mode
='OBJECT')
1869 loader
= SVGLoader(context
, filepath
, do_colormanage
)
1871 loader
.createGeom(False)
1874 def load(operator
, context
, filepath
=""):
1876 # error in code should raise exceptions but loading
1877 # non SVG files can give useful messages.
1878 do_colormanage
= context
.scene
.display_settings
.display_device
!= 'NONE'
1880 load_svg(context
, filepath
, do_colormanage
)
1881 except (xml
.parsers
.expat
.ExpatError
, UnicodeEncodeError) as e
:
1883 traceback
.print_exc()
1885 operator
.report({'WARNING'}, "Unable to parse XML, %s:%s for file %r" % (type(e
).__name
__, e
, filepath
))
1886 return {'CANCELLED'}