diff --git a/io_curve_svg/__init__.py b/io_curve_svg/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c3ddc8d2f4272349e520ca50f620e68be80eefe7 --- /dev/null +++ b/io_curve_svg/__init__.py @@ -0,0 +1,83 @@ +# ##### 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> + +bl_info = { + "name": "Scalable Vector Graphics (SVG) 1.1 format", + "author": "Sergey Sharybin", + "blender": (2, 5, 6), + "api": 34996, + "location": "File > Import-Export", + "description": "Import SVG", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\ + "Scripts/Import-Export/SVG", + "tracker_url": "", + "support": 'OFFICIAL', + "category": "Import-Export"} + +# To support reload properly, try to access a package var, +# if it's there, reload everything +if "bpy" in locals(): + import imp + if "import_svg" in locals(): + imp.reload(import_svg) + + +import bpy +from bpy.props import * +from io_utils import ImportHelper, ExportHelper + + +class ImportSVG(bpy.types.Operator, ImportHelper): + '''Load a SVG file''' + bl_idname = "import_curve.svg" + bl_label = "Import SVG" + + filename_ext = ".svg" + filter_glob = StringProperty(default="*.svg", options={'HIDDEN'}) + + def execute(self, context): + from . import import_svg + + return import_svg.load(self, context, + **self.as_keywords(ignore=("filter_glob",))) + + +def menu_func_import(self, context): + self.layout.operator(ImportSVG.bl_idname, + text="Scalable Vector Graphics (.svg)") + + +def register(): + bpy.utils.register_module(__name__) + + bpy.types.INFO_MT_file_import.append(menu_func_import) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + bpy.types.INFO_MT_file_import.remove(menu_func_import) + +# NOTES +# - blender version is hardcoded + +if __name__ == "__main__": + register() diff --git a/io_curve_svg/import_svg.py b/io_curve_svg/import_svg.py new file mode 100644 index 0000000000000000000000000000000000000000..4443565a0c46b2cd2286e36de56861d40d3bacca --- /dev/null +++ b/io_curve_svg/import_svg.py @@ -0,0 +1,1156 @@ +# ##### 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 * 0.254, + 'cm': 90 * 2.54, + 'pt': 1.25, + 'pc': 15.0, + 'em': 1.0, + 'ex': 1.0} + + +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.. + """ + + r = re.compile('([0-9\\-\\+\\.])([A-z%]*)') + val = float(r.sub('\\1', coord)) + unit = r.sub('\\2', coord).lower() + + if unit == '%': + return float(size) / 100.0 * val + else: + global SVGUnits + + 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').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 + """ + + 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.Translation(Vector((x, y, 0.0))) + if len(context['rects']) > 1: + m = m * m.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0))) + m = m * m.Scale(h / rect[1], 4, Vector((0.0, 1.0, 0.0))) + + if node.getAttribute('viewBox'): + viewBox = node.getAttribute('viewBox').split() + vx = SVGParseCoord(viewBox[0], w) + vy = SVGParseCoord(viewBox[1], h) + vw = SVGParseCoord(viewBox[2], w) + vh = SVGParseCoord(viewBox[3], h) + + m = m * m.Translation(Vector((-vx, -vy, 0.0))) + m = m * m.Scale(w / vw, 4, Vector((1.0, 0.0, 0.0))) + m = m * m.Scale(h / vh, 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'] + + 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] + else: + return None + + mat = bpy.data.materials.new(name='SVGMat') + mat.diffuse_color = diff + + materials[color] = mat + + return mat + + +def SVGTransformTranslate(params): + """ + translate SVG transform command + """ + + tx = float(params[0]) + ty = float(params[1]) + 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, 0.0), + (b, d, 0.0, 0.0), + (0, 0, 1.0, 0.0), + (e, f, 0.0, 1.0))) + + +def SVGTransformScale(params): + """ + scale SVG transform command + """ + + sx = sy = float(params[0]) + if len(params) > 1: + sy = float(params[1]) + + m = Matrix() + + m = m * m.Scale(sx, 4, Vector((1.0, 0.0, 0.0))) + m = m * m.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} + +#### 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 + """ + + # Convert to easy-to-parse format + d = d.replace(',', ' ') + d = re.sub('([A-z])', ' \\1 ', d) + + self._data = d.split() + self._index = 0 + self._len = len(self._data) + + 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 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) + + 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 + """ + + c = code.lower() + 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 + + 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'] + + id = node.getAttribute('id') + if id and defs.get('#' + id) is None: + defs['#' + 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 _transformCoord(self, point): + """ + Transform SVG-file coords + """ + + v = Vector((point[0], point[1], 0.0)) + + return v * self._context['matrix'] + + 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): + """ + Internal handler to create real geometries + """ + + pass + + def _getTranformMatrix(self): + """ + Get matrix created from "transform" attribute + """ + + if not hasattr(self._node, 'getAttribute'): + return None + + transform = self._node.getAttribute('transform') + + if transform: + return SVGParseTransform(transform) + + return None + + def createGeom(self): + """ + Create real geometries + """ + + if self._creating: + return + + self._creating = True + + matrix = self._getTranformMatrix() + if matrix is not None: + self._pushMatrix(matrix) + + self._doCreateGeom() + + if matrix is not None: + self._popMatrix() + + self._creating = False + + +class SVGGeometryContainer(SVGGeometry): + """ + Container of SVG geometries + """ + + __slots__ = ('_geometries') # List of chold geometries + + def __init__(self, node, context): + """ + Initialize SVG geometry container + """ + + super().__init__(node, context) + + self._geometries = [] + + def parse(self): + """ + Parse XML node to memory + """ + + 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) + + def _doCreateGeom(self): + """ + Create real geometries + """ + + for geom in self._geometries: + geom.createGeom() + + def getGeometries(self): + """ + Get list of parsed geometries + """ + + return self._geometries + + +class SVGGeometryPATH(SVGGeometry): + """ + SVG path geometry + """ + + __slots__ = ('_splines', # List of splines after parsing + '_useFill', # Should path be filled? + '_fill') # Material used for filling + + def __init__(self, node, context): + """ + Initialize SVG path + """ + + super().__init__(node, context) + + self._splines = [] + self._fill = None + self._useFill = False + + def parse(self): + """ + Parse SVG path node + """ + + d = self._node.getAttribute('d') + + pathParser = SVGPathParser(d) + pathParser.parse() + + self._splines = pathParser.getSplines() + self._fill = None + self._useFill = False + + fill = self._node.getAttribute('fill') + if fill: + self._useFill = True + self._fill = SVGGetMaterial(fill, self._context) + + def _doCreateGeom(self): + """ + Create real geometries + """ + + ob = SVGCreateCurve() + cu = ob.data + + if self._useFill: + cu.dimensions = '2D' + cu.materials.append(self._fill) + else: + cu.dimensions = '3D' + + for spline in self._splines: + act_spline = None + for point in spline['points']: + loc = 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.select_control_point = True + bezt.select_left_handle = True + bezt.select_right_handle = True + bezt.co = loc + + 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 _doCreateGeom(self): + """ + Create real geometries + """ + + pass + + +class SVGGeometrySYMBOL(SVGGeometryContainer): + """ + Referenced element + """ + + def _doCreateGeom(self): + """ + Create real geometries + """ + + pass + + +class SVGGeometryG(SVGGeometryContainer): + """ + Geometry group + """ + + pass + + +class SVGGeometryUSE(SVGGeometry): + """ + User of referenced elements + """ + + def _doCreateGeom(self): + """ + Create real geometries + """ + + geometries = [] + ref = self._node.getAttribute('xlink:href') + geom = self._context['defines'].get(ref) + + if geom is not None: + self._pushMatrix(self.getNodeMatrix()) + self._pushMatrix(geom.getNodeMatrix()) + + if isinstance(geom, SVGGeometryContainer): + geometries = geom.getGeometries() + else: + geometries = [geom] + + for g in geometries: + g.createGeom() + + self._popMatrix() + self._popMatrix() + + +class SVGGeometrySVG(SVGGeometryContainer): + """ + Main geometry holder + """ + + def _doCreateGeom(self): + """ + Create real geometries + """ + + rect = SVGRectFromNode(self._node, self._context) + + self._pushMatrix(self.getNodeMatrix()) + self._pushRect(rect) + + super()._doCreateGeom() + + self._popRect() + self._popMatrix() + + +class SVGLoader(SVGGeometryContainer): + """ + SVG file loader + """ + + def __init__(self, filepath): + """ + Initialize SVG loader + """ + + node = xml.dom.minidom.parse(filepath) + + m = Matrix() + m = m * m.Scale(1.0 / 90.0, 4, Vector((1.0, 0.0, 0.0))) + m = m * m.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': {}} + + super().__init__(node, self._context) + + +svgGeometryClasses = { + 'svg': SVGGeometrySVG, + 'path': SVGGeometryPATH, + 'defs': SVGGeometryDEFS, + 'symbol': SVGGeometrySYMBOL, + 'use': SVGGeometryUSE, + 'g': SVGGeometryG} + + +def parseAbstractNode(node, context): + name = node.tagName.lower() + 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() + + +def load(operator, context, filepath=""): + + load_svg(filepath) + + return {'FINISHED'} diff --git a/io_curve_svg/svg_colors.py b/io_curve_svg/svg_colors.py new file mode 100644 index 0000000000000000000000000000000000000000..fd5e95488bf9e2db057ff80e7e00fe15d17894e3 --- /dev/null +++ b/io_curve_svg/svg_colors.py @@ -0,0 +1,172 @@ +# ##### 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> + +# +# Copied and adoptedfrom paths_svg2obj.py scring for Blender 2.49 which is +# Copyright (c) jm soler juillet/novembre 2004-april 2009, +# + +SVGColors = {'aliceblue': (240, 248, 255), + 'antiquewhite': (250, 235, 215), + 'aqua': (0, 255, 255), + 'aquamarine': (127, 255, 212), + 'azure': (240, 255, 255), + 'beige': (245, 245, 220), + 'bisque': (255, 228, 196), + 'black': (0, 0, 0), + 'blanchedalmond': (255, 235, 205), + 'blue': (0, 0, 255), + 'blueviolet': (138, 43, 226), + 'brown': (165, 42, 42), + 'burlywood': (222, 184, 135), + 'cadetblue': (95, 158, 160), + 'chartreuse': (127, 255, 0), + 'chocolate': (210, 105, 30), + 'coral': (255, 127, 80), + 'cornflowerblue': (100, 149, 237), + 'cornsilk': (255, 248, 220), + 'crimson': (220, 20, 60), + 'cyan': (0, 255, 255), + 'darkblue': (0, 0, 139), + 'darkcyan': (0, 139, 139), + 'darkgoldenrod': (184, 134, 11), + 'darkgray': (169, 169, 169), + 'darkgreen': (0, 100, 0), + 'darkgrey': (169, 169, 169), + 'darkkhaki': (189, 183, 107), + 'darkmagenta': (139, 0, 139), + 'darkolivegreen': (85, 107, 47), + 'darkorange': (255, 140, 0), + 'darkorchid': (153, 50, 204), + 'darkred': (139, 0, 0), + 'darksalmon': (233, 150, 122), + 'darkseagreen': (143, 188, 143), + 'darkslateblue': (72, 61, 139), + 'darkslategray': (47, 79, 79), + 'darkslategrey': (47, 79, 79), + 'darkturquoise': (0, 206, 209), + 'darkviolet': (148, 0, 211), + 'deeppink': (255, 20, 147), + 'deepskyblue': (0, 191, 255), + 'dimgray': (105, 105, 105), + 'dimgrey': (105, 105, 105), + 'dodgerblue': (30, 144, 255), + 'firebrick': (178, 34, 34), + 'floralwhite': (255, 250, 240), + 'forestgreen': (34, 139, 34), + 'fuchsia': (255, 0, 255), + 'gainsboro': (220, 220, 220), + 'ghostwhite': (248, 248, 255), + 'gold': (255, 215, 0), + 'goldenrod': (218, 165, 32), + 'gray': (128, 128, 128), + 'grey': (128, 128, 128), + 'green': (0, 128, 0), + 'greenyellow': (173, 255, 47), + 'honeydew': (240, 255, 240), + 'hotpink': (255, 105, 180), + 'indianred': (205, 92, 92), + 'indigo': (75, 0, 130), + 'ivory': (255, 255, 240), + 'khaki': (240, 230, 140), + 'lavender': (230, 230, 250), + 'lavenderblush': (255, 240, 245), + 'lawngreen': (124, 252, 0), + 'lemonchiffon': (255, 250, 205), + 'lightblue': (173, 216, 230), + 'lightcoral': (240, 128, 128), + 'lightcyan': (224, 255, 255), + 'lightgoldenrodyellow': (250, 250, 210), + 'lightgray': (211, 211, 211), + 'lightgreen': (144, 238, 144), + 'lightgrey': (211, 211, 211), + 'lightpink': (255, 182, 193), + 'lightsalmon': (255, 160, 122), + 'lightseagreen': (32, 178, 170), + 'lightskyblue': (135, 206, 250), + 'lightslategray': (119, 136, 153), + 'lightslategrey': (119, 136, 153), + 'lightsteelblue': (176, 196, 222), + 'lightyellow': (255, 255, 224), + 'lime': (0, 255, 0), + 'limegreen': (50, 205, 50), + 'linen': (250, 240, 230), + 'magenta': (255, 0, 255), + 'maroon': (128, 0, 0), + 'mediumaquamarine': (102, 205, 170), + 'mediumblue': (0, 0, 205), + 'mediumorchid': (186, 85, 211), + 'mediumpurple': (147, 112, 219), + 'mediumseagreen': (60, 179, 113), + 'mediumslateblue': (123, 104, 238), + 'mediumspringgreen': (0, 250, 154), + 'mediumturquoise': (72, 209, 204), + 'mediumvioletred': (199, 21, 133), + 'midnightblue': (25, 25, 112), + 'mintcream': (245, 255, 250), + 'mistyrose': (255, 228, 225), + 'moccasin': (255, 228, 181), + 'navajowhite': (255, 222, 173), + 'navy': (0, 0, 128), + 'oldlace': (253, 245, 230), + 'olive': (128, 128, 0), + 'olivedrab': (107, 142, 35), + 'orange': (255, 165, 0), + 'orangered': (255, 69, 0), + 'orchid': (218, 112, 214), + 'palegoldenrod': (238, 232, 170), + 'palegreen': (152, 251, 152), + 'paleturquoise': (175, 238, 238), + 'palevioletred': (219, 112, 147), + 'papayawhip': (255, 239, 213), + 'peachpuff': (255, 218, 185), + 'peru': (205, 133, 63), + 'pink': (255, 192, 203), + 'plum': (221, 160, 221), + 'powderblue': (176, 224, 230), + 'purple': (128, 0, 128), + 'red': (255, 0, 0), + 'rosybrown': (188, 143, 143), + 'royalblue': (65, 105, 225), + 'saddlebrown': (139, 69, 19), + 'salmon': (250, 128, 114), + 'sandybrown': (244, 164, 96), + 'seagreen': (46, 139, 87), + 'seashell': (255, 245, 238), + 'sienna': (160, 82, 45), + 'silver': (192, 192, 192), + 'skyblue': (135, 206, 235), + 'slateblue': (106, 90, 205), + 'slategray': (112, 128, 144), + 'slategrey': (112, 128, 144), + 'snow': (255, 250, 250), + 'springgreen': (0, 255, 127), + 'steelblue': (70, 130, 180), + 'tan': (210, 180, 140), + 'teal': (0, 128, 128), + 'thistle': (216, 191, 216), + 'tomato': (255, 99, 71), + 'turquoise': (64, 224, 208), + 'violet': (238, 130, 238), + 'wheat': (245, 222, 179), + 'white': (255, 255, 255), + 'whitesmoke': (245, 245, 245), + 'yellow': (255, 255, 0), + 'yellowgreen': (154, 205, 50)}