1 # SPDX-License-Identifier: GPL-2.0-or-later
5 from math
import cos
, sin
, tan
, atan2
, pi
, ceil
8 from mathutils
import Vector
, Matrix
9 from bpy
.app
.translations
import pgettext_tip
as tip_
11 from . import svg_colors
12 from .svg_util
import (units
,
15 parse_array_of_floats
,
18 #### Common utilities ####
20 SVGEmptyStyles
= {'useFill': None,
24 def SVGCreateCurve(context
):
26 Create new curve object to hold splines in
29 cu
= bpy
.data
.curves
.new("Curve", 'CURVE')
30 obj
= bpy
.data
.objects
.new("Curve", cu
)
32 context
['collection'].objects
.link(obj
)
45 def SVGFlipHandle(x
, y
, x1
, y1
):
47 Flip handle around base point
56 def SVGParseCoord(coord
, size
):
58 Parse coordinate component to common basis
60 Needed to handle coordinates set in cm, mm, inches.
63 token
, last_char
= read_float(coord
)
65 unit
= coord
[last_char
:].strip() # strip() in case there is a space
68 return float(size
) / 100.0 * val
69 return val
* units
[unit
]
72 def SVGRectFromNode(node
, context
):
74 Get display rectangle from node
77 w
= context
['rect'][0]
78 h
= context
['rect'][1]
80 if node
.getAttribute('viewBox'):
81 viewBox
= node
.getAttribute('viewBox').replace(',', ' ').split()
82 w
= SVGParseCoord(viewBox
[2], w
)
83 h
= SVGParseCoord(viewBox
[3], h
)
85 if node
.getAttribute('width'):
86 w
= SVGParseCoord(node
.getAttribute('width'), w
)
88 if node
.getAttribute('height'):
89 h
= SVGParseCoord(node
.getAttribute('height'), h
)
94 def SVGMatrixFromNode(node
, context
):
96 Get transformation matrix from given node
99 tagName
= node
.tagName
.lower()
100 tags
= ['svg:svg', 'svg:use', 'svg:symbol']
102 if tagName
not in tags
and 'svg:' + tagName
not in tags
:
105 rect
= context
['rect']
106 has_user_coordinate
= (len(context
['rects']) > 1)
109 x
= SVGParseCoord(node
.getAttribute('x') or '0', rect
[0])
110 y
= SVGParseCoord(node
.getAttribute('y') or '0', rect
[1])
111 w
= SVGParseCoord(node
.getAttribute('width') or str(rect
[0]), rect
[0])
112 h
= SVGParseCoord(node
.getAttribute('height') or str(rect
[1]), rect
[1])
114 m
= Matrix
.Translation(Vector((x
, y
, 0.0)))
115 if has_user_coordinate
:
116 if rect
[0] != 0 and rect
[1] != 0:
117 m
= m
@ Matrix
.Scale(w
/ rect
[0], 4, Vector((1.0, 0.0, 0.0)))
118 m
= m
@ Matrix
.Scale(h
/ rect
[1], 4, Vector((0.0, 1.0, 0.0)))
120 if node
.getAttribute('viewBox'):
121 viewBox
= node
.getAttribute('viewBox').replace(',', ' ').split()
122 vx
= SVGParseCoord(viewBox
[0], w
)
123 vy
= SVGParseCoord(viewBox
[1], h
)
124 vw
= SVGParseCoord(viewBox
[2], w
)
125 vh
= SVGParseCoord(viewBox
[3], h
)
127 if vw
== 0 or vh
== 0:
130 if has_user_coordinate
or (w
!= 0 and h
!= 0):
139 tx
= (w
- vw
* scale
) / 2
140 ty
= (h
- vh
* scale
) / 2
141 m
= m
@ Matrix
.Translation(Vector((tx
, ty
, 0.0)))
143 m
= m
@ Matrix
.Translation(Vector((-vx
, -vy
, 0.0)))
144 m
= m
@ Matrix
.Scale(scale
, 4, Vector((1.0, 0.0, 0.0)))
145 m
= m
@ Matrix
.Scale(scale
, 4, Vector((0.0, 1.0, 0.0)))
150 def SVGParseTransform(transform
):
152 Parse transform string and return transformation matrix
156 r
= re
.compile(r
'\s*([A-z]+)\s*\((.*?)\)')
158 for match
in r
.finditer(transform
):
159 func
= match
.group(1)
160 params
= match
.group(2)
161 params
= params
.replace(',', ' ').split()
163 proc
= SVGTransforms
.get(func
)
165 raise Exception('Unknown transform function: ' + func
)
172 def SVGGetMaterial(color
, context
):
174 Get material for specified color
177 materials
= context
['materials']
178 rgb_re
= re
.compile(r
'^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
180 if color
in materials
:
181 return materials
[color
]
184 if color
.startswith('#'):
188 color
= color
[0] * 2 + color
[1] * 2 + color
[2] * 2
190 diff
= (int(color
[0:2], 16), int(color
[2:4], 16), int(color
[4:6], 16))
191 elif color
in svg_colors
.SVGColors
:
192 diff
= svg_colors
.SVGColors
[color
]
193 elif rgb_re
.match(color
):
194 c
= rgb_re
.findall(color
)[0]
195 diff
= (float(c
[0]), float(c
[1]), float(c
[2]))
199 diffuse_color
= ([x
/ 255.0 for x
in diff
])
201 if context
['do_colormanage']:
202 diffuse_color
[0] = srgb_to_linearrgb(diffuse_color
[0])
203 diffuse_color
[1] = srgb_to_linearrgb(diffuse_color
[1])
204 diffuse_color
[2] = srgb_to_linearrgb(diffuse_color
[2])
206 mat
= bpy
.data
.materials
.new(name
='SVGMat')
207 mat
.diffuse_color
= (*diffuse_color
, 1.0)
209 materials
[color
] = mat
214 def SVGTransformTranslate(params
):
216 translate SVG transform command
219 tx
= float(params
[0])
220 ty
= float(params
[1]) if len(params
) > 1 else 0.0
222 return Matrix
.Translation(Vector((tx
, ty
, 0.0)))
225 def SVGTransformMatrix(params
):
227 matrix SVG transform command
237 return Matrix(((a
, c
, 0.0, e
),
243 def SVGTransformScale(params
):
245 scale SVG transform command
248 sx
= float(params
[0])
249 sy
= float(params
[1]) if len(params
) > 1 else sx
253 m
= m
@ Matrix
.Scale(sx
, 4, Vector((1.0, 0.0, 0.0)))
254 m
= m
@ Matrix
.Scale(sy
, 4, Vector((0.0, 1.0, 0.0)))
259 def SVGTransformSkewY(params
):
261 skewY SVG transform command
264 ang
= float(params
[0]) * pi
/ 180.0
266 return Matrix(((1.0, 0.0, 0.0),
267 (tan(ang
), 1.0, 0.0),
268 (0.0, 0.0, 1.0))).to_4x4()
271 def SVGTransformSkewX(params
):
273 skewX SVG transform command
276 ang
= float(params
[0]) * pi
/ 180.0
278 return Matrix(((1.0, tan(ang
), 0.0),
280 (0.0, 0.0, 1.0))).to_4x4()
283 def SVGTransformRotate(params
):
285 skewX SVG transform command
288 ang
= float(params
[0]) * pi
/ 180.0
292 cx
= float(params
[1])
293 cy
= float(params
[2])
295 tm
= Matrix
.Translation(Vector((cx
, cy
, 0.0)))
296 rm
= Matrix
.Rotation(ang
, 4, Vector((0.0, 0.0, 1.0)))
298 return tm
@ rm
@ tm
.inverted()
300 SVGTransforms
= {'translate': SVGTransformTranslate
,
301 'scale': SVGTransformScale
,
302 'skewX': SVGTransformSkewX
,
303 'skewY': SVGTransformSkewY
,
304 'matrix': SVGTransformMatrix
,
305 'rotate': SVGTransformRotate
}
308 def SVGParseStyles(node
, context
):
310 Parse node to get different styles for displaying geometries
311 (materials, filling flags, etc..)
314 styles
= SVGEmptyStyles
.copy()
316 style
= node
.getAttribute('style')
318 elems
= style
.split(';')
325 name
= s
[0].strip().lower()
331 styles
['useFill'] = False
333 styles
['useFill'] = True
334 styles
['fill'] = SVGGetMaterial(val
, context
)
336 if styles
['useFill'] is None:
337 styles
['useFill'] = True
338 styles
['fill'] = SVGGetMaterial('#000', context
)
342 if styles
['useFill'] is None:
343 fill
= node
.getAttribute('fill')
347 styles
['useFill'] = False
349 styles
['useFill'] = True
350 styles
['fill'] = SVGGetMaterial(fill
, context
)
352 if styles
['useFill'] is None and context
['style']:
353 styles
= context
['style'].copy()
355 if styles
['useFill'] is None:
356 styles
['useFill'] = True
357 styles
['fill'] = SVGGetMaterial('#000', context
)
361 def id_names_from_node(node
, ob
):
362 if node
.getAttribute('id'):
363 name
= node
.getAttribute('id')
367 #### SVG path helpers ####
372 SVG Path data token supplier
375 __slots__
= ('_data', # List of tokens
376 '_index', # Index of current token in tokens list
377 '_len') # Length of tokens list
379 def __init__(self
, d
):
381 Initialize new path data supplier
383 d - the definition of the outline of a shape
387 commands
= {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
398 elif c
.lower() in commands
:
402 elif c
in ['-', '.'] or c
.isdigit():
403 # Special case for 'a/A' commands.
404 # Arguments 4 and 5 are either 0 or 1 and might not
405 # be separated from the next argument with space or comma.
406 if current_command
.lower() == 'a':
407 if arg_index
% 7 in [4,5]:
411 token
, last_char
= read_float(d
, i
)
413 token
, last_char
= read_float(d
, i
)
418 # in most cases len(token) and (last_char - i) are the same
419 # but with whitespace or ',' prefix they are not.
421 i
+= (last_char
- i
) - 1
427 self
._len
= len(tokens
)
431 Check if end of data reached
434 return self
._index
>= self
._len
444 return self
._data
[self
._index
]
446 def lookupNext(self
):
448 get next token without moving pointer
454 return self
._data
[self
._index
]
458 Return current token and go to next one
464 token
= self
._data
[self
._index
]
471 Return coordinate created from current token and move to next token
484 Parser of SVG path data
487 __slots__
= ('_data', # Path data supplird
488 '_point', # Current point coordinate
489 '_handle', # Last handle coordinate
490 '_splines', # List of all splies created during parsing
491 '_spline', # Currently handling spline
492 '_commands', # Hash of all supported path commands
493 '_use_fill', # Splines would be filled, so expected to be closed
496 def __init__(self
, d
, use_fill
):
498 Initialize path parser
500 d - the definition of the outline of a shape
503 self
._data
= SVGPathData(d
)
504 self
._point
= None # Current point
505 self
._handle
= None # Last handle
506 self
._splines
= [] # List of splines in path
507 self
._spline
= None # Current spline
508 self
._use
_fill
= use_fill
510 self
._commands
= {'M': self
._pathMoveTo
,
511 'L': self
._pathLineTo
,
512 'H': self
._pathLineTo
,
513 'V': self
._pathLineTo
,
514 'C': self
._pathCurveToCS
,
515 'S': self
._pathCurveToCS
,
516 'Q': self
._pathCurveToQT
,
517 'T': self
._pathCurveToQT
,
518 'A': self
._pathCurveToA
,
519 'Z': self
._pathClose
,
521 'm': self
._pathMoveTo
,
522 'l': self
._pathLineTo
,
523 'h': self
._pathLineTo
,
524 'v': self
._pathLineTo
,
525 'c': self
._pathCurveToCS
,
526 's': self
._pathCurveToCS
,
527 'q': self
._pathCurveToQT
,
528 't': self
._pathCurveToQT
,
529 'a': self
._pathCurveToA
,
530 'z': self
._pathClose
}
532 def _getCoordPair(self
, relative
, point
):
534 Get next coordinate pair
537 x
= self
._data
.nextCoord()
538 y
= self
._data
.nextCoord()
540 if relative
and point
is not None:
546 def _appendPoint(self
, x
, y
, handle_left
=None, handle_left_type
='VECTOR',
547 handle_right
=None, handle_right_type
='VECTOR'):
549 Append point to spline
551 If there's no active spline, create one and set it's first point
552 to current point coordinate
555 if self
._spline
is None:
556 self
._spline
= {'points': [],
559 self
._splines
.append(self
._spline
)
561 if len(self
._spline
['points']) > 0:
562 # Not sure about specifications, but Illustrator could create
563 # last point at the same position, as start point (which was
564 # reached by MoveTo command) to set needed handle coords.
565 # It's also could use last point at last position to make path
568 first
= self
._spline
['points'][0]
569 if check_points_equal((first
['x'], first
['y']), (x
, y
)):
570 if handle_left
is not None:
571 first
['handle_left'] = handle_left
572 first
['handle_left_type'] = 'FREE'
574 if handle_left_type
!= 'VECTOR':
575 first
['handle_left_type'] = handle_left_type
577 if self
._data
.eof() or self
._data
.lookupNext().lower() == 'm':
578 self
._spline
['closed'] = True
582 last
= self
._spline
['points'][-1]
583 if last
['handle_right_type'] == 'VECTOR' and handle_left_type
== 'FREE':
584 last
['handle_right'] = (last
['x'], last
['y'])
585 last
['handle_right_type'] = 'FREE'
586 if last
['handle_right_type'] == 'FREE' and handle_left_type
== 'VECTOR':
588 handle_left_type
= 'FREE'
593 'handle_left': handle_left
,
594 'handle_left_type': handle_left_type
,
596 'handle_right': handle_right
,
597 'handle_right_type': handle_right_type
}
599 self
._spline
['points'].append(point
)
601 def _updateHandle(self
, handle
=None, handle_type
=None):
603 Update right handle of previous point when adding new point to spline
606 point
= self
._spline
['points'][-1]
608 if handle_type
is not None:
609 point
['handle_right_type'] = handle_type
611 if handle
is not None:
612 point
['handle_right'] = handle
614 def _pathMoveTo(self
, code
):
619 relative
= code
.islower()
620 x
, y
= self
._getCoordPair
(relative
, self
._point
)
622 self
._spline
= None # Flag to start new spline
625 cur
= self
._data
.cur()
626 while cur
is not None and not cur
.isalpha():
627 x
, y
= self
._getCoordPair
(relative
, self
._point
)
629 if self
._spline
is None:
630 self
._appendPoint
(self
._point
[0], self
._point
[1])
632 self
._appendPoint
(x
, y
)
635 cur
= self
._data
.cur()
639 def _pathLineTo(self
, code
):
646 cur
= self
._data
.cur()
647 while cur
is not None and not cur
.isalpha():
649 x
, y
= self
._getCoordPair
(code
== 'l', self
._point
)
651 x
= self
._data
.nextCoord()
655 y
= self
._data
.nextCoord()
662 if self
._spline
is None:
663 self
._appendPoint
(self
._point
[0], self
._point
[1])
665 self
._appendPoint
(x
, y
)
668 cur
= self
._data
.cur()
672 def _pathCurveToCS(self
, code
):
674 Cubic BEZIER CurveTo path command
678 cur
= self
._data
.cur()
679 while cur
is not None and not cur
.isalpha():
681 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
682 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
684 if self
._handle
is not None:
685 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
686 self
._handle
[0], self
._handle
[1])
690 x2
, y2
= self
._getCoordPair
(code
.islower(), self
._point
)
692 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
694 if self
._spline
is None:
695 self
._appendPoint
(self
._point
[0], self
._point
[1],
696 handle_left_type
='FREE', handle_left
=self
._point
,
697 handle_right_type
='FREE', handle_right
=(x1
, y1
))
699 self
._updateHandle
(handle
=(x1
, y1
), handle_type
='FREE')
701 self
._appendPoint
(x
, y
,
702 handle_left_type
='FREE', handle_left
=(x2
, y2
),
703 handle_right_type
='FREE', handle_right
=(x
, y
))
706 self
._handle
= (x2
, y2
)
707 cur
= self
._data
.cur()
709 def _pathCurveToQT(self
, code
):
711 Quadratic BEZIER CurveTo path command
715 cur
= self
._data
.cur()
717 while cur
is not None and not cur
.isalpha():
719 x1
, y1
= self
._getCoordPair
(code
.islower(), self
._point
)
721 if self
._handle
is not None:
722 x1
, y1
= SVGFlipHandle(self
._point
[0], self
._point
[1],
723 self
._handle
[0], self
._handle
[1])
727 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
729 if not check_points_equal((x
, y
), self
._point
):
730 if self
._spline
is None:
731 self
._appendPoint
(self
._point
[0], self
._point
[1],
732 handle_left_type
='FREE', handle_left
=self
._point
,
733 handle_right_type
='FREE', handle_right
=self
._point
)
735 self
._appendPoint
(x
, y
,
736 handle_left_type
='FREE', handle_left
=(x1
, y1
),
737 handle_right_type
='FREE', handle_right
=(x
, y
))
740 self
._handle
= (x1
, y1
)
741 cur
= self
._data
.cur()
743 def _calcArc(self
, rx
, ry
, ang
, fa
, fs
, x
, y
):
747 Copied and adoptedfrom paths_svg2obj.py script for Blender 2.49
748 which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
755 px
= abs((cos(ang
) * (cpx
- x
) + sin(ang
) * (cpy
- y
)) * 0.5) ** 2.0
756 py
= abs((cos(ang
) * (cpy
- y
) - sin(ang
) * (cpx
- x
)) * 0.5) ** 2.0
760 px
= px
/ (rx
** 2.0)
763 rpy
= py
/ (ry
** 2.0)
771 carx
= sarx
= cary
= sary
= 0.0
781 x0
= carx
* cpx
+ sarx
* cpy
782 y0
= -sary
* cpx
+ cary
* cpy
783 x1
= carx
* x
+ sarx
* y
784 y1
= -sary
* x
+ cary
* y
785 d
= (x1
- x0
) * (x1
- x0
) + (y1
- y0
) * (y1
- y0
)
799 xc
= 0.5 * (x0
+ x1
) - sf
* (y1
- y0
)
800 yc
= 0.5 * (y0
+ y1
) + sf
* (x1
- x0
)
801 ang_0
= atan2(y0
- yc
, x0
- xc
)
802 ang_1
= atan2(y1
- yc
, x1
- xc
)
803 ang_arc
= ang_1
- ang_0
805 if ang_arc
< 0.0 and fs
== 1:
807 elif ang_arc
> 0.0 and fs
== 0:
810 n_segs
= int(ceil(abs(ang_arc
* 2.0 / (pi
* 0.5 + 0.001))))
812 if self
._spline
is None:
813 self
._appendPoint
(cpx
, cpy
,
814 handle_left_type
='FREE', handle_left
=(cpx
, cpy
),
815 handle_right_type
='FREE', handle_right
=(cpx
, cpy
))
817 for i
in range(n_segs
):
818 ang0
= ang_0
+ i
* ang_arc
/ n_segs
819 ang1
= ang_0
+ (i
+ 1) * ang_arc
/ n_segs
820 ang_demi
= 0.25 * (ang1
- ang0
)
821 t
= 2.66666 * sin(ang_demi
) * sin(ang_demi
) / sin(ang_demi
* 2.0)
822 x1
= xc
+ cos(ang0
) - t
* sin(ang0
)
823 y1
= yc
+ sin(ang0
) + t
* cos(ang0
)
826 x3
= x2
+ t
* sin(ang1
)
827 y3
= y2
- t
* cos(ang1
)
829 coord1
= ((cos(ang
) * rx
) * x1
+ (-sin(ang
) * ry
) * y1
,
830 (sin(ang
) * rx
) * x1
+ (cos(ang
) * ry
) * y1
)
831 coord2
= ((cos(ang
) * rx
) * x3
+ (-sin(ang
) * ry
) * y3
,
832 (sin(ang
) * rx
) * x3
+ (cos(ang
) * ry
) * y3
)
833 coord3
= ((cos(ang
) * rx
) * x2
+ (-sin(ang
) * ry
) * y2
,
834 (sin(ang
) * rx
) * x2
+ (cos(ang
) * ry
) * y2
)
836 self
._updateHandle
(handle
=coord1
, handle_type
='FREE')
838 self
._appendPoint
(coord3
[0], coord3
[1],
839 handle_left_type
='FREE', handle_left
=coord2
,
840 handle_right_type
='FREE', handle_right
=coord3
)
842 def _pathCurveToA(self
, code
):
844 Elliptical arc CurveTo path command
847 cur
= self
._data
.cur()
849 while cur
is not None and not cur
.isalpha():
850 rx
= float(self
._data
.next())
851 ry
= float(self
._data
.next())
852 ang
= float(self
._data
.next()) / 180 * pi
853 fa
= float(self
._data
.next())
854 fs
= float(self
._data
.next())
855 x
, y
= self
._getCoordPair
(code
.islower(), self
._point
)
857 self
._calcArc
(rx
, ry
, ang
, fa
, fs
, x
, y
)
861 cur
= self
._data
.cur()
863 def _pathClose(self
, code
):
869 self
._spline
['closed'] = True
871 cv
= self
._spline
['points'][0]
872 self
._point
= (cv
['x'], cv
['y'])
874 def _pathCloseImplicitly(self
):
876 Close path implicitly without changing current point coordinate
880 self
._spline
['closed'] = True
889 while not self
._data
.eof():
890 code
= self
._data
.next()
891 cmd
= self
._commands
.get(code
)
894 raise Exception('Unknown path command: {0}' . format(code
))
896 if code
in {'Z', 'z'}:
901 if code
in {'M', 'm'} and self
._use
_fill
and not closed
:
902 self
._pathCloseImplicitly
() # Ensure closed before MoveTo path command
905 if self
._use
_fill
and not closed
:
906 self
._pathCloseImplicitly
() # Ensure closed at the end of parsing
908 def getSplines(self
):
910 Get splines definitions
918 Abstract SVG geometry
921 __slots__
= ('_node', # XML node for geometry
922 '_context', # Global SVG context (holds matrices stack, i.e.)
923 '_creating') # Flag if geometry is already creating
925 # need to detect cycles for USE node
927 def __init__(self
, node
, context
):
929 Initialize SVG geometry
933 self
._context
= context
934 self
._creating
= False
936 if hasattr(node
, 'getAttribute'):
937 defs
= context
['defines']
939 attr_id
= node
.getAttribute('id')
940 if attr_id
and defs
.get('#' + attr_id
) is None:
941 defs
['#' + attr_id
] = self
943 className
= node
.getAttribute('class')
944 if className
and defs
.get(className
) is None:
945 defs
[className
] = self
947 def _pushRect(self
, rect
):
949 Push display rectangle
952 self
._context
['rects'].append(rect
)
953 self
._context
['rect'] = rect
957 Pop display rectangle
960 self
._context
['rects'].pop()
961 self
._context
['rect'] = self
._context
['rects'][-1]
963 def _pushMatrix(self
, matrix
):
965 Push transformation matrix
968 current_matrix
= self
._context
['matrix']
969 self
._context
['matrix_stack'].append(current_matrix
)
970 self
._context
['matrix'] = current_matrix
@ matrix
972 def _popMatrix(self
):
974 Pop transformation matrix
977 old_matrix
= self
._context
['matrix_stack'].pop()
978 self
._context
['matrix'] = old_matrix
980 def _pushStyle(self
, style
):
985 self
._context
['styles'].append(style
)
986 self
._context
['style'] = style
993 self
._context
['styles'].pop()
994 self
._context
['style'] = self
._context
['styles'][-1]
996 def _transformCoord(self
, point
):
998 Transform SVG-file coords
1001 v
= Vector((point
[0], point
[1], 0.0))
1003 return self
._context
['matrix'] @ v
1005 def getNodeMatrix(self
):
1007 Get transformation matrix of node
1010 return SVGMatrixFromNode(self
._node
, self
._context
)
1014 Parse XML node to memory
1019 def _doCreateGeom(self
, instancing
):
1021 Internal handler to create real geometries
1026 def getTransformMatrix(self
):
1028 Get matrix created from "transform" attribute
1031 transform
= self
._node
.getAttribute('transform')
1034 return SVGParseTransform(transform
)
1038 def createGeom(self
, instancing
):
1040 Create real geometries
1046 self
._creating
= True
1048 matrix
= self
.getTransformMatrix()
1049 if matrix
is not None:
1050 self
._pushMatrix
(matrix
)
1052 self
._doCreateGeom
(instancing
)
1054 if matrix
is not None:
1057 self
._creating
= False
1060 class SVGGeometryContainer(SVGGeometry
):
1062 Container of SVG geometries
1065 __slots__
= ('_geometries', # List of chold geometries
1066 '_styles') # Styles, used for displaying
1068 def __init__(self
, node
, context
):
1070 Initialize SVG geometry container
1073 super().__init
__(node
, context
)
1075 self
._geometries
= []
1076 self
._styles
= SVGEmptyStyles
1080 Parse XML node to memory
1083 if type(self
._node
) is xml
.dom
.minidom
.Element
:
1084 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1086 self
._pushStyle
(self
._styles
)
1088 for node
in self
._node
.childNodes
:
1089 if type(node
) is not xml
.dom
.minidom
.Element
:
1092 ob
= parseAbstractNode(node
, self
._context
)
1094 self
._geometries
.append(ob
)
1098 def _doCreateGeom(self
, instancing
):
1100 Create real geometries
1103 for geom
in self
._geometries
:
1104 geom
.createGeom(instancing
)
1106 def getGeometries(self
):
1108 Get list of parsed geometries
1111 return self
._geometries
1114 class SVGGeometryPATH(SVGGeometry
):
1119 __slots__
= ('_splines', # List of splines after parsing
1120 '_styles') # Styles, used for displaying
1122 def __init__(self
, node
, context
):
1127 super().__init
__(node
, context
)
1130 self
._styles
= SVGEmptyStyles
1137 d
= self
._node
.getAttribute('d')
1139 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1141 pathParser
= SVGPathParser(d
, self
._styles
['useFill'])
1144 self
._splines
= pathParser
.getSplines()
1146 def _doCreateGeom(self
, instancing
):
1148 Create real geometries
1151 ob
= SVGCreateCurve(self
._context
)
1154 id_names_from_node(self
._node
, ob
)
1156 if self
._styles
['useFill']:
1157 cu
.dimensions
= '2D'
1158 cu
.fill_mode
= 'BOTH'
1159 cu
.materials
.append(self
._styles
['fill'])
1161 cu
.dimensions
= '3D'
1163 for spline
in self
._splines
:
1166 if spline
['closed'] and len(spline
['points']) >= 2:
1167 first
= spline
['points'][0]
1168 last
= spline
['points'][-1]
1169 if ( first
['handle_left_type'] == 'FREE' and
1170 last
['handle_right_type'] == 'VECTOR'):
1171 last
['handle_right_type'] = 'FREE'
1172 last
['handle_right'] = (last
['x'], last
['y'])
1173 if ( last
['handle_right_type'] == 'FREE' and
1174 first
['handle_left_type'] == 'VECTOR'):
1175 first
['handle_left_type'] = 'FREE'
1176 first
['handle_left'] = (first
['x'], first
['y'])
1178 for point
in spline
['points']:
1179 co
= self
._transformCoord
((point
['x'], point
['y']))
1181 if act_spline
is None:
1182 cu
.splines
.new('BEZIER')
1184 act_spline
= cu
.splines
[-1]
1185 act_spline
.use_cyclic_u
= spline
['closed']
1187 act_spline
.bezier_points
.add(1)
1189 bezt
= act_spline
.bezier_points
[-1]
1192 bezt
.handle_left_type
= point
['handle_left_type']
1193 if point
['handle_left'] is not None:
1194 handle
= point
['handle_left']
1195 bezt
.handle_left
= self
._transformCoord
(handle
)
1197 bezt
.handle_right_type
= point
['handle_right_type']
1198 if point
['handle_right'] is not None:
1199 handle
= point
['handle_right']
1200 bezt
.handle_right
= self
._transformCoord
(handle
)
1205 class SVGGeometryDEFS(SVGGeometryContainer
):
1207 Container for referenced elements
1210 def createGeom(self
, instancing
):
1212 Create real geometries
1218 class SVGGeometrySYMBOL(SVGGeometryContainer
):
1223 def _doCreateGeom(self
, instancing
):
1225 Create real geometries
1228 self
._pushMatrix
(self
.getNodeMatrix())
1230 super()._doCreateGeom
(False)
1234 def createGeom(self
, instancing
):
1236 Create real geometries
1242 super().createGeom(instancing
)
1245 class SVGGeometryG(SVGGeometryContainer
):
1253 class SVGGeometryUSE(SVGGeometry
):
1255 User of referenced elements
1258 def _doCreateGeom(self
, instancing
):
1260 Create real geometries
1263 ref
= self
._node
.getAttribute('xlink:href')
1264 geom
= self
._context
['defines'].get(ref
)
1266 if geom
is not None:
1267 rect
= SVGRectFromNode(self
._node
, self
._context
)
1268 self
._pushRect
(rect
)
1270 self
._pushMatrix
(self
.getNodeMatrix())
1272 geom
.createGeom(True)
1279 class SVGGeometryRECT(SVGGeometry
):
1284 __slots__
= ('_rect', # coordinate and dimensions of rectangle
1285 '_radius', # Rounded corner radiuses
1286 '_styles') # Styles, used for displaying
1288 def __init__(self
, node
, context
):
1290 Initialize new rectangle
1293 super().__init
__(node
, context
)
1295 self
._rect
= ('0', '0', '0', '0')
1296 self
._radius
= ('0', '0')
1297 self
._styles
= SVGEmptyStyles
1301 Parse SVG rectangle node
1304 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1307 for attr
in ['x', 'y', 'width', 'height']:
1308 val
= self
._node
.getAttribute(attr
)
1309 rect
.append(val
or '0')
1313 rx
= self
._node
.getAttribute('rx')
1314 ry
= self
._node
.getAttribute('ry')
1316 self
._radius
= (rx
, ry
)
1318 def _appendCorner(self
, spline
, coord
, firstTime
, rounded
):
1320 Append new corner to rectangle
1325 handle
= self
._transformCoord
(coord
[2])
1326 coord
= (coord
[0], coord
[1])
1328 co
= self
._transformCoord
(coord
)
1331 spline
.bezier_points
.add(1)
1333 bezt
= spline
.bezier_points
[-1]
1338 bezt
.handle_left_type
= 'VECTOR'
1339 bezt
.handle_right_type
= 'FREE'
1341 bezt
.handle_right
= handle
1343 bezt
.handle_left_type
= 'FREE'
1344 bezt
.handle_right_type
= 'VECTOR'
1345 bezt
.handle_left
= co
1348 bezt
.handle_left_type
= 'VECTOR'
1349 bezt
.handle_right_type
= 'VECTOR'
1351 def _doCreateGeom(self
, instancing
):
1353 Create real geometries
1356 # Run-time parsing -- percents would be correct only if
1358 crect
= self
._context
['rect']
1362 rect
.append(SVGParseCoord(self
._rect
[i
], crect
[i
% 2]))
1368 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1369 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1371 rx
= min(SVGParseCoord(r
[0], rect
[0]), rect
[2] / 2)
1372 ry
= min(rx
, rect
[3] / 2)
1373 rx
= ry
= min(rx
, ry
)
1375 ry
= min(SVGParseCoord(r
[1], rect
[1]), rect
[3] / 2)
1376 rx
= min(ry
, rect
[2] / 2)
1377 rx
= ry
= min(rx
, ry
)
1382 ob
= SVGCreateCurve(self
._context
)
1385 id_names_from_node(self
._node
, ob
)
1387 if self
._styles
['useFill']:
1388 cu
.dimensions
= '2D'
1389 cu
.fill_mode
= 'BOTH'
1390 cu
.materials
.append(self
._styles
['fill'])
1392 cu
.dimensions
= '3D'
1394 cu
.splines
.new('BEZIER')
1396 spline
= cu
.splines
[-1]
1397 spline
.use_cyclic_u
= True
1399 x
, y
= rect
[0], rect
[1]
1400 w
, h
= rect
[2], rect
[3]
1401 rx
, ry
= radius
[0], radius
[1]
1418 # Optional third component -- right handle coord
1419 coords
= [(x
+ rx
, y
),
1420 (x
+ w
- rx
, y
, (x
+ w
, y
)),
1422 (x
+ w
, y
+ h
- ry
, (x
+ w
, y
+ h
)),
1423 (x
+ w
- rx
, y
+ h
),
1424 (x
+ rx
, y
+ h
, (x
, y
+ h
)),
1426 (x
, y
+ ry
, (x
, y
))]
1430 coords
= [(x
, y
), (x
+ w
, y
), (x
+ w
, y
+ h
), (x
, y
+ h
)]
1433 for coord
in coords
:
1434 self
._appendCorner
(spline
, coord
, firstTime
, rounded
)
1440 class SVGGeometryELLIPSE(SVGGeometry
):
1445 __slots__
= ('_cx', # X-coordinate of center
1446 '_cy', # Y-coordinate of center
1447 '_rx', # X-axis radius of circle
1448 '_ry', # Y-axis radius of circle
1449 '_styles') # Styles, used for displaying
1451 def __init__(self
, node
, context
):
1453 Initialize new ellipse
1456 super().__init
__(node
, context
)
1462 self
._styles
= SVGEmptyStyles
1466 Parse SVG ellipse node
1469 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1471 self
._cx
= self
._node
.getAttribute('cx') or '0'
1472 self
._cy
= self
._node
.getAttribute('cy') or '0'
1473 self
._rx
= self
._node
.getAttribute('rx') or '0'
1474 self
._ry
= self
._node
.getAttribute('ry') or '0'
1476 def _doCreateGeom(self
, instancing
):
1478 Create real geometries
1481 # Run-time parsing -- percents would be correct only if
1483 crect
= self
._context
['rect']
1485 cx
= SVGParseCoord(self
._cx
, crect
[0])
1486 cy
= SVGParseCoord(self
._cy
, crect
[1])
1487 rx
= SVGParseCoord(self
._rx
, crect
[0])
1488 ry
= SVGParseCoord(self
._ry
, crect
[1])
1490 if not rx
or not ry
:
1491 # Automaic handles will work incorrect in this case
1495 ob
= SVGCreateCurve(self
._context
)
1498 id_names_from_node(self
._node
, ob
)
1500 if self
._styles
['useFill']:
1501 cu
.dimensions
= '2D'
1502 cu
.fill_mode
= 'BOTH'
1503 cu
.materials
.append(self
._styles
['fill'])
1505 cu
.dimensions
= '3D'
1507 coords
= [((cx
- rx
, cy
),
1508 (cx
- rx
, cy
+ ry
* 0.552),
1509 (cx
- rx
, cy
- ry
* 0.552)),
1512 (cx
- rx
* 0.552, cy
- ry
),
1513 (cx
+ rx
* 0.552, cy
- ry
)),
1516 (cx
+ rx
, cy
- ry
* 0.552),
1517 (cx
+ rx
, cy
+ ry
* 0.552)),
1520 (cx
+ rx
* 0.552, cy
+ ry
),
1521 (cx
- rx
* 0.552, cy
+ ry
))]
1524 for coord
in coords
:
1525 co
= self
._transformCoord
(coord
[0])
1526 handle_left
= self
._transformCoord
(coord
[1])
1527 handle_right
= self
._transformCoord
(coord
[2])
1530 cu
.splines
.new('BEZIER')
1531 spline
= cu
.splines
[-1]
1532 spline
.use_cyclic_u
= True
1534 spline
.bezier_points
.add(1)
1536 bezt
= spline
.bezier_points
[-1]
1538 bezt
.handle_left_type
= 'FREE'
1539 bezt
.handle_right_type
= 'FREE'
1540 bezt
.handle_left
= handle_left
1541 bezt
.handle_right
= handle_right
1546 class SVGGeometryCIRCLE(SVGGeometryELLIPSE
):
1553 Parse SVG circle node
1556 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1558 self
._cx
= self
._node
.getAttribute('cx') or '0'
1559 self
._cy
= self
._node
.getAttribute('cy') or '0'
1561 r
= self
._node
.getAttribute('r') or '0'
1562 self
._rx
= self
._ry
= r
1565 class SVGGeometryLINE(SVGGeometry
):
1570 __slots__
= ('_x1', # X-coordinate of beginning
1571 '_y1', # Y-coordinate of beginning
1572 '_x2', # X-coordinate of ending
1573 '_y2') # Y-coordinate of ending
1575 def __init__(self
, node
, context
):
1580 super().__init
__(node
, context
)
1592 self
._x
1 = self
._node
.getAttribute('x1') or '0'
1593 self
._y
1 = self
._node
.getAttribute('y1') or '0'
1594 self
._x
2 = self
._node
.getAttribute('x2') or '0'
1595 self
._y
2 = self
._node
.getAttribute('y2') or '0'
1597 def _doCreateGeom(self
, instancing
):
1599 Create real geometries
1602 # Run-time parsing -- percents would be correct only if
1604 crect
= self
._context
['rect']
1606 x1
= SVGParseCoord(self
._x
1, crect
[0])
1607 y1
= SVGParseCoord(self
._y
1, crect
[1])
1608 x2
= SVGParseCoord(self
._x
2, crect
[0])
1609 y2
= SVGParseCoord(self
._y
2, crect
[1])
1612 ob
= SVGCreateCurve(self
._context
)
1615 id_names_from_node(self
._node
, ob
)
1617 coords
= [(x1
, y1
), (x2
, y2
)]
1620 for coord
in coords
:
1621 co
= self
._transformCoord
(coord
)
1624 cu
.splines
.new('BEZIER')
1625 spline
= cu
.splines
[-1]
1626 spline
.use_cyclic_u
= True
1628 spline
.bezier_points
.add(1)
1630 bezt
= spline
.bezier_points
[-1]
1632 bezt
.handle_left_type
= 'VECTOR'
1633 bezt
.handle_right_type
= 'VECTOR'
1638 class SVGGeometryPOLY(SVGGeometry
):
1640 Abstract class for handling poly-geometries
1641 (polylines and polygons)
1644 __slots__
= ('_points', # Array of points for poly geometry
1645 '_styles', # Styles, used for displaying
1646 '_closed') # Should generated curve be closed?
1648 def __init__(self
, node
, context
):
1650 Initialize new poly geometry
1653 super().__init
__(node
, context
)
1656 self
._styles
= SVGEmptyStyles
1657 self
._closed
= False
1664 self
._styles
= SVGParseStyles(self
._node
, self
._context
)
1666 points
= parse_array_of_floats(self
._node
.getAttribute('points'))
1675 self
._points
.append((prev
, p
))
1678 def _doCreateGeom(self
, instancing
):
1680 Create real geometries
1683 ob
= SVGCreateCurve(self
._context
)
1686 id_names_from_node(self
._node
, ob
)
1688 if self
._closed
and self
._styles
['useFill']:
1689 cu
.dimensions
= '2D'
1690 cu
.fill_mode
= 'BOTH'
1691 cu
.materials
.append(self
._styles
['fill'])
1693 cu
.dimensions
= '3D'
1697 for point
in self
._points
:
1698 co
= self
._transformCoord
(point
)
1701 cu
.splines
.new('BEZIER')
1702 spline
= cu
.splines
[-1]
1703 spline
.use_cyclic_u
= self
._closed
1705 spline
.bezier_points
.add(1)
1707 bezt
= spline
.bezier_points
[-1]
1709 bezt
.handle_left_type
= 'VECTOR'
1710 bezt
.handle_right_type
= 'VECTOR'
1715 class SVGGeometryPOLYLINE(SVGGeometryPOLY
):
1717 SVG polyline geometry
1723 class SVGGeometryPOLYGON(SVGGeometryPOLY
):
1725 SVG polygon geometry
1728 def __init__(self
, node
, context
):
1730 Initialize new polygon geometry
1733 super().__init
__(node
, context
)
1738 class SVGGeometrySVG(SVGGeometryContainer
):
1740 Main geometry holder
1743 def _doCreateGeom(self
, instancing
):
1745 Create real geometries
1748 rect
= SVGRectFromNode(self
._node
, self
._context
)
1750 matrix
= self
.getNodeMatrix()
1752 # Better SVG compatibility: match svg-document units
1753 # with blender units
1758 if self
._node
.getAttribute('height'):
1759 raw_height
= self
._node
.getAttribute('height')
1760 token
, last_char
= read_float(raw_height
)
1761 document_height
= float(token
)
1762 unit
= raw_height
[last_char
:].strip()
1764 if self
._node
.getAttribute('viewBox'):
1765 viewbox
= parse_array_of_floats(self
._node
.getAttribute('viewBox'))
1767 if len(viewbox
) == 4 and unit
in ('cm', 'mm', 'in', 'pt', 'pc'):
1769 #convert units to BU:
1770 unitscale
= units
[unit
] / 90 * 1000 / 39.3701
1772 #apply blender unit scale:
1773 unitscale
= unitscale
/ bpy
.context
.scene
.unit_settings
.scale_length
1775 matrix
= matrix
@ Matrix
.Scale(unitscale
, 4, Vector((1.0, 0.0, 0.0)))
1776 matrix
= matrix
@ Matrix
.Scale(unitscale
, 4, Vector((0.0, 1.0, 0.0)))
1778 # match document origin with 3D space origin.
1779 if self
._node
.getAttribute('viewBox'):
1780 viewbox
= parse_array_of_floats(self
._node
.getAttribute('viewBox'))
1781 matrix
= matrix
@ matrix
.Translation([0.0, - viewbox
[1] - viewbox
[3], 0.0])
1783 self
._pushMatrix
(matrix
)
1784 self
._pushRect
(rect
)
1786 super()._doCreateGeom
(False)
1792 class SVGLoader(SVGGeometryContainer
):
1797 def getTransformMatrix(self
):
1799 Get matrix created from "transform" attribute
1802 # SVG document doesn't support transform specification
1803 # it can't even hold attributes
1807 def __init__(self
, context
, filepath
, do_colormanage
):
1809 Initialize SVG loader
1813 svg_name
= os
.path
.basename(filepath
)
1814 scene
= context
.scene
1815 collection
= bpy
.data
.collections
.new(name
=svg_name
)
1816 scene
.collection
.children
.link(collection
)
1818 node
= xml
.dom
.minidom
.parse(filepath
)
1821 m
= m
@ Matrix
.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
1822 m
= m
@ Matrix
.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
1826 self
._context
= {'defines': {},
1834 'do_colormanage': do_colormanage
,
1835 'collection': collection
}
1837 super().__init
__(node
, self
._context
)
1840 svgGeometryClasses
= {
1841 'svg': SVGGeometrySVG
,
1842 'path': SVGGeometryPATH
,
1843 'defs': SVGGeometryDEFS
,
1844 'symbol': SVGGeometrySYMBOL
,
1845 'use': SVGGeometryUSE
,
1846 'rect': SVGGeometryRECT
,
1847 'ellipse': SVGGeometryELLIPSE
,
1848 'circle': SVGGeometryCIRCLE
,
1849 'line': SVGGeometryLINE
,
1850 'polyline': SVGGeometryPOLYLINE
,
1851 'polygon': SVGGeometryPOLYGON
,
1855 def parseAbstractNode(node
, context
):
1856 name
= node
.tagName
.lower()
1858 if name
.startswith('svg:'):
1861 geomClass
= svgGeometryClasses
.get(name
)
1863 if geomClass
is not None:
1864 ob
= geomClass(node
, context
)
1872 def load_svg(context
, filepath
, do_colormanage
):
1874 Load specified SVG file
1877 if bpy
.ops
.object.mode_set
.poll():
1878 bpy
.ops
.object.mode_set(mode
='OBJECT')
1880 loader
= SVGLoader(context
, filepath
, do_colormanage
)
1882 loader
.createGeom(False)
1885 def load(operator
, context
, filepath
=""):
1887 # error in code should raise exceptions but loading
1888 # non SVG files can give useful messages.
1889 do_colormanage
= context
.scene
.display_settings
.display_device
!= 'NONE'
1891 load_svg(context
, filepath
, do_colormanage
)
1892 except (xml
.parsers
.expat
.ExpatError
, UnicodeEncodeError) as e
:
1894 traceback
.print_exc()
1896 operator
.report({'WARNING'}, tip_("Unable to parse XML, %s:%s for file %r") % (type(e
).__name
__, e
, filepath
))
1897 return {'CANCELLED'}