1 # SPDX-FileCopyrightText: 2004-2009 JM Soler
2 # SPDX-FileCopyrightText: 2011-2022 Blender Foundation
4 # SPDX-License-Identifier: GPL-2.0-or-later
8 from math
import cos
, sin
, tan
, atan2
, pi
, ceil
11 from mathutils
import Vector
, Matrix
12 from bpy
.app
.translations
import pgettext_tip
as tip_
14 from . import svg_colors
15 from .svg_util
import (units
,
18 parse_array_of_floats
,
21 #### Common utilities ####
23 SVGEmptyStyles
= {'useFill': None,
27 def SVGCreateCurve(context
):
29 Create new curve object to hold splines in
32 cu
= bpy
.data
.curves
.new("Curve", 'CURVE')
33 obj
= bpy
.data
.objects
.new("Curve", cu
)
35 context
['collection'].objects
.link(obj
)
48 def SVGFlipHandle(x
, y
, x1
, y1
):
50 Flip handle around base point
59 def SVGParseCoord(coord
, size
):
61 Parse coordinate component to common basis
63 Needed to handle coordinates set in cm, mm, inches.
66 token
, last_char
= read_float(coord
)
68 unit
= coord
[last_char
:].strip() # strip() in case there is a space
71 return float(size
) / 100.0 * val
72 return val
* units
[unit
]
75 def SVGRectFromNode(node
, context
):
77 Get display rectangle from node
80 w
= context
['rect'][0]
81 h
= context
['rect'][1]
83 if node
.getAttribute('viewBox'):
84 viewBox
= node
.getAttribute('viewBox').replace(',', ' ').split()
85 w
= SVGParseCoord(viewBox
[2], w
)
86 h
= SVGParseCoord(viewBox
[3], h
)
88 if node
.getAttribute('width'):
89 w
= SVGParseCoord(node
.getAttribute('width'), w
)
91 if node
.getAttribute('height'):
92 h
= SVGParseCoord(node
.getAttribute('height'), h
)
97 def SVGMatrixFromNode(node
, context
):
99 Get transformation matrix from given node
102 tagName
= node
.tagName
.lower()
103 tags
= ['svg:svg', 'svg:use', 'svg:symbol']
105 if tagName
not in tags
and 'svg:' + tagName
not in tags
:
108 rect
= context
['rect']
109 has_user_coordinate
= (len(context
['rects']) > 1)
112 x
= SVGParseCoord(node
.getAttribute('x') or '0', rect
[0])
113 y
= SVGParseCoord(node
.getAttribute('y') or '0', rect
[1])
114 w
= SVGParseCoord(node
.getAttribute('width') or str(rect
[0]), rect
[0])
115 h
= SVGParseCoord(node
.getAttribute('height') or str(rect
[1]), rect
[1])
117 m
= Matrix
.Translation(Vector((x
, y
, 0.0)))
118 if has_user_coordinate
:
119 if rect
[0] != 0 and rect
[1] != 0:
120 m
= m
@ Matrix
.Scale(w
/ rect
[0], 4, Vector((1.0, 0.0, 0.0)))
121 m
= m
@ Matrix
.Scale(h
/ rect
[1], 4, Vector((0.0, 1.0, 0.0)))
123 if node
.getAttribute('viewBox'):
124 viewBox
= node
.getAttribute('viewBox').replace(',', ' ').split()
125 vx
= SVGParseCoord(viewBox
[0], w
)
126 vy
= SVGParseCoord(viewBox
[1], h
)
127 vw
= SVGParseCoord(viewBox
[2], w
)
128 vh
= SVGParseCoord(viewBox
[3], h
)
130 if vw
== 0 or vh
== 0:
133 if has_user_coordinate
or (w
!= 0 and h
!= 0):
142 tx
= (w
- vw
* scale
) / 2
143 ty
= (h
- vh
* scale
) / 2
144 m
= m
@ Matrix
.Translation(Vector((tx
, ty
, 0.0)))
146 m
= m
@ Matrix
.Translation(Vector((-vx
, -vy
, 0.0)))
147 m
= m
@ Matrix
.Scale(scale
, 4, Vector((1.0, 0.0, 0.0)))
148 m
= m
@ Matrix
.Scale(scale
, 4, Vector((0.0, 1.0, 0.0)))
153 def SVGParseTransform(transform
):
155 Parse transform string and return transformation matrix
159 r
= re
.compile(r
'\s*([A-z]+)\s*\((.*?)\)')
161 for match
in r
.finditer(transform
):
162 func
= match
.group(1)
163 params
= match
.group(2)
164 params
= params
.replace(',', ' ').split()
166 proc
= SVGTransforms
.get(func
)
168 raise Exception('Unknown transform function: ' + func
)
175 def SVGGetMaterial(color
, context
):
177 Get material for specified color
180 materials
= context
['materials']
181 rgb_re
= re
.compile(r
'^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
183 if color
in materials
:
184 return materials
[color
]
187 if color
.startswith('#'):
191 color
= color
[0] * 2 + color
[1] * 2 + color
[2] * 2
193 diff
= (int(color
[0:2], 16), int(color
[2:4], 16), int(color
[4:6], 16))
194 elif color
in svg_colors
.SVGColors
:
195 diff
= svg_colors
.SVGColors
[color
]
196 elif rgb_re
.match(color
):
197 c
= rgb_re
.findall(color
)[0]
198 diff
= (float(c
[0]), float(c
[1]), float(c
[2]))
202 diffuse_color
= ([x
/ 255.0 for x
in diff
])
204 if context
['do_colormanage']:
205 diffuse_color
[0] = srgb_to_linearrgb(diffuse_color
[0])
206 diffuse_color
[1] = srgb_to_linearrgb(diffuse_color
[1])
207 diffuse_color
[2] = srgb_to_linearrgb(diffuse_color
[2])
209 mat
= bpy
.data
.materials
.new(name
='SVGMat')
210 mat
.diffuse_color
= (*diffuse_color
, 1.0)
212 materials
[color
] = mat
217 def SVGTransformTranslate(params
):
219 translate SVG transform command
222 tx
= float(params
[0])
223 ty
= float(params
[1]) if len(params
) > 1 else 0.0
225 return Matrix
.Translation(Vector((tx
, ty
, 0.0)))
228 def SVGTransformMatrix(params
):
230 matrix SVG transform command
240 return Matrix(((a
, c
, 0.0, e
),
246 def SVGTransformScale(params
):
248 scale SVG transform command
251 sx
= float(params
[0])
252 sy
= float(params
[1]) if len(params
) > 1 else sx
256 m
= m
@ Matrix
.Scale(sx
, 4, Vector((1.0, 0.0, 0.0)))
257 m
= m
@ Matrix
.Scale(sy
, 4, Vector((0.0, 1.0, 0.0)))
262 def SVGTransformSkewY(params
):
264 skewY SVG transform command
267 ang
= float(params
[0]) * pi
/ 180.0
269 return Matrix(((1.0, 0.0, 0.0),
270 (tan(ang
), 1.0, 0.0),
271 (0.0, 0.0, 1.0))).to_4x4()
274 def SVGTransformSkewX(params
):
276 skewX SVG transform command
279 ang
= float(params
[0]) * pi
/ 180.0
281 return Matrix(((1.0, tan(ang
), 0.0),
283 (0.0, 0.0, 1.0))).to_4x4()
286 def SVGTransformRotate(params
):
288 skewX SVG transform command
291 ang
= float(params
[0]) * pi
/ 180.0
295 cx
= float(params
[1])
296 cy
= float(params
[2])
298 tm
= Matrix
.Translation(Vector((cx
, cy
, 0.0)))
299 rm
= Matrix
.Rotation(ang
, 4, Vector((0.0, 0.0, 1.0)))
301 return tm
@ rm
@ tm
.inverted()
303 SVGTransforms
= {'translate': SVGTransformTranslate
,
304 'scale': SVGTransformScale
,
305 'skewX': SVGTransformSkewX
,
306 'skewY': SVGTransformSkewY
,
307 'matrix': SVGTransformMatrix
,
308 'rotate': SVGTransformRotate
}
311 def SVGParseStyles(node
, context
):
313 Parse node to get different styles for displaying geometries
314 (materials, filling flags, etc..)
317 styles
= SVGEmptyStyles
.copy()
319 style
= node
.getAttribute('style')
321 elems
= style
.split(';')
328 name
= s
[0].strip().lower()
334 styles
['useFill'] = False
336 styles
['useFill'] = True
337 styles
['fill'] = SVGGetMaterial(val
, context
)
339 if styles
['useFill'] is None:
340 styles
['useFill'] = True
341 styles
['fill'] = SVGGetMaterial('#000', context
)
345 if styles
['useFill'] is None:
346 fill
= node
.getAttribute('fill')
350 styles
['useFill'] = False
352 styles
['useFill'] = True
353 styles
['fill'] = SVGGetMaterial(fill
, context
)
355 if styles
['useFill'] is None and context
['style']:
356 styles
= context
['style'].copy()
358 if styles
['useFill'] is None:
359 styles
['useFill'] = True
360 styles
['fill'] = SVGGetMaterial('#000', context
)
364 def id_names_from_node(node
, ob
):
365 if node
.getAttribute('id'):
366 name
= node
.getAttribute('id')
370 #### SVG path helpers ####
375 SVG Path data token supplier
378 __slots__
= ('_data', # List of tokens
379 '_index', # Index of current token in tokens list
380 '_len') # Length of tokens list
382 def __init__(self
, d
):
384 Initialize new path data supplier
386 d - the definition of the outline of a shape
390 commands
= {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
401 elif c
.lower() in commands
:
405 elif c
in ['-', '.'] or c
.isdigit():
406 # Special case for 'a/A' commands.
407 # Arguments 4 and 5 are either 0 or 1 and might not
408 # be separated from the next argument with space or comma.
409 if current_command
.lower() == 'a':
410 if arg_index
% 7 in [4,5]:
414 token
, last_char
= read_float(d
, i
)
416 token
, last_char
= read_float(d
, i
)
421 # in most cases len(token) and (last_char - i) are the same
422 # but with whitespace or ',' prefix they are not.
424 i
+= (last_char
- i
) - 1
430 self
._len
= len(tokens
)
434 Check if end of data reached
437 return self
._index
>= self
._len
447 return self
._data
[self
._index
]
449 def lookupNext(self
):
451 get next token without moving pointer
457 return self
._data
[self
._index
]
461 Return current token and go to next one
467 token
= self
._data
[self
._index
]
474 Return coordinate created from current token and move to next token
487 Parser of SVG path data
490 __slots__
= ('_data', # Path data supplird
491 '_point', # Current point coordinate
492 '_handle', # Last handle coordinate
493 '_splines', # List of all splies created during parsing
494 '_spline', # Currently handling spline
495 '_commands', # Hash of all supported path commands
496 '_use_fill', # Splines would be filled, so expected to be closed
499 def __init__(self
, d
, use_fill
):
501 Initialize path parser
503 d - the definition of the outline of a shape
506 self
._data
= SVGPathData(d
)
507 self
._point
= None # Current point
508 self
._handle
= None # Last handle
509 self
._splines
= [] # List of splines in path
510 self
._spline
= None # Current spline
511 self
._use
_fill
= use_fill
513 self
._commands
= {'M': self
._pathMoveTo
,
514 'L': self
._pathLineTo
,
515 'H': self
._pathLineTo
,
516 'V': self
._pathLineTo
,
517 'C': self
._pathCurveToCS
,
518 'S': self
._pathCurveToCS
,
519 'Q': self
._pathCurveToQT
,
520 'T': self
._pathCurveToQT
,
521 'A': self
._pathCurveToA
,
522 'Z': self
._pathClose
,
524 'm': self
._pathMoveTo
,
525 'l': self
._pathLineTo
,
526 'h': self
._pathLineTo
,
527 'v': self
._pathLineTo
,
528 'c': self
._pathCurveToCS
,
529 's': self
._pathCurveToCS
,
530 'q': self
._pathCurveToQT
,
531 't': self
._pathCurveToQT
,
532 'a': self
._pathCurveToA
,
533 'z': self
._pathClose
}
535 def _getCoordPair(self
, relative
, point
):
537 Get next coordinate pair
540 x
= self
._data
.nextCoord()
541 y
= self
._data
.nextCoord()
543 if relative
and point
is not None:
549 def _appendPoint(self
, x
, y
, handle_left
=None, handle_left_type
='VECTOR',
550 handle_right
=None, handle_right_type
='VECTOR'):
552 Append point to spline
554 If there's no active spline, create one and set it's first point
555 to current point coordinate
558 if self
._spline
is None:
559 self
._spline
= {'points': [],
562 self
._splines
.append(self
._spline
)
564 if len(self
._spline
['points']) > 0:
565 # Not sure about specifications, but Illustrator could create
566 # last point at the same position, as start point (which was
567 # reached by MoveTo command) to set needed handle coords.
568 # It's also could use last point at last position to make path
571 first
= self
._spline
['points'][0]
572 if check_points_equal((first
['x'], first
['y']), (x
, y
)):
573 if handle_left
is not None:
574 first
['handle_left'] = handle_left
575 first
['handle_left_type'] = 'FREE'
577 if handle_left_type
!= 'VECTOR':
578 first
['handle_left_type'] = handle_left_type
580 if self
._data
.eof() or self
._data
.lookupNext().lower() == 'm':
581 self
._spline
['closed'] = True
585 last
= self
._spline
['points'][-1]
586 if last
['handle_right_type'] == 'VECTOR' and handle_left_type
== 'FREE':
587 last
['handle_right'] = (last
['x'], last
['y'])
588 last
['handle_right_type'] = 'FREE'
589 if last
['handle_right_type'] == 'FREE' and handle_left_type
== 'VECTOR':
591 handle_left_type
= 'FREE'
596 'handle_left': handle_left
,
597 'handle_left_type': handle_left_type
,
599 'handle_right': handle_right
,
600 'handle_right_type': handle_right_type
}
602 self
._spline
['points'].append(point
)
604 def _updateHandle(self
, handle
=None, handle_type
=None):
606 Update right handle of previous point when adding new point to spline
609 point
= self
._spline
['points'][-1]
611 if handle_type
is not None:
612 point
['handle_right_type'] = handle_type
614 if handle
is not None:
615 point
['handle_right'] = handle
617 def _pathMoveTo(self
, code
):
622 relative
= code
.islower()
623 x
, y
= self
._getCoordPair
(relative
, self
._point
)
625 self
._spline
= None # Flag to start new spline
628 cur
= self
._data
.cur()
629 while cur
is not None and not cur
.isalpha():
630 x
, y
= self
._getCoordPair
(relative
, self
._point
)
632 if self
._spline
is None:
633 self
._appendPoint
(self
._point
[0], self
._point
[1])
635 self
._appendPoint
(x
, y
)
638 cur
= self
._data
.cur()
642 def _pathLineTo(self
, code
):
649 cur
= self
._data
.cur()
650 while cur
is not None and not cur
.isalpha():
652 x
, y
= self
._getCoordPair
(code
== 'l', self
._point
)
654 x
= self
._data
.nextCoord()
658 y
= self
._data
.nextCoord()
665 if self
._spline
is None:
666 self
._appendPoint
(self
._point
[0], self
._point
[1])
668 self
._appendPoint
(x
, y
)
671 cur
= self
._data
.cur()
675 def _pathCurveToCS(self
, code
):
677 Cubic BEZIER CurveTo path command
681 cur
= self
._data
.cur()
682 while cur
is not None and not cur
.isalpha():
684 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
685 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
687 if self
._handle
is not None:
688 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
689 self
._handle
[0], self
._handle
[1])
693 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
695 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
697 if self
._spline
is None:
698 self
._appendPoint
(self
._point
[0], self
._point
[1],
699 handle_left_type
='FREE', handle_left
=self
._point
,
700 handle_right_type
='FREE', handle_right
=(x1
, y1
))
702 self
._updateHandle
(handle
=(x1
, y1
), handle_type
='FREE')
704 self
._appendPoint
(x
, y
,
705 handle_left_type
='FREE', handle_left
=(x2
, y2
),
706 handle_right_type
='FREE', handle_right
=(x
, y
))
709 self
._handle
= (x2
, y2
)
710 cur
= self
._data
.cur()
712 def _pathCurveToQT(self
, code
):
714 Quadratic BEZIER CurveTo path command
718 cur
= self
._data
.cur()
720 while cur
is not None and not cur
.isalpha():
722 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
724 if self
._handle
is not None:
725 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
726 self
._handle
[0], self
._handle
[1])
730 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
732 if not check_points_equal((x
, y
), self
._point
):
733 if self
._spline
is None:
734 self
._appendPoint
(self
._point
[0], self
._point
[1],
735 handle_left_type
='FREE', handle_left
=self
._point
,
736 handle_right_type
='FREE', handle_right
=self
._point
)
738 self
._appendPoint
(x
, y
,
739 handle_left_type
='FREE', handle_left
=(x1
, y1
),
740 handle_right_type
='FREE', handle_right
=(x
, y
))
743 self
._handle
= (x1
, y1
)
744 cur
= self
._data
.cur()
746 def _calcArc(self
, rx
, ry
, ang
, fa
, fs
, x
, y
):
750 Copied and adopted from `paths_svg2obj.py` script for Blender 2.49:
751 ``Copyright (c) jm soler juillet/novembre 2004-april 2009``.
758 px
= abs((cos(ang
) * (cpx
- x
) + sin(ang
) * (cpy
- y
)) * 0.5) ** 2.0
759 py
= abs((cos(ang
) * (cpy
- y
) - sin(ang
) * (cpx
- x
)) * 0.5) ** 2.0
763 px
= px
/ (rx
** 2.0)
766 rpy
= py
/ (ry
** 2.0)
774 carx
= sarx
= cary
= sary
= 0.0
784 x0
= carx
* cpx
+ sarx
* cpy
785 y0
= -sary
* cpx
+ cary
* cpy
786 x1
= carx
* x
+ sarx
* y
787 y1
= -sary
* x
+ cary
* y
788 d
= (x1
- x0
) * (x1
- x0
) + (y1
- y0
) * (y1
- y0
)
802 xc
= 0.5 * (x0
+ x1
) - sf
* (y1
- y0
)
803 yc
= 0.5 * (y0
+ y1
) + sf
* (x1
- x0
)
804 ang_0
= atan2(y0
- yc
, x0
- xc
)
805 ang_1
= atan2(y1
- yc
, x1
- xc
)
806 ang_arc
= ang_1
- ang_0
808 if ang_arc
< 0.0 and fs
== 1:
810 elif ang_arc
> 0.0 and fs
== 0:
813 n_segs
= int(ceil(abs(ang_arc
* 2.0 / (pi
* 0.5 + 0.001))))
815 if self
._spline
is None:
816 self
._appendPoint
(cpx
, cpy
,
817 handle_left_type
='FREE', handle_left
=(cpx
, cpy
),
818 handle_right_type
='FREE', handle_right
=(cpx
, cpy
))
820 for i
in range(n_segs
):
821 ang0
= ang_0
+ i
* ang_arc
/ n_segs
822 ang1
= ang_0
+ (i
+ 1) * ang_arc
/ n_segs
823 ang_demi
= 0.25 * (ang1
- ang0
)
824 t
= 2.66666 * sin(ang_demi
) * sin(ang_demi
) / sin(ang_demi
* 2.0)
825 x1
= xc
+ cos(ang0
) - t
* sin(ang0
)
826 y1
= yc
+ sin(ang0
) + t
* cos(ang0
)
829 x3
= x2
+ t
* sin(ang1
)
830 y3
= y2
- t
* cos(ang1
)
832 coord1
= ((cos(ang
) * rx
) * x1
+ (-sin(ang
) * ry
) * y1
,
833 (sin(ang
) * rx
) * x1
+ (cos(ang
) * ry
) * y1
)
834 coord2
= ((cos(ang
) * rx
) * x3
+ (-sin(ang
) * ry
) * y3
,
835 (sin(ang
) * rx
) * x3
+ (cos(ang
) * ry
) * y3
)
836 coord3
= ((cos(ang
) * rx
) * x2
+ (-sin(ang
) * ry
) * y2
,
837 (sin(ang
) * rx
) * x2
+ (cos(ang
) * ry
) * y2
)
839 self
._updateHandle
(handle
=coord1
, handle_type
='FREE')
841 self
._appendPoint
(coord3
[0], coord3
[1],
842 handle_left_type
='FREE', handle_left
=coord2
,
843 handle_right_type
='FREE', handle_right
=coord3
)
845 def _pathCurveToA(self
, code
):
847 Elliptical arc CurveTo path command
850 cur
= self
._data
.cur()
852 while cur
is not None and not cur
.isalpha():
853 rx
= float(self
._data
.next())
854 ry
= float(self
._data
.next())
855 ang
= float(self
._data
.next()) / 180 * pi
856 fa
= float(self
._data
.next())
857 fs
= float(self
._data
.next())
858 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
860 self
._calcArc
(rx
, ry
, ang
, fa
, fs
, x
, y
)
864 cur
= self
._data
.cur()
866 def _pathClose(self
, code
):
872 self
._spline
['closed'] = True
874 cv
= self
._spline
['points'][0]
875 self
._point
= (cv
['x'], cv
['y'])
877 def _pathCloseImplicitly(self
):
879 Close path implicitly without changing current point coordinate
883 self
._spline
['closed'] = True
892 while not self
._data
.eof():
893 code
= self
._data
.next()
894 cmd
= self
._commands
.get(code
)
897 raise Exception('Unknown path command: {0}' . format(code
))
899 if code
in {'Z', 'z'}:
904 if code
in {'M', 'm'} and self
._use
_fill
and not closed
:
905 self
._pathCloseImplicitly
() # Ensure closed before MoveTo path command
908 if self
._use
_fill
and not closed
:
909 self
._pathCloseImplicitly
() # Ensure closed at the end of parsing
911 def getSplines(self
):
913 Get splines definitions
921 Abstract SVG geometry
924 __slots__
= ('_node', # XML node for geometry
925 '_context', # Global SVG context (holds matrices stack, i.e.)
926 '_creating') # Flag if geometry is already creating
928 # need to detect cycles for USE node
930 def __init__(self
, node
, context
):
932 Initialize SVG geometry
936 self
._context
= context
937 self
._creating
= False
939 if hasattr(node
, 'getAttribute'):
940 defs
= context
['defines']
942 attr_id
= node
.getAttribute('id')
943 if attr_id
and defs
.get('#' + attr_id
) is None:
944 defs
['#' + attr_id
] = self
946 className
= node
.getAttribute('class')
947 if className
and defs
.get(className
) is None:
948 defs
[className
] = self
950 def _pushRect(self
, rect
):
952 Push display rectangle
955 self
._context
['rects'].append(rect
)
956 self
._context
['rect'] = rect
960 Pop display rectangle
963 self
._context
['rects'].pop()
964 self
._context
['rect'] = self
._context
['rects'][-1]
966 def _pushMatrix(self
, matrix
):
968 Push transformation matrix
971 current_matrix
= self
._context
['matrix']
972 self
._context
['matrix_stack'].append(current_matrix
)
973 self
._context
['matrix'] = current_matrix
@ matrix
975 def _popMatrix(self
):
977 Pop transformation matrix
980 old_matrix
= self
._context
['matrix_stack'].pop()
981 self
._context
['matrix'] = old_matrix
983 def _pushStyle(self
, style
):
988 self
._context
['styles'].append(style
)
989 self
._context
['style'] = style
996 self
._context
['styles'].pop()
997 self
._context
['style'] = self
._context
['styles'][-1]
999 def _transformCoord(self
, point
):
1001 Transform SVG-file coords
1004 v
= Vector((point
[0], point
[1], 0.0))
1006 return self
._context
['matrix'] @ v
1008 def getNodeMatrix(self
):
1010 Get transformation matrix of node
1013 return SVGMatrixFromNode(self
._node
, self
._context
)
1017 Parse XML node to memory
1022 def _doCreateGeom(self
, instancing
):
1024 Internal handler to create real geometries
1029 def getTransformMatrix(self
):
1031 Get matrix created from "transform" attribute
1034 transform
= self
._node
.getAttribute('transform')
1037 return SVGParseTransform(transform
)
1041 def createGeom(self
, instancing
):
1043 Create real geometries
1049 self
._creating
= True
1051 matrix
= self
.getTransformMatrix()
1052 if matrix
is not None:
1053 self
._pushMatrix
(matrix
)
1055 self
._doCreateGeom
(instancing
)
1057 if matrix
is not None:
1060 self
._creating
= False
1063 class SVGGeometryContainer(SVGGeometry
):
1065 Container of SVG geometries
1068 __slots__
= ('_geometries', # List of chold geometries
1069 '_styles') # Styles, used for displaying
1071 def __init__(self
, node
, context
):
1073 Initialize SVG geometry container
1076 super().__init
__(node
, context
)
1078 self
._geometries
= []
1079 self
._styles
= SVGEmptyStyles
1083 Parse XML node to memory
1086 if type(self
._node
) is xml
.dom
.minidom
.Element
:
1087 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1089 self
._pushStyle
(self
._styles
)
1091 for node
in self
._node
.childNodes
:
1092 if type(node
) is not xml
.dom
.minidom
.Element
:
1095 ob
= parseAbstractNode(node
, self
._context
)
1097 self
._geometries
.append(ob
)
1101 def _doCreateGeom(self
, instancing
):
1103 Create real geometries
1106 for geom
in self
._geometries
:
1107 geom
.createGeom(instancing
)
1109 def getGeometries(self
):
1111 Get list of parsed geometries
1114 return self
._geometries
1117 class SVGGeometryPATH(SVGGeometry
):
1122 __slots__
= ('_splines', # List of splines after parsing
1123 '_styles') # Styles, used for displaying
1125 def __init__(self
, node
, context
):
1130 super().__init
__(node
, context
)
1133 self
._styles
= SVGEmptyStyles
1140 d
= self
._node
.getAttribute('d')
1142 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1144 pathParser
= SVGPathParser(d
, self
._styles
['useFill'])
1147 self
._splines
= pathParser
.getSplines()
1149 def _doCreateGeom(self
, instancing
):
1151 Create real geometries
1154 ob
= SVGCreateCurve(self
._context
)
1157 id_names_from_node(self
._node
, ob
)
1159 if self
._styles
['useFill']:
1160 cu
.dimensions
= '2D'
1161 cu
.fill_mode
= 'BOTH'
1162 cu
.materials
.append(self
._styles
['fill'])
1164 cu
.dimensions
= '3D'
1166 for spline
in self
._splines
:
1169 if spline
['closed'] and len(spline
['points']) >= 2:
1170 first
= spline
['points'][0]
1171 last
= spline
['points'][-1]
1172 if ( first
['handle_left_type'] == 'FREE' and
1173 last
['handle_right_type'] == 'VECTOR'):
1174 last
['handle_right_type'] = 'FREE'
1175 last
['handle_right'] = (last
['x'], last
['y'])
1176 if ( last
['handle_right_type'] == 'FREE' and
1177 first
['handle_left_type'] == 'VECTOR'):
1178 first
['handle_left_type'] = 'FREE'
1179 first
['handle_left'] = (first
['x'], first
['y'])
1181 for point
in spline
['points']:
1182 co
= self
._transformCoord
((point
['x'], point
['y']))
1184 if act_spline
is None:
1185 cu
.splines
.new('BEZIER')
1187 act_spline
= cu
.splines
[-1]
1188 act_spline
.use_cyclic_u
= spline
['closed']
1190 act_spline
.bezier_points
.add(1)
1192 bezt
= act_spline
.bezier_points
[-1]
1195 bezt
.handle_left_type
= point
['handle_left_type']
1196 if point
['handle_left'] is not None:
1197 handle
= point
['handle_left']
1198 bezt
.handle_left
= self
._transformCoord
(handle
)
1200 bezt
.handle_right_type
= point
['handle_right_type']
1201 if point
['handle_right'] is not None:
1202 handle
= point
['handle_right']
1203 bezt
.handle_right
= self
._transformCoord
(handle
)
1208 class SVGGeometryDEFS(SVGGeometryContainer
):
1210 Container for referenced elements
1213 def createGeom(self
, instancing
):
1215 Create real geometries
1221 class SVGGeometrySYMBOL(SVGGeometryContainer
):
1226 def _doCreateGeom(self
, instancing
):
1228 Create real geometries
1231 self
._pushMatrix
(self
.getNodeMatrix())
1233 super()._doCreateGeom
(False)
1237 def createGeom(self
, instancing
):
1239 Create real geometries
1245 super().createGeom(instancing
)
1248 class SVGGeometryG(SVGGeometryContainer
):
1256 class SVGGeometryUSE(SVGGeometry
):
1258 User of referenced elements
1261 def _doCreateGeom(self
, instancing
):
1263 Create real geometries
1266 ref
= self
._node
.getAttribute('xlink:href')
1267 geom
= self
._context
['defines'].get(ref
)
1269 if geom
is not None:
1270 rect
= SVGRectFromNode(self
._node
, self
._context
)
1271 self
._pushRect
(rect
)
1273 self
._pushMatrix
(self
.getNodeMatrix())
1275 geom
.createGeom(True)
1282 class SVGGeometryRECT(SVGGeometry
):
1287 __slots__
= ('_rect', # coordinate and dimensions of rectangle
1288 '_radius', # Rounded corner radiuses
1289 '_styles') # Styles, used for displaying
1291 def __init__(self
, node
, context
):
1293 Initialize new rectangle
1296 super().__init
__(node
, context
)
1298 self
._rect
= ('0', '0', '0', '0')
1299 self
._radius
= ('0', '0')
1300 self
._styles
= SVGEmptyStyles
1304 Parse SVG rectangle node
1307 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1310 for attr
in ['x', 'y', 'width', 'height']:
1311 val
= self
._node
.getAttribute(attr
)
1312 rect
.append(val
or '0')
1316 rx
= self
._node
.getAttribute('rx')
1317 ry
= self
._node
.getAttribute('ry')
1319 self
._radius
= (rx
, ry
)
1321 def _appendCorner(self
, spline
, coord
, firstTime
, rounded
):
1323 Append new corner to rectangle
1328 handle
= self
._transformCoord
(coord
[2])
1329 coord
= (coord
[0], coord
[1])
1331 co
= self
._transformCoord
(coord
)
1334 spline
.bezier_points
.add(1)
1336 bezt
= spline
.bezier_points
[-1]
1341 bezt
.handle_left_type
= 'VECTOR'
1342 bezt
.handle_right_type
= 'FREE'
1344 bezt
.handle_right
= handle
1346 bezt
.handle_left_type
= 'FREE'
1347 bezt
.handle_right_type
= 'VECTOR'
1348 bezt
.handle_left
= co
1351 bezt
.handle_left_type
= 'VECTOR'
1352 bezt
.handle_right_type
= 'VECTOR'
1354 def _doCreateGeom(self
, instancing
):
1356 Create real geometries
1359 # Run-time parsing -- percents would be correct only if
1361 crect
= self
._context
['rect']
1365 rect
.append(SVGParseCoord(self
._rect
[i
], crect
[i
% 2]))
1371 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1372 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1374 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1375 ry
= min(rx
, rect
[3] / 2)
1376 rx
= ry
= min(rx
, ry
)
1378 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1379 rx
= min(ry
, rect
[2] / 2)
1380 rx
= ry
= min(rx
, ry
)
1385 ob
= SVGCreateCurve(self
._context
)
1388 id_names_from_node(self
._node
, ob
)
1390 if self
._styles
['useFill']:
1391 cu
.dimensions
= '2D'
1392 cu
.fill_mode
= 'BOTH'
1393 cu
.materials
.append(self
._styles
['fill'])
1395 cu
.dimensions
= '3D'
1397 cu
.splines
.new('BEZIER')
1399 spline
= cu
.splines
[-1]
1400 spline
.use_cyclic_u
= True
1402 x
, y
= rect
[0], rect
[1]
1403 w
, h
= rect
[2], rect
[3]
1404 rx
, ry
= radius
[0], radius
[1]
1421 # Optional third component -- right handle coord
1422 coords
= [(x
+ rx
, y
),
1423 (x
+ w
- rx
, y
, (x
+ w
, y
)),
1425 (x
+ w
, y
+ h
- ry
, (x
+ w
, y
+ h
)),
1426 (x
+ w
- rx
, y
+ h
),
1427 (x
+ rx
, y
+ h
, (x
, y
+ h
)),
1429 (x
, y
+ ry
, (x
, y
))]
1433 coords
= [(x
, y
), (x
+ w
, y
), (x
+ w
, y
+ h
), (x
, y
+ h
)]
1436 for coord
in coords
:
1437 self
._appendCorner
(spline
, coord
, firstTime
, rounded
)
1443 class SVGGeometryELLIPSE(SVGGeometry
):
1448 __slots__
= ('_cx', # X-coordinate of center
1449 '_cy', # Y-coordinate of center
1450 '_rx', # X-axis radius of circle
1451 '_ry', # Y-axis radius of circle
1452 '_styles') # Styles, used for displaying
1454 def __init__(self
, node
, context
):
1456 Initialize new ellipse
1459 super().__init
__(node
, context
)
1465 self
._styles
= SVGEmptyStyles
1469 Parse SVG ellipse node
1472 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1474 self
._cx
= self
._node
.getAttribute('cx') or '0'
1475 self
._cy
= self
._node
.getAttribute('cy') or '0'
1476 self
._rx
= self
._node
.getAttribute('rx') or '0'
1477 self
._ry
= self
._node
.getAttribute('ry') or '0'
1479 def _doCreateGeom(self
, instancing
):
1481 Create real geometries
1484 # Run-time parsing -- percents would be correct only if
1486 crect
= self
._context
['rect']
1488 cx
= SVGParseCoord(self
._cx
, crect
[0])
1489 cy
= SVGParseCoord(self
._cy
, crect
[1])
1490 rx
= SVGParseCoord(self
._rx
, crect
[0])
1491 ry
= SVGParseCoord(self
._ry
, crect
[1])
1493 if not rx
or not ry
:
1494 # Automaic handles will work incorrect in this case
1498 ob
= SVGCreateCurve(self
._context
)
1501 id_names_from_node(self
._node
, ob
)
1503 if self
._styles
['useFill']:
1504 cu
.dimensions
= '2D'
1505 cu
.fill_mode
= 'BOTH'
1506 cu
.materials
.append(self
._styles
['fill'])
1508 cu
.dimensions
= '3D'
1510 coords
= [((cx
- rx
, cy
),
1511 (cx
- rx
, cy
+ ry
* 0.552),
1512 (cx
- rx
, cy
- ry
* 0.552)),
1515 (cx
- rx
* 0.552, cy
- ry
),
1516 (cx
+ rx
* 0.552, cy
- ry
)),
1519 (cx
+ rx
, cy
- ry
* 0.552),
1520 (cx
+ rx
, cy
+ ry
* 0.552)),
1523 (cx
+ rx
* 0.552, cy
+ ry
),
1524 (cx
- rx
* 0.552, cy
+ ry
))]
1527 for coord
in coords
:
1528 co
= self
._transformCoord
(coord
[0])
1529 handle_left
= self
._transformCoord
(coord
[1])
1530 handle_right
= self
._transformCoord
(coord
[2])
1533 cu
.splines
.new('BEZIER')
1534 spline
= cu
.splines
[-1]
1535 spline
.use_cyclic_u
= True
1537 spline
.bezier_points
.add(1)
1539 bezt
= spline
.bezier_points
[-1]
1541 bezt
.handle_left_type
= 'FREE'
1542 bezt
.handle_right_type
= 'FREE'
1543 bezt
.handle_left
= handle_left
1544 bezt
.handle_right
= handle_right
1549 class SVGGeometryCIRCLE(SVGGeometryELLIPSE
):
1556 Parse SVG circle node
1559 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1561 self
._cx
= self
._node
.getAttribute('cx') or '0'
1562 self
._cy
= self
._node
.getAttribute('cy') or '0'
1564 r
= self
._node
.getAttribute('r') or '0'
1565 self
._rx
= self
._ry
= r
1568 class SVGGeometryLINE(SVGGeometry
):
1573 __slots__
= ('_x1', # X-coordinate of beginning
1574 '_y1', # Y-coordinate of beginning
1575 '_x2', # X-coordinate of ending
1576 '_y2') # Y-coordinate of ending
1578 def __init__(self
, node
, context
):
1583 super().__init
__(node
, context
)
1595 self
._x
1 = self
._node
.getAttribute('x1') or '0'
1596 self
._y
1 = self
._node
.getAttribute('y1') or '0'
1597 self
._x
2 = self
._node
.getAttribute('x2') or '0'
1598 self
._y
2 = self
._node
.getAttribute('y2') or '0'
1600 def _doCreateGeom(self
, instancing
):
1602 Create real geometries
1605 # Run-time parsing -- percents would be correct only if
1607 crect
= self
._context
['rect']
1609 x1
= SVGParseCoord(self
._x
1, crect
[0])
1610 y1
= SVGParseCoord(self
._y
1, crect
[1])
1611 x2
= SVGParseCoord(self
._x
2, crect
[0])
1612 y2
= SVGParseCoord(self
._y
2, crect
[1])
1615 ob
= SVGCreateCurve(self
._context
)
1618 id_names_from_node(self
._node
, ob
)
1620 coords
= [(x1
, y1
), (x2
, y2
)]
1623 for coord
in coords
:
1624 co
= self
._transformCoord
(coord
)
1627 cu
.splines
.new('BEZIER')
1628 spline
= cu
.splines
[-1]
1629 spline
.use_cyclic_u
= True
1631 spline
.bezier_points
.add(1)
1633 bezt
= spline
.bezier_points
[-1]
1635 bezt
.handle_left_type
= 'VECTOR'
1636 bezt
.handle_right_type
= 'VECTOR'
1641 class SVGGeometryPOLY(SVGGeometry
):
1643 Abstract class for handling poly-geometries
1644 (polylines and polygons)
1647 __slots__
= ('_points', # Array of points for poly geometry
1648 '_styles', # Styles, used for displaying
1649 '_closed') # Should generated curve be closed?
1651 def __init__(self
, node
, context
):
1653 Initialize new poly geometry
1656 super().__init
__(node
, context
)
1659 self
._styles
= SVGEmptyStyles
1660 self
._closed
= False
1667 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1669 points
= parse_array_of_floats(self
._node
.getAttribute('points'))
1678 self
._points
.append((prev
, p
))
1681 def _doCreateGeom(self
, instancing
):
1683 Create real geometries
1686 ob
= SVGCreateCurve(self
._context
)
1689 id_names_from_node(self
._node
, ob
)
1691 if self
._closed
and self
._styles
['useFill']:
1692 cu
.dimensions
= '2D'
1693 cu
.fill_mode
= 'BOTH'
1694 cu
.materials
.append(self
._styles
['fill'])
1696 cu
.dimensions
= '3D'
1700 for point
in self
._points
:
1701 co
= self
._transformCoord
(point
)
1704 cu
.splines
.new('BEZIER')
1705 spline
= cu
.splines
[-1]
1706 spline
.use_cyclic_u
= self
._closed
1708 spline
.bezier_points
.add(1)
1710 bezt
= spline
.bezier_points
[-1]
1712 bezt
.handle_left_type
= 'VECTOR'
1713 bezt
.handle_right_type
= 'VECTOR'
1718 class SVGGeometryPOLYLINE(SVGGeometryPOLY
):
1720 SVG polyline geometry
1726 class SVGGeometryPOLYGON(SVGGeometryPOLY
):
1728 SVG polygon geometry
1731 def __init__(self
, node
, context
):
1733 Initialize new polygon geometry
1736 super().__init
__(node
, context
)
1741 class SVGGeometrySVG(SVGGeometryContainer
):
1743 Main geometry holder
1746 def _doCreateGeom(self
, instancing
):
1748 Create real geometries
1751 rect
= SVGRectFromNode(self
._node
, self
._context
)
1753 matrix
= self
.getNodeMatrix()
1755 # Better SVG compatibility: match svg-document units
1756 # with blender units
1761 if self
._node
.getAttribute('height'):
1762 raw_height
= self
._node
.getAttribute('height')
1763 token
, last_char
= read_float(raw_height
)
1764 document_height
= float(token
)
1765 unit
= raw_height
[last_char
:].strip()
1767 if self
._node
.getAttribute('viewBox'):
1768 viewbox
= parse_array_of_floats(self
._node
.getAttribute('viewBox'))
1770 if len(viewbox
) == 4 and unit
in ('cm', 'mm', 'in', 'pt', 'pc'):
1772 #convert units to BU:
1773 unitscale
= units
[unit
] / 90 * 1000 / 39.3701
1775 #apply blender unit scale:
1776 unitscale
= unitscale
/ bpy
.context
.scene
.unit_settings
.scale_length
1778 matrix
= matrix
@ Matrix
.Scale(unitscale
, 4, Vector((1.0, 0.0, 0.0)))
1779 matrix
= matrix
@ Matrix
.Scale(unitscale
, 4, Vector((0.0, 1.0, 0.0)))
1781 # match document origin with 3D space origin.
1782 if self
._node
.getAttribute('viewBox'):
1783 viewbox
= parse_array_of_floats(self
._node
.getAttribute('viewBox'))
1784 matrix
= matrix
@ matrix
.Translation([0.0, - viewbox
[1] - viewbox
[3], 0.0])
1786 self
._pushMatrix
(matrix
)
1787 self
._pushRect
(rect
)
1789 super()._doCreateGeom
(False)
1795 class SVGLoader(SVGGeometryContainer
):
1800 def getTransformMatrix(self
):
1802 Get matrix created from "transform" attribute
1805 # SVG document doesn't support transform specification
1806 # it can't even hold attributes
1810 def __init__(self
, context
, filepath
, do_colormanage
):
1812 Initialize SVG loader
1816 svg_name
= os
.path
.basename(filepath
)
1817 scene
= context
.scene
1818 collection
= bpy
.data
.collections
.new(name
=svg_name
)
1819 scene
.collection
.children
.link(collection
)
1821 node
= xml
.dom
.minidom
.parse(filepath
)
1824 m
= m
@ Matrix
.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
1825 m
= m
@ Matrix
.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
1829 self
._context
= {'defines': {},
1837 'do_colormanage': do_colormanage
,
1838 'collection': collection
}
1840 super().__init
__(node
, self
._context
)
1843 svgGeometryClasses
= {
1844 'svg': SVGGeometrySVG
,
1845 'path': SVGGeometryPATH
,
1846 'defs': SVGGeometryDEFS
,
1847 'symbol': SVGGeometrySYMBOL
,
1848 'use': SVGGeometryUSE
,
1849 'rect': SVGGeometryRECT
,
1850 'ellipse': SVGGeometryELLIPSE
,
1851 'circle': SVGGeometryCIRCLE
,
1852 'line': SVGGeometryLINE
,
1853 'polyline': SVGGeometryPOLYLINE
,
1854 'polygon': SVGGeometryPOLYGON
,
1858 def parseAbstractNode(node
, context
):
1859 name
= node
.tagName
.lower()
1861 if name
.startswith('svg:'):
1864 geomClass
= svgGeometryClasses
.get(name
)
1866 if geomClass
is not None:
1867 ob
= geomClass(node
, context
)
1875 def load_svg(context
, filepath
, do_colormanage
):
1877 Load specified SVG file
1880 if bpy
.ops
.object.mode_set
.poll():
1881 bpy
.ops
.object.mode_set(mode
='OBJECT')
1883 loader
= SVGLoader(context
, filepath
, do_colormanage
)
1885 loader
.createGeom(False)
1888 def load(operator
, context
, filepath
=""):
1890 # error in code should raise exceptions but loading
1891 # non SVG files can give useful messages.
1892 do_colormanage
= context
.scene
.display_settings
.display_device
!= 'NONE'
1894 load_svg(context
, filepath
, do_colormanage
)
1895 except (xml
.parsers
.expat
.ExpatError
, UnicodeEncodeError) as e
:
1897 traceback
.print_exc()
1899 operator
.report({'WARNING'}, tip_("Unable to parse XML, %s:%s for file %r") % (type(e
).__name
__, e
, filepath
))
1900 return {'CANCELLED'}