# ##### 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 ##### # 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.0, "mm": 90.0 / 25.4, "cm": 90.0 / 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 srgb_to_linearrgb(c): if c < 0.04045: return 0.0 if c < 0.0 else c * (1.0 / 12.92); else: return pow((c + 0.055) * (1.0 / 1.055), 2.4); 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 weird 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]) 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 = m * Matrix.Translation(Vector((x, y, 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) if vw != 0 and vh != 0: 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))) else: vw = w vh = h if len(context['rects']) > 1: if rect[0] != 0 and rect[1] != 0: m = m * Matrix.Scale(vw / rect[0], 4, Vector((1.0, 0.0, 0.0))) m = m * Matrix.Scale(vh / rect[1], 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 diffuse_color = ([x / 255.0 for x in diff]) if context['do_colormanage']: diffuse_color[0] = srgb_to_linearrgb(diffuse_color[0]) diffuse_color[1] = srgb_to_linearrgb(diffuse_color[1]) diffuse_color[2] = srgb_to_linearrgb(diffuse_color[2]) mat = bpy.data.materials.new(name='SVGMat') mat.diffuse_color = diffuse_color mat.diffuse_intensity = 1.0 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') # Length of 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 '_use_fill', # Splines would be filled, so expected to be closed ) def __init__(self, d, use_fill): """ 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._use_fill = use_fill 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 about 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 last = self._spline['points'][-1] if last['handle_right_type'] == 'VECTOR' and handle_left_type == 'FREE': last['handle_right'] = (last['x'], last['y']) last['handle_right_type'] = 'FREE' 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 script 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 """ closed = False 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)) if cmd in {'Z', 'z'}: closed =True else: closed = False cmd(code) if self._use_fill and not closed: self._pathClose('z') 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') self._styles = SVGParseStyles(self._node, self._context) pathParser = SVGPathParser(d, self._styles['useFill']) pathParser.parse() self._splines = pathParser.getSplines() 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._pushRect(rect) matrix = self.getNodeMatrix() # Better Inkscape compatibility: match document origin with # 3D space origin. if self._node.getAttribute('inkscape:version'): raw_height = self._node.getAttribute('height') document_height = SVGParseCoord(raw_height, 1.0) matrix = matrix * Matrix.Translation([0.0, -document_height , 0.0]) self._pushMatrix(matrix) 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, do_colormanage): """ Initialize SVG loader """ node = xml.dom.minidom.parse(filepath) m = Matrix() m = m * Matrix.Scale( 1.0/90.0 * 25.4/1000, 4, Vector((1.0, 0.0, 0.0))) m = m * Matrix.Scale(-1.0/90.0 * 25.4/1000, 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, 'do_colormanage': do_colormanage} 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, do_colormanage): """ Load specified SVG file """ if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode='OBJECT') loader = SVGLoader(filepath, do_colormanage) 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. do_colormanage = context.scene.display_settings.display_device != 'NONE' try: load_svg(filepath, do_colormanage) 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'}