Skip to content
Snippets Groups Projects
import_svg.py 49.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • 
        def parse(self):
            """
            Parse XML node to memory
            """
    
            pass
    
    
        def _doCreateGeom(self, instancing):
    
            """
            Internal handler to create real geometries
            """
    
            pass
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
        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
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            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 = []
    
    
        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)
    
    
        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
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
                     '_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):
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            ob = SVGCreateCurve(self._context)
    
            id_names_from_node(self._node, ob)
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            if self._styles['useFill']:
    
                cu.dimensions = '2D'
    
                cu.fill_mode = 'BOTH'
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
                cu.materials.append(self._styles['fill'])
    
            else:
                cu.dimensions = '3D'
    
            for spline in self._splines:
                act_spline = None
    
    
                if spline['closed'] and len(spline['points']) >= 2:
                    first = spline['points'][0]
                    last = spline['points'][-1]
                    if (    first['handle_left_type'] == 'FREE' and
                            last['handle_right_type'] == 'VECTOR'):
                        last['handle_right_type'] = 'FREE'
                        last['handle_right'] = (last['x'], last['y'])
                    if (    last['handle_right_type'] == 'FREE' and
                            first['handle_left_type'] == 'VECTOR'):
                        first['handle_left_type'] = 'FREE'
                        first['handle_left'] = (first['x'], first['y'])
    
    
                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(1)
    
    
                    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):
    
            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:
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
                rect = SVGRectFromNode(self._node, self._context)
                self._pushRect(rect)
    
    
                self._pushMatrix(self.getNodeMatrix())
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
    
    
                geom.createGeom(True)
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
                self._popRect()
    
    
    class SVGGeometryRECT(SVGGeometry):
        """
        SVG rectangle
        """
    
    
        __slots__ = ('_rect',  # coordinate and dimensions 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(1)
    
    
            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
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            ob = SVGCreateCurve(self._context)
    
            cu = ob.data
    
    
            id_names_from_node(self._node, ob)
    
    
            if self._styles['useFill']:
                cu.dimensions = '2D'
    
                cu.fill_mode = 'BOTH'
    
                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
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            ob = SVGCreateCurve(self._context)
    
            cu = ob.data
    
    
            id_names_from_node(self._node, ob)
    
            if self._styles['useFill']:
                cu.dimensions = '2D'
    
                cu.fill_mode = 'BOTH'
    
                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(1)
    
    
                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
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            ob = SVGCreateCurve(self._context)
    
            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(1)
    
    
                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 = parse_array_of_floats(self._node.getAttribute('points'))
    
    
            prev = None
            self._points = []
    
            for p in points:
                if prev is None:
                    prev = p
                else:
    
                    self._points.append((prev, p))
    
        def _doCreateGeom(self, instancing):
    
            """
            Create real geometries
            """
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            ob = SVGCreateCurve(self._context)
    
            cu = ob.data
    
    
            if self._closed and self._styles['useFill']:
                cu.dimensions = '2D'
    
                cu.fill_mode = 'BOTH'
    
                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(1)
    
    
                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)
    
    
            # Better SVG compatibility: match svg-document units
    
            # with blender units
    
    
            if self._node.getAttribute('height'):
                raw_height = self._node.getAttribute('height')
    
                token, last_char = read_float(raw_height)
    
                document_height = float(token)
    
                unit = raw_height[last_char:].strip()
    
    
            if self._node.getAttribute('viewBox'):
                viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
    
            if len(viewbox) == 4 and unit in ('cm', 'mm', 'in', 'pt', 'pc'):
    
    
                #convert units to BU:
                unitscale = units[unit] / 90 * 1000 / 39.3701
    
                #apply blender unit scale:
    
                unitscale = unitscale / bpy.context.scene.unit_settings.scale_length
    
                matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((1.0, 0.0, 0.0)))
    
                matrix = matrix @ Matrix.Scale(unitscale, 4, Vector((0.0, 1.0, 0.0)))
    
            # match document origin with 3D space origin.
            if self._node.getAttribute('viewBox'):
                viewbox = parse_array_of_floats(self._node.getAttribute('viewBox'))
                matrix = matrix @ matrix.Translation([0.0, - viewbox[1] - viewbox[3], 0.0])
    
            super()._doCreateGeom(False)
    
    
            self._popRect()
            self._popMatrix()
    
    
    class SVGLoader(SVGGeometryContainer):
        """
        SVG file loader
        """
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
        def getTransformMatrix(self):
            """
            Get matrix created from "transform" attribute
            """
    
            # SVG document doesn't support transform specification
            # it can't even hold attributes
    
            return None
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
        def __init__(self, context, filepath, do_colormanage):
    
            """
            Initialize SVG loader
            """
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            import os
    
            svg_name = os.path.basename(filepath)
            scene = context.scene
            collection = bpy.data.collections.new(name=svg_name)
            scene.collection.children.link(collection)
    
    
            node = xml.dom.minidom.parse(filepath)
    
            m = Matrix()
    
            m = m @ Matrix.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
            m = m @ Matrix.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
    
    
            self._context = {'defines': {},
                             'transform': [],
                             'rects': [rect],
                             'rect': rect,
                             'matrix': m,
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
                             'do_colormanage': do_colormanage,
                             'collection': collection}
    
    
            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()
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
    
        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
    
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
    def load_svg(context, filepath, do_colormanage):
    
        """
        Load specified SVG file
        """
    
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set(mode='OBJECT')
    
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
        loader = SVGLoader(context, filepath, do_colormanage)
    
        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'
    
    Sergey Sharybin's avatar
    Sergey Sharybin committed
            load_svg(context, 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'}