-
Sergey Sharybin authored
Additional changes: - Fixed wrong pop of rectangle area (function wasn't called) - Removed unused code
Sergey Sharybin authoredAdditional changes: - Fixed wrong pop of rectangle area (function wasn't called) - Removed unused code
import_svg.py 47.22 KiB
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
import re
import xml.dom.minidom
from math import cos, sin, tan, atan2, pi, ceil
import bpy
from mathutils import Vector, Matrix
from . import svg_colors
#### Common utilities ####
# TODO: "em" and "ex" aren't actually supported
SVGUnits = {"": 1.0,
"px": 1.0,
"in": 90,
"mm": 90 / 25.4,
"cm": 90 / 2.54,
"pt": 1.25,
"pc": 15.0,
"em": 1.0,
"ex": 1.0,
"INVALID": 1.0, # some DocBook files contain this
}
SVGEmptyStyles = {'useFill': None,
'fill': None}
def SVGParseFloat(s, i=0):
"""
Parse first float value from string
Returns value as string
"""
start = i
n = len(s)
token = ''
# Skip leading whitespace characters
while i < n and (s[i].isspace() or s[i] == ','):
i += 1
if i == n:
return None, i
# Read sign
if s[i] == '-':
token += '-'
i += 1
elif s[i] == '+':
i += 1
# Read integer part
if s[i].isdigit():
while i < n and s[i].isdigit():
token += s[i]
i += 1
# Fractional part
if i < n and s[i] == '.':
token += '.'
i += 1
if s[i].isdigit():
while i < n and s[i].isdigit():
token += s[i]
i += 1
elif s[i].isspace() or s[i] == ',':
# Inkscape sometimes uses qeird float format with missed
# fractional part after dot. Suppose zero fractional part
# for this case
pass
else:
raise Exception('Invalid float value near ' + s[start:start + 10])
# Degree
if i < n and (s[i] == 'e' or s[i] == 'E'):
token += s[i]
i += 1
if s[i] == '+' or s[i] == '-':
token += s[i]
i += 1
if s[i].isdigit():
while i < n and s[i].isdigit():
token += s[i]
i += 1
else:
raise Exception('Invalid float value near ' +
s[start:start + 10])
else:
raise Exception('Invalid float value near ' + s[start:start + 10])
return token, i
def SVGCreateCurve():
"""
Create new curve object to hold splines in
"""
cu = bpy.data.curves.new("Curve", 'CURVE')
obj = bpy.data.objects.new("Curve", cu)
bpy.context.scene.objects.link(obj)
return obj
def SVGFinishCurve():
"""
Finish curve creation
"""
pass
def SVGFlipHandle(x, y, x1, y1):
"""
Flip handle around base point
"""
x = x + (x - x1)
y = y + (y - y1)
return x, y
def SVGParseCoord(coord, size):
"""
Parse coordinate component to common basis
Needed to handle coordinates set in cm, mm, iches..
"""
token, last_char = SVGParseFloat(coord)
val = float(token)
unit = coord[last_char:].strip() # strip() in case there is a space
if unit == '%':
return float(size) / 100.0 * val
else:
return val * SVGUnits[unit]
return val
def SVGRectFromNode(node, context):
"""
Get display rectangle from node
"""
w = context['rect'][0]
h = context['rect'][1]
if node.getAttribute('viewBox'):
viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
w = SVGParseCoord(viewBox[2], w)
h = SVGParseCoord(viewBox[3], h)
else:
if node.getAttribute('width'):
w = SVGParseCoord(node.getAttribute('width'), w)
if node.getAttribute('height'):
h = SVGParseCoord(node.getAttribute('height'), h)
return (w, h)
def SVGMatrixFromNode(node, context):
"""
Get transformation matrix from given node
"""
tagName = node.tagName.lower()
tags = ['svg:svg', 'svg:use', 'svg:symbol']
if tagName not in tags and 'svg:' + tagName not in tags:
return Matrix()
rect = context['rect']
m = Matrix()
x = SVGParseCoord(node.getAttribute('x') or '0', rect[0])
y = SVGParseCoord(node.getAttribute('y') or '0', rect[1])
w = SVGParseCoord(node.getAttribute('width') or str(rect[0]), rect[0])
h = SVGParseCoord(node.getAttribute('height') or str(rect[1]), rect[1])
m = Matrix.Translation(Vector((x, y, 0.0)))
if len(context['rects']) > 1:
m = m * Matrix.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0)))
m = m * Matrix.Scale(h / rect[1], 4, Vector((0.0, 1.0, 0.0)))
if node.getAttribute('viewBox'):
viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
vx = SVGParseCoord(viewBox[0], w)
vy = SVGParseCoord(viewBox[1], h)
vw = SVGParseCoord(viewBox[2], w)
vh = SVGParseCoord(viewBox[3], h)
sx = w / vw
sy = h / vh
scale = min(sx, sy)
tx = (w - vw * scale) / 2
ty = (h - vh * scale) / 2
m = m * Matrix.Translation(Vector((tx, ty, 0.0)))
m = m * Matrix.Translation(Vector((-vx, -vy, 0.0)))
m = m * Matrix.Scale(scale, 4, Vector((1.0, 0.0, 0.0)))
m = m * Matrix.Scale(scale, 4, Vector((0.0, 1.0, 0.0)))
return m
def SVGParseTransform(transform):
"""
Parse transform string and return transformation matrix
"""
m = Matrix()
r = re.compile('\s*([A-z]+)\s*\((.*?)\)')
for match in r.finditer(transform):
func = match.group(1)
params = match.group(2)
params = params.replace(',', ' ').split()
proc = SVGTransforms.get(func)
if proc is None:
raise Exception('Unknown trasnform function: ' + func)
m = m * proc(params)
return m
def SVGGetMaterial(color, context):
"""
Get material for specified color
"""
materials = context['materials']
rgb_re = re.compile('^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
if color in materials:
return materials[color]
diff = None
if color.startswith('#'):
color = color[1:]
if len(color) == 3:
color = color[0] * 2 + color[1] * 2 + color[2] * 2
diff = (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
elif color in svg_colors.SVGColors:
diff = svg_colors.SVGColors[color]
elif rgb_re.match(color):
c = rgb_re.findall(color)[0]
diff = (float(c[0]), float(c[1]), float(c[2]))
else:
return None
mat = bpy.data.materials.new(name='SVGMat')
mat.diffuse_color = ([x / 255.0 for x in diff])
materials[color] = mat
return mat
def SVGTransformTranslate(params):
"""
translate SVG transform command
"""
tx = float(params[0])
ty = float(params[1]) if len(params) > 1 else 0.0
return Matrix.Translation(Vector((tx, ty, 0.0)))
def SVGTransformMatrix(params):
"""
matrix SVG transform command
"""
a = float(params[0])
b = float(params[1])
c = float(params[2])
d = float(params[3])
e = float(params[4])
f = float(params[5])
return Matrix(((a, c, 0.0, e),
(b, d, 0.0, f),
(0, 0, 1.0, 0),
(0, 0, 0.0, 1)))
def SVGTransformScale(params):
"""
scale SVG transform command
"""
sx = float(params[0])
sy = float(params[1]) if len(params) > 1 else sx
m = Matrix()
m = m * Matrix.Scale(sx, 4, Vector((1.0, 0.0, 0.0)))
m = m * Matrix.Scale(sy, 4, Vector((0.0, 1.0, 0.0)))
return m
def SVGTransformSkewX(params):
"""
skewX SVG transform command
"""
ang = float(params[0]) * pi / 180.0
return Matrix(((1.0, 0.0, 0.0),
(tan(ang), 1.0, 0.0),
(0.0, 0.0, 1.0))).to_4x4()
def SVGTransformSkewY(params):
"""
skewX SVG transform command
"""
ang = float(params[0]) * pi / 180.0
return Matrix(((1.0, tan(ang), 0.0),
(0.0, 1.0, 0.0),
(0.0, 0.0, 1.0))).to_4x4()
def SVGTransformRotate(params):
"""
skewX SVG transform command
"""
ang = float(params[0]) * pi / 180.0
cx = cy = 0.0
if len(params) >= 3:
cx = float(params[1])
cy = float(params[2])
tm = Matrix.Translation(Vector((cx, cy, 0.0)))
rm = Matrix.Rotation(ang, 4, Vector((0.0, 0.0, 1.0)))
return tm * rm * tm.inverted()
SVGTransforms = {'translate': SVGTransformTranslate,
'scale': SVGTransformScale,
'skewX': SVGTransformSkewX,
'skewY': SVGTransformSkewY,
'matrix': SVGTransformMatrix,
'rotate': SVGTransformRotate}
def SVGParseStyles(node, context):
"""
Parse node to get different styles for displaying geometries
(materilas, filling flags, etc..)
"""
styles = SVGEmptyStyles.copy()
style = node.getAttribute('style')
if style:
elems = style.split(';')
for elem in elems:
s = elem.split(':')
if len(s) != 2:
continue
name = s[0].strip().lower()
val = s[1].strip()
if name == 'fill':
val = val.lower()
if val == 'none':
styles['useFill'] = False
else:
styles['useFill'] = True
styles['fill'] = SVGGetMaterial(val, context)
if styles['useFill'] is None:
styles['useFill'] = True
styles['fill'] = SVGGetMaterial('#000', context)
return styles
if styles['useFill'] is None:
fill = node.getAttribute('fill')
if fill:
fill = fill.lower()
if fill == 'none':
styles['useFill'] = False
else:
styles['useFill'] = True
styles['fill'] = SVGGetMaterial(fill, context)
if styles['useFill'] is None and context['style']:
styles = context['style'].copy()
if styles['useFill'] is None:
styles['useFill'] = True
styles['fill'] = SVGGetMaterial('#000', context)
return styles
#### SVG path helpers ####
class SVGPathData:
"""
SVG Path data token supplier
"""
__slots__ = ('_data', # List of tokens
'_index', # Index of current token in tokens list
'_len') # Lenght og tokens list
def __init__(self, d):
"""
Initialize new path data supplier
d - the definition of the outline of a shape
"""
spaces = ' ,\t'
commands = {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
tokens = []
i = 0
n = len(d)
while i < n:
c = d[i]
if c in spaces:
pass
elif c.lower() in commands:
tokens.append(c)
elif c in ['-', '.'] or c.isdigit():
token, last_char = SVGParseFloat(d, i)
tokens.append(token)
# in most cases len(token) and (last_char - i) are the same
# but with whitespace or ',' prefix they are not.
i += (last_char - i) - 1
i += 1
self._data = tokens
self._index = 0
self._len = len(tokens)
def eof(self):
"""
Check if end of data reached
"""
return self._index >= self._len
def cur(self):
"""
Return current token
"""
if self.eof():
return None
return self._data[self._index]
def lookupNext(self):
"""
get next token without moving pointer
"""
if self.eof():
return None
return self._data[self._index]
def next(self):
"""
Return current token and go to next one
"""
if self.eof():
return None
token = self._data[self._index]
self._index += 1
return token
def nextCoord(self):
"""
Return coordinate created from current token and move to next token
"""
token = self.next()
if token is None:
return None
return float(token)
class SVGPathParser:
"""
Parser of SVG path data
"""
__slots__ = ('_data', # Path data supplird
'_point', # Current point coorfinate
'_handle', # Last handle coordinate
'_splines', # List of all splies created during parsing
'_spline', # Currently handling spline
'_commands') # Hash of all supported path commands
def __init__(self, d):
"""
Initialize path parser
d - the definition of the outline of a shape
"""
self._data = SVGPathData(d)
self._point = None # Current point
self._handle = None # Last handle
self._splines = [] # List of splines in path
self._spline = None # Current spline
self._commands = {'M': self._pathMoveTo,
'L': self._pathLineTo,
'H': self._pathLineTo,
'V': self._pathLineTo,
'C': self._pathCurveToCS,
'S': self._pathCurveToCS,
'Q': self._pathCurveToQT,
'T': self._pathCurveToQT,
'A': self._pathCurveToA,
'Z': self._pathClose,
'm': self._pathMoveTo,
'l': self._pathLineTo,
'h': self._pathLineTo,
'v': self._pathLineTo,
'c': self._pathCurveToCS,
's': self._pathCurveToCS,
'q': self._pathCurveToQT,
't': self._pathCurveToQT,
'a': self._pathCurveToA,
'z': self._pathClose}
def _getCoordPair(self, relative, point):
"""
Get next coordinate pair
"""
x = self._data.nextCoord()
y = self._data.nextCoord()
if relative and point is not None:
x += point[0]
y += point[1]
return x, y
def _appendPoint(self, x, y, handle_left=None, handle_left_type='VECTOR',
handle_right=None, handle_right_type='VECTOR'):
"""
Append point to spline
If there's no active spline, create one and set it's first point
to current point coordinate
"""
if self._spline is None:
self._spline = {'points': [],
'closed': False}
self._splines.append(self._spline)
if len(self._spline['points']) > 0:
# Not sure bout specifications, but Illustrator could create
# last point at the same position, as start point (which was
# reached by MoveTo command) to set needed handle coords.
# It's also could use last point at last position to make path
# filled.
first = self._spline['points'][0]
if abs(first['x'] - x) < 1e-6 and abs(first['y'] - y) < 1e-6:
if handle_left is not None:
first['handle_left'] = handle_left
first['handle_left_type'] = 'FREE'
if handle_left_type != 'VECTOR':
first['handle_left_type'] = handle_left_type
if self._data.eof() or self._data.lookupNext().lower() == 'm':
self._spline['closed'] = True
return
point = {'x': x,
'y': y,
'handle_left': handle_left,
'handle_left_type': handle_left_type,
'handle_right': handle_right,
'handle_right_type': handle_right_type}
self._spline['points'].append(point)
def _updateHandle(self, handle=None, handle_type=None):
"""
Update right handle of previous point when adding new point to spline
"""
point = self._spline['points'][-1]
if handle_type is not None:
point['handle_right_type'] = handle_type
if handle is not None:
point['handle_right'] = handle
def _pathMoveTo(self, code):
"""
MoveTo path command
"""
relative = code.islower()
x, y = self._getCoordPair(relative, self._point)
self._spline = None # Flag to start new spline
self._point = (x, y)
cur = self._data.cur()
while cur is not None and not cur.isalpha():
x, y = self._getCoordPair(relative, self._point)
if self._spline is None:
self._appendPoint(self._point[0], self._point[1])
self._appendPoint(x, y)
self._point = (x, y)
cur = self._data.cur()
self._handle = None
def _pathLineTo(self, code):
"""
LineTo path command
"""
c = code.lower()
cur = self._data.cur()
while cur is not None and not cur.isalpha():
if c == 'l':
x, y = self._getCoordPair(code == 'l', self._point)
elif c == 'h':
x = self._data.nextCoord()
y = self._point[1]
else:
x = self._point[0]
y = self._data.nextCoord()
if code == 'h':
x += self._point[0]
elif code == 'v':
y += self._point[1]
if self._spline is None:
self._appendPoint(self._point[0], self._point[1])
self._appendPoint(x, y)
self._point = (x, y)
cur = self._data.cur()
self._handle = None
def _pathCurveToCS(self, code):
"""
Cubic BEZIER CurveTo path command
"""
c = code.lower()
cur = self._data.cur()
while cur is not None and not cur.isalpha():
if c == 'c':
x1, y1 = self._getCoordPair(code.islower(), self._point)
x2, y2 = self._getCoordPair(code.islower(), self._point)
else:
if self._handle is not None:
x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
self._handle[0], self._handle[1])
else:
x1, y1 = self._point
x2, y2 = self._getCoordPair(code.islower(), self._point)
x, y = self._getCoordPair(code.islower(), self._point)
if self._spline is None:
self._appendPoint(self._point[0], self._point[1],
handle_left_type='FREE', handle_left=self._point,
handle_right_type='FREE', handle_right=(x1, y1))
else:
self._updateHandle(handle=(x1, y1), handle_type='FREE')
self._appendPoint(x, y,
handle_left_type='FREE', handle_left=(x2, y2),
handle_right_type='FREE', handle_right=(x, y))
self._point = (x, y)
self._handle = (x2, y2)
cur = self._data.cur()
def _pathCurveToQT(self, code):
"""
Qyadracic BEZIER CurveTo path command
"""
c = code.lower()
cur = self._data.cur()
while cur is not None and not cur.isalpha():
if c == 'q':
x1, y1 = self._getCoordPair(code.islower(), self._point)
else:
if self._handle is not None:
x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
self._handle[0], self._handle[1])
else:
x1, y1 = self._point
x, y = self._getCoordPair(code.islower(), self._point)
if self._spline is None:
self._appendPoint(self._point[0], self._point[1],
handle_left_type='FREE', handle_left=self._point,
handle_right_type='FREE', handle_right=self._point)
self._appendPoint(x, y,
handle_left_type='FREE', handle_left=(x1, y1),
handle_right_type='FREE', handle_right=(x, y))
self._point = (x, y)
self._handle = (x1, y1)
cur = self._data.cur()
def _calcArc(self, rx, ry, ang, fa, fs, x, y):
"""
Calc arc paths
Copied and adoptedfrom paths_svg2obj.py scring for Blender 2.49
which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
"""
cpx = self._point[0]
cpy = self._point[1]
rx = abs(rx)
ry = abs(ry)
px = abs((cos(ang) * (cpx - x) + sin(ang) * (cpy - y)) * 0.5) ** 2.0
py = abs((cos(ang) * (cpy - y) - sin(ang) * (cpx - x)) * 0.5) ** 2.0
rpx = rpy = 0.0
if abs(rx) > 0.0:
px = px / (rx ** 2.0)
if abs(ry) > 0.0:
rpy = py / (ry ** 2.0)
pl = rpx + rpy
if pl > 1.0:
pl = pl ** 0.5
rx *= pl
ry *= pl
carx = sarx = cary = sary = 0.0
if abs(rx) > 0.0:
carx = cos(ang) / rx
sarx = sin(ang) / rx
if abs(ry) > 0.0:
cary = cos(ang) / ry
sary = sin(ang) / ry
x0 = carx * cpx + sarx * cpy
y0 = -sary * cpx + cary * cpy
x1 = carx * x + sarx * y
y1 = -sary * x + cary * y
d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
if abs(d) > 0.0:
sq = 1.0 / d - 0.25
else:
sq = -0.25
if sq < 0.0:
sq = 0.0
sf = sq ** 0.5
if fs == fa:
sf = -sf
xc = 0.5 * (x0 + x1) - sf * (y1 - y0)
yc = 0.5 * (y0 + y1) + sf * (x1 - x0)
ang_0 = atan2(y0 - yc, x0 - xc)
ang_1 = atan2(y1 - yc, x1 - xc)
ang_arc = ang_1 - ang_0
if ang_arc < 0.0 and fs == 1:
ang_arc += 2.0 * pi
elif ang_arc > 0.0 and fs == 0:
ang_arc -= 2.0 * pi
n_segs = int(ceil(abs(ang_arc * 2.0 / (pi * 0.5 + 0.001))))
if self._spline is None:
self._appendPoint(cpx, cpy,
handle_left_type='FREE', handle_left=(cpx, cpy),
handle_right_type='FREE', handle_right=(cpx, cpy))
for i in range(n_segs):
ang0 = ang_0 + i * ang_arc / n_segs
ang1 = ang_0 + (i + 1) * ang_arc / n_segs
ang_demi = 0.25 * (ang1 - ang0)
t = 2.66666 * sin(ang_demi) * sin(ang_demi) / sin(ang_demi * 2.0)
x1 = xc + cos(ang0) - t * sin(ang0)
y1 = yc + sin(ang0) + t * cos(ang0)
x2 = xc + cos(ang1)
y2 = yc + sin(ang1)
x3 = x2 + t * sin(ang1)
y3 = y2 - t * cos(ang1)
coord1 = ((cos(ang) * rx) * x1 + (-sin(ang) * ry) * y1,
(sin(ang) * rx) * x1 + (cos(ang) * ry) * y1)
coord2 = ((cos(ang) * rx) * x3 + (-sin(ang) * ry) * y3,
(sin(ang) * rx) * x3 + (cos(ang) * ry) * y3)
coord3 = ((cos(ang) * rx) * x2 + (-sin(ang) * ry) * y2,
(sin(ang) * rx) * x2 + (cos(ang) * ry) * y2)
self._updateHandle(handle=coord1, handle_type='FREE')
self._appendPoint(coord3[0], coord3[1],
handle_left_type='FREE', handle_left=coord2,
handle_right_type='FREE', handle_right=coord3)
def _pathCurveToA(self, code):
"""
Elliptical arc CurveTo path command
"""
cur = self._data.cur()
while cur is not None and not cur.isalpha():
rx = float(self._data.next())
ry = float(self._data.next())
ang = float(self._data.next()) / 180 * pi
fa = float(self._data.next())
fs = float(self._data.next())
x, y = self._getCoordPair(code.islower(), self._point)
self._calcArc(rx, ry, ang, fa, fs, x, y)
self._point = (x, y)
self._handle = None
cur = self._data.cur()
def _pathClose(self, code):
"""
Close path command
"""
if self._spline:
self._spline['closed'] = True
cv = self._spline['points'][0]
self._point = (cv['x'], cv['y'])
def parse(self):
"""
Execute parser
"""
while not self._data.eof():
code = self._data.next()
cmd = self._commands.get(code)
if cmd is None:
raise Exception('Unknown path command: {0}' . format(code))
cmd(code)
def getSplines(self):
"""
Get splines definitions
"""
return self._splines
class SVGGeometry:
"""
Abstract SVG geometry
"""
__slots__ = ('_node', # XML node for geometry
'_context', # Global SVG context (holds matrices stack, i.e.)
'_creating') # Flag if geometry is already creating
# for this node
# need to detect cycles for USE node
def __init__(self, node, context):
"""
Initialize SVG geometry
"""
self._node = node
self._context = context
self._creating = False
if hasattr(node, 'getAttribute'):
defs = context['defines']
attr_id = node.getAttribute('id')
if attr_id and defs.get('#' + attr_id) is None:
defs['#' + attr_id] = self
className = node.getAttribute('class')
if className and defs.get(className) is None:
defs[className] = self
def _pushRect(self, rect):
"""
Push display rectangle
"""
self._context['rects'].append(rect)
self._context['rect'] = rect
def _popRect(self):
"""
Pop display rectangle
"""
self._context['rects'].pop()
self._context['rect'] = self._context['rects'][-1]
def _pushMatrix(self, matrix):
"""
Push transformation matrix
"""
self._context['transform'].append(matrix)
self._context['matrix'] = self._context['matrix'] * matrix
def _popMatrix(self):
"""
Pop transformation matrix
"""
matrix = self._context['transform'].pop()
self._context['matrix'] = self._context['matrix'] * matrix.inverted()
def _pushStyle(self, style):
"""
Push style
"""
self._context['styles'].append(style)
self._context['style'] = style
def _popStyle(self):
"""
Pop style
"""
self._context['styles'].pop()
self._context['style'] = self._context['styles'][-1]
def _transformCoord(self, point):
"""
Transform SVG-file coords
"""
v = Vector((point[0], point[1], 0.0))
return self._context['matrix'] * v
def getNodeMatrix(self):
"""
Get transformation matrix of node
"""
return SVGMatrixFromNode(self._node, self._context)
def parse(self):
"""
Parse XML node to memory
"""
pass
def _doCreateGeom(self, instancing):
"""
Internal handler to create real geometries
"""
pass
def getTransformMatrix(self):
"""
Get matrix created from "transform" attribute
"""
transform = self._node.getAttribute('transform')
if transform:
return SVGParseTransform(transform)
return None
def createGeom(self, instancing):
"""
Create real geometries
"""
if self._creating:
return
self._creating = True
matrix = self.getTransformMatrix()
if matrix is not None:
self._pushMatrix(matrix)
self._doCreateGeom(instancing)
if matrix is not None:
self._popMatrix()
self._creating = False
class SVGGeometryContainer(SVGGeometry):
"""
Container of SVG geometries
"""
__slots__ = ('_geometries', # List of chold geometries
'_styles') # Styles, used for displaying
def __init__(self, node, context):
"""
Initialize SVG geometry container
"""
super().__init__(node, context)
self._geometries = []
self._styles = SVGEmptyStyles
def parse(self):
"""
Parse XML node to memory
"""
if type(self._node) is xml.dom.minidom.Element:
self._styles = SVGParseStyles(self._node, self._context)
self._pushStyle(self._styles)
for node in self._node.childNodes:
if type(node) is not xml.dom.minidom.Element:
continue
ob = parseAbstractNode(node, self._context)
if ob is not None:
self._geometries.append(ob)
self._popStyle()
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
for geom in self._geometries:
geom.createGeom(instancing)
def getGeometries(self):
"""
Get list of parsed geometries
"""
return self._geometries
class SVGGeometryPATH(SVGGeometry):
"""
SVG path geometry
"""
__slots__ = ('_splines', # List of splines after parsing
'_styles') # Styles, used for displaying
def __init__(self, node, context):
"""
Initialize SVG path
"""
super().__init__(node, context)
self._splines = []
self._styles = SVGEmptyStyles
def parse(self):
"""
Parse SVG path node
"""
d = self._node.getAttribute('d')
pathParser = SVGPathParser(d)
pathParser.parse()
self._splines = pathParser.getSplines()
self._styles = SVGParseStyles(self._node, self._context)
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
ob = SVGCreateCurve()
cu = ob.data
if self._node.getAttribute('id'):
cu.name = self._node.getAttribute('id')
if self._styles['useFill']:
cu.dimensions = '2D'
cu.materials.append(self._styles['fill'])
else:
cu.dimensions = '3D'
for spline in self._splines:
act_spline = None
for point in spline['points']:
co = self._transformCoord((point['x'], point['y']))
if act_spline is None:
cu.splines.new('BEZIER')
act_spline = cu.splines[-1]
act_spline.use_cyclic_u = spline['closed']
else:
act_spline.bezier_points.add()
bezt = act_spline.bezier_points[-1]
bezt.co = co
bezt.handle_left_type = point['handle_left_type']
if point['handle_left'] is not None:
handle = point['handle_left']
bezt.handle_left = self._transformCoord(handle)
bezt.handle_right_type = point['handle_right_type']
if point['handle_right'] is not None:
handle = point['handle_right']
bezt.handle_right = self._transformCoord(handle)
SVGFinishCurve()
class SVGGeometryDEFS(SVGGeometryContainer):
"""
Container for referenced elements
"""
def createGeom(self, instancing):
"""
Create real geometries
"""
pass
class SVGGeometrySYMBOL(SVGGeometryContainer):
"""
Referenced element
"""
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
self._pushMatrix(self.getNodeMatrix())
super()._doCreateGeom(False)
self._popMatrix()
def createGeom(self, instancing):
"""
Create real geometries
"""
if not instancing:
return
super().createGeom(instancing)
class SVGGeometryG(SVGGeometryContainer):
"""
Geometry group
"""
pass
class SVGGeometryUSE(SVGGeometry):
"""
User of referenced elements
"""
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
ref = self._node.getAttribute('xlink:href')
geom = self._context['defines'].get(ref)
if geom is not None:
rect = SVGRectFromNode(self._node, self._context)
self._pushRect(rect)
self._pushMatrix(self.getNodeMatrix())
geom.createGeom(True)
self._popMatrix()
self._popRect()
class SVGGeometryRECT(SVGGeometry):
"""
SVG rectangle
"""
__slots__ = ('_rect', # coordinate and domensions of rectangle
'_radius', # Rounded corner radiuses
'_styles') # Styles, used for displaying
def __init__(self, node, context):
"""
Initialize new rectangle
"""
super().__init__(node, context)
self._rect = ('0', '0', '0', '0')
self._radius = ('0', '0')
self._styles = SVGEmptyStyles
def parse(self):
"""
Parse SVG rectangle node
"""
self._styles = SVGParseStyles(self._node, self._context)
rect = []
for attr in ['x', 'y', 'width', 'height']:
val = self._node.getAttribute(attr)
rect.append(val or '0')
self._rect = (rect)
rx = self._node.getAttribute('rx')
ry = self._node.getAttribute('ry')
self._radius = (rx, ry)
def _appendCorner(self, spline, coord, firstTime, rounded):
"""
Append new corner to rectangle
"""
handle = None
if len(coord) == 3:
handle = self._transformCoord(coord[2])
coord = (coord[0], coord[1])
co = self._transformCoord(coord)
if not firstTime:
spline.bezier_points.add()
bezt = spline.bezier_points[-1]
bezt.co = co
if rounded:
if handle:
bezt.handle_left_type = 'VECTOR'
bezt.handle_right_type = 'FREE'
bezt.handle_right = handle
else:
bezt.handle_left_type = 'FREE'
bezt.handle_right_type = 'VECTOR'
bezt.handle_left = co
else:
bezt.handle_left_type = 'VECTOR'
bezt.handle_right_type = 'VECTOR'
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
# Run-time parsing -- percents would be correct only if
# parsing them now
crect = self._context['rect']
rect = []
for i in range(4):
rect.append(SVGParseCoord(self._rect[i], crect[i % 2]))
r = self._radius
rx = ry = 0.0
if r[0] and r[1]:
rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
elif r[0]:
rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
ry = min(rx, rect[3] / 2)
rx = ry = min(rx, ry)
elif r[1]:
ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
rx = min(ry, rect[2] / 2)
rx = ry = min(rx, ry)
radius = (rx, ry)
# Geometry creation
ob = SVGCreateCurve()
cu = ob.data
if self._styles['useFill']:
cu.dimensions = '2D'
cu.materials.append(self._styles['fill'])
else:
cu.dimensions = '3D'
cu.splines.new('BEZIER')
spline = cu.splines[-1]
spline.use_cyclic_u = True
x, y = rect[0], rect[1]
w, h = rect[2], rect[3]
rx, ry = radius[0], radius[1]
rounded = False
if rx or ry:
#
# 0 _______ 1
# / \
# / \
# 7 2
# | |
# | |
# 6 3
# \ /
# \ /
# 5 _______ 4
#
# Optional third component -- right handle coord
coords = [(x + rx, y),
(x + w - rx, y, (x + w, y)),
(x + w, y + ry),
(x + w, y + h - ry, (x + w, y + h)),
(x + w - rx, y + h),
(x + rx, y + h, (x, y + h)),
(x, y + h - ry),
(x, y + ry, (x, y))]
rounded = True
else:
coords = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
firstTime = True
for coord in coords:
self._appendCorner(spline, coord, firstTime, rounded)
firstTime = False
SVGFinishCurve()
class SVGGeometryELLIPSE(SVGGeometry):
"""
SVG ellipse
"""
__slots__ = ('_cx', # X-coordinate of center
'_cy', # Y-coordinate of center
'_rx', # X-axis radius of circle
'_ry', # Y-axis radius of circle
'_styles') # Styles, used for displaying
def __init__(self, node, context):
"""
Initialize new ellipse
"""
super().__init__(node, context)
self._cx = '0.0'
self._cy = '0.0'
self._rx = '0.0'
self._ry = '0.0'
self._styles = SVGEmptyStyles
def parse(self):
"""
Parse SVG ellipse node
"""
self._styles = SVGParseStyles(self._node, self._context)
self._cx = self._node.getAttribute('cx') or '0'
self._cy = self._node.getAttribute('cy') or '0'
self._rx = self._node.getAttribute('rx') or '0'
self._ry = self._node.getAttribute('ry') or '0'
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
# Run-time parsing -- percents would be correct only if
# parsing them now
crect = self._context['rect']
cx = SVGParseCoord(self._cx, crect[0])
cy = SVGParseCoord(self._cy, crect[1])
rx = SVGParseCoord(self._rx, crect[0])
ry = SVGParseCoord(self._ry, crect[1])
if not rx or not ry:
# Automaic handles will work incorrect in this case
return
# Create circle
ob = SVGCreateCurve()
cu = ob.data
if self._styles['useFill']:
cu.dimensions = '2D'
cu.materials.append(self._styles['fill'])
else:
cu.dimensions = '3D'
coords = [((cx - rx, cy),
(cx - rx, cy + ry * 0.552),
(cx - rx, cy - ry * 0.552)),
((cx, cy - ry),
(cx - rx * 0.552, cy - ry),
(cx + rx * 0.552, cy - ry)),
((cx + rx, cy),
(cx + rx, cy - ry * 0.552),
(cx + rx, cy + ry * 0.552)),
((cx, cy + ry),
(cx + rx * 0.552, cy + ry),
(cx - rx * 0.552, cy + ry))]
spline = None
for coord in coords:
co = self._transformCoord(coord[0])
handle_left = self._transformCoord(coord[1])
handle_right = self._transformCoord(coord[2])
if spline is None:
cu.splines.new('BEZIER')
spline = cu.splines[-1]
spline.use_cyclic_u = True
else:
spline.bezier_points.add()
bezt = spline.bezier_points[-1]
bezt.co = co
bezt.handle_left_type = 'FREE'
bezt.handle_right_type = 'FREE'
bezt.handle_left = handle_left
bezt.handle_right = handle_right
SVGFinishCurve()
class SVGGeometryCIRCLE(SVGGeometryELLIPSE):
"""
SVG circle
"""
def parse(self):
"""
Parse SVG circle node
"""
self._styles = SVGParseStyles(self._node, self._context)
self._cx = self._node.getAttribute('cx') or '0'
self._cy = self._node.getAttribute('cy') or '0'
r = self._node.getAttribute('r') or '0'
self._rx = self._ry = r
class SVGGeometryLINE(SVGGeometry):
"""
SVG line
"""
__slots__ = ('_x1', # X-coordinate of beginning
'_y1', # Y-coordinate of beginning
'_x2', # X-coordinate of ending
'_y2') # Y-coordinate of ending
def __init__(self, node, context):
"""
Initialize new line
"""
super().__init__(node, context)
self._x1 = '0.0'
self._y1 = '0.0'
self._x2 = '0.0'
self._y2 = '0.0'
def parse(self):
"""
Parse SVG line node
"""
self._x1 = self._node.getAttribute('x1') or '0'
self._y1 = self._node.getAttribute('y1') or '0'
self._x2 = self._node.getAttribute('x2') or '0'
self._y2 = self._node.getAttribute('y2') or '0'
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
# Run-time parsing -- percents would be correct only if
# parsing them now
crect = self._context['rect']
x1 = SVGParseCoord(self._x1, crect[0])
y1 = SVGParseCoord(self._y1, crect[1])
x2 = SVGParseCoord(self._x2, crect[0])
y2 = SVGParseCoord(self._y2, crect[1])
# Create cline
ob = SVGCreateCurve()
cu = ob.data
coords = [(x1, y1), (x2, y2)]
spline = None
for coord in coords:
co = self._transformCoord(coord)
if spline is None:
cu.splines.new('BEZIER')
spline = cu.splines[-1]
spline.use_cyclic_u = True
else:
spline.bezier_points.add()
bezt = spline.bezier_points[-1]
bezt.co = co
bezt.handle_left_type = 'VECTOR'
bezt.handle_right_type = 'VECTOR'
SVGFinishCurve()
class SVGGeometryPOLY(SVGGeometry):
"""
Abstract class for handling poly-geometries
(polylines and polygons)
"""
__slots__ = ('_points', # Array of points for poly geometry
'_styles', # Styles, used for displaying
'_closed') # Should generated curve be closed?
def __init__(self, node, context):
"""
Initialize new poly geometry
"""
super().__init__(node, context)
self._points = []
self._styles = SVGEmptyStyles
self._closed = False
def parse(self):
"""
Parse poly node
"""
self._styles = SVGParseStyles(self._node, self._context)
points = self._node.getAttribute('points')
points = points.replace(',', ' ').replace('-', ' -')
points = points.split()
prev = None
self._points = []
for p in points:
if prev is None:
prev = p
else:
self._points.append((float(prev), float(p)))
prev = None
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
ob = SVGCreateCurve()
cu = ob.data
if self._closed and self._styles['useFill']:
cu.dimensions = '2D'
cu.materials.append(self._styles['fill'])
else:
cu.dimensions = '3D'
spline = None
for point in self._points:
co = self._transformCoord(point)
if spline is None:
cu.splines.new('BEZIER')
spline = cu.splines[-1]
spline.use_cyclic_u = self._closed
else:
spline.bezier_points.add()
bezt = spline.bezier_points[-1]
bezt.co = co
bezt.handle_left_type = 'VECTOR'
bezt.handle_right_type = 'VECTOR'
SVGFinishCurve()
class SVGGeometryPOLYLINE(SVGGeometryPOLY):
"""
SVG polyline geometry
"""
pass
class SVGGeometryPOLYGON(SVGGeometryPOLY):
"""
SVG polygon geometry
"""
def __init__(self, node, context):
"""
Initialize new polygon geometry
"""
super().__init__(node, context)
self._closed = True
class SVGGeometrySVG(SVGGeometryContainer):
"""
Main geometry holder
"""
def _doCreateGeom(self, instancing):
"""
Create real geometries
"""
rect = SVGRectFromNode(self._node, self._context)
self._pushMatrix(self.getNodeMatrix())
self._pushRect(rect)
super()._doCreateGeom(False)
self._popRect()
self._popMatrix()
class SVGLoader(SVGGeometryContainer):
"""
SVG file loader
"""
def getTransformMatrix(self):
"""
Get matrix created from "transform" attribute
"""
# SVG document doesn't support transform specification
# it can't even hold attributes
return None
def __init__(self, filepath):
"""
Initialize SVG loader
"""
node = xml.dom.minidom.parse(filepath)
m = Matrix()
m = m * Matrix.Scale(1.0 / 90.0, 4, Vector((1.0, 0.0, 0.0)))
m = m * Matrix.Scale(-1.0 / 90.0, 4, Vector((0.0, 1.0, 0.0)))
rect = (1, 1)
self._context = {'defines': {},
'transform': [],
'rects': [rect],
'rect': rect,
'matrix': m,
'materials': {},
'styles': [None],
'style': None}
super().__init__(node, self._context)
svgGeometryClasses = {
'svg': SVGGeometrySVG,
'path': SVGGeometryPATH,
'defs': SVGGeometryDEFS,
'symbol': SVGGeometrySYMBOL,
'use': SVGGeometryUSE,
'rect': SVGGeometryRECT,
'ellipse': SVGGeometryELLIPSE,
'circle': SVGGeometryCIRCLE,
'line': SVGGeometryLINE,
'polyline': SVGGeometryPOLYLINE,
'polygon': SVGGeometryPOLYGON,
'g': SVGGeometryG}
def parseAbstractNode(node, context):
name = node.tagName.lower()
if name.startswith('svg:'):
name = name[4:]
geomClass = svgGeometryClasses.get(name)
if geomClass is not None:
ob = geomClass(node, context)
ob.parse()
return ob
return None
def load_svg(filepath):
"""
Load specified SVG file
"""
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='OBJECT')
loader = SVGLoader(filepath)
loader.parse()
loader.createGeom(False)
def load(operator, context, filepath=""):
# error in code should raise exceptions but loading
# non SVG files can give useful messages.
try:
load_svg(filepath)
except (xml.parsers.expat.ExpatError, UnicodeEncodeError) as e:
import traceback
traceback.print_exc()
operator.report({'WARNING'}, "Unable to parse XML, %s:%s for file %r" % (type(e).__name__, e, filepath))
return {'CANCELLED'}
return {'FINISHED'}