소스 검색

Basic support for importing SVG. Via shell only at this time. See issue #179.

Juan Pablo Caram 10 년 전
부모
커밋
fdf809774f
5개의 변경된 파일480개의 추가작업 그리고 2개의 파일을 삭제
  1. 45 0
      FlatCAMApp.py
  2. 1 1
      PlotCanvas.py
  3. 40 1
      camlib.py
  4. 268 0
      svgparse.py
  5. 126 0
      tests/svg/drawing.svg

+ 45 - 0
FlatCAMApp.py

@@ -1604,6 +1604,34 @@ class App(QtCore.QObject):
         else:
             self.inform.emit("Project copy saved to: " + self.project_filename)
 
+    def import_svg(self, filename, outname=None):
+        """
+        Adds a new Geometry Object to the projects and populates
+        it with shapes extracted from the SVG file.
+
+        :param filename: Path to the SVG file.
+        :param outname:
+        :return:
+        """
+
+        def obj_init(geo_obj, app_obj):
+
+            geo_obj.import_svg(filename)
+
+        with self.proc_container.new("Importing SVG") as proc:
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+
+            self.new_object("geometry", name, obj_init)
+
+            # TODO: No support for this yet.
+            # Register recent file
+            # self.file_opened.emit("gerber", filename)
+
+            # GUI feedback
+            self.inform.emit("Opened: " + filename)
+
     def open_gerber(self, filename, follow=False, outname=None):
         """
         Opens a Gerber file, parses it and creates a new object for
@@ -1959,6 +1987,17 @@ class App(QtCore.QObject):
 
             return a, kwa
 
+        def import_svg(filename, *args):
+            a, kwa = h(*args)
+            types = {'outname': str}
+
+            for key in kwa:
+                if key not in types:
+                    return 'Unknown parameter: %s' % key
+                kwa[key] = types[key](kwa[key])
+
+            self.import_svg(str(filename), **kwa)
+
         def open_gerber(filename, *args):
             a, kwa = h(*args)
             types = {'follow': bool,
@@ -2556,6 +2595,12 @@ class App(QtCore.QObject):
                 'fcn': shelp,
                 'help': "Shows list of commands."
             },
+            'import_svg': {
+                'fcn': import_svg,
+                'help': "Import an SVG file as a Geometry Object.\n" +
+                        "> import_svg <filename>" +
+                        "   filename: Path to the file to import."
+            },
             'open_gerber': {
                 'fcn': open_gerber,
                 'help': "Opens a Gerber file.\n' +"

+ 1 - 1
PlotCanvas.py

@@ -124,7 +124,7 @@ class PlotCanvas:
 
     def connect(self, event_name, callback):
         """
-        Attach an event handler to the canvas through the native GTK interface.
+        Attach an event handler to the canvas through the native Qt interface.
 
         :param event_name: Name of the event
         :type event_name: str

+ 40 - 1
camlib.py

@@ -42,6 +42,16 @@ import simplejson as json
 # TODO: Commented for FlatCAM packaging with cx_freeze
 #from matplotlib.pyplot import plot, subplot
 
+import xml.etree.ElementTree as ET
+from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
+import itertools
+
+import xml.etree.ElementTree as ET
+from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
+
+
+from svgparse import *
+
 import logging
 
 log = logging.getLogger('base2')
@@ -193,7 +203,6 @@ class Geometry(object):
 
         return interiors
 
-
     def get_exteriors(self, geometry=None):
         """
         Returns all exteriors of polygons in geometry. Uses
@@ -344,6 +353,36 @@ class Geometry(object):
 
         return False
 
+    def import_svg(self, filename):
+        """
+        Imports shapes from an SVG file into the object's geometry.
+
+        :param filename: Path to the SVG file.
+        :return: None
+        """
+
+        # Parse into list of shapely objects
+        svg_tree = ET.parse(filename)
+        svg_root = svg_tree.getroot()
+
+        # Change origin to bottom left
+        h = float(svg_root.get('height'))
+        # w = float(svg_root.get('width'))
+        geos = getsvggeo(svg_root)
+        geo_flip = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
+
+        # Add to object
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            self.solid_geometry.append(cascaded_union(geo_flip))
+        else:  # It's shapely geometry
+            self.solid_geometry = cascaded_union([self.solid_geometry,
+                                                  cascaded_union(geo_flip)])
+
+        return
+
     def size(self):
         """
         Returns (width, height) of rectangular

+ 268 - 0
svgparse.py

@@ -0,0 +1,268 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 12/18/2015                                           #
+# MIT Licence                                              #
+############################################################
+
+import xml.etree.ElementTree as ET
+import re
+import itertools
+from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
+from shapely.geometry import LinearRing, LineString
+from shapely.affinity import translate, rotate, scale, skew, affine_transform
+
+
+def path2shapely(path, res=1.0):
+    """
+    Converts an svg.path.Path into a Shapely
+    LinearRing or LinearString.
+
+    :rtype : LinearRing
+    :rtype : LineString
+    :param path: svg.path.Path instance
+    :param res: Resolution (minimum step along path)
+    :return: Shapely geometry object
+    """
+    points = []
+
+    for component in path:
+
+        # Line
+        if isinstance(component, Line):
+            start = component.start
+            x, y = start.real, start.imag
+            if len(points) == 0 or points[-1] != (x, y):
+                points.append((x, y))
+            end = component.end
+            points.append((end.real, end.imag))
+            continue
+
+        # Arc, CubicBezier or QuadraticBezier
+        if isinstance(component, Arc) or \
+           isinstance(component, CubicBezier) or \
+           isinstance(component, QuadraticBezier):
+            length = component.length(res / 10.0)
+            steps = int(length / res + 0.5)
+            frac = 1.0 / steps
+            print length, steps, frac
+            for i in range(steps):
+                point = component.point(i * frac)
+                x, y = point.real, point.imag
+                if len(points) == 0 or points[-1] != (x, y):
+                    points.append((x, y))
+            end = component.point(1.0)
+            points.append((end.real, end.imag))
+            continue
+
+        print "I don't know what this is:", component
+        continue
+
+    if path.closed:
+        return LinearRing(points)
+    else:
+        return LineString(points)
+
+
+def svgrect2shapely(rect):
+    w = float(rect.get('width'))
+    h = float(rect.get('height'))
+    x = float(rect.get('x'))
+    y = float(rect.get('y'))
+    pts = [
+        (x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
+    ]
+    return LinearRing(pts)
+
+
+def getsvggeo(node):
+    """
+    Extracts and flattens all geometry from an SVG node
+    into a list of Shapely geometry.
+
+    :param node:
+    :return:
+    """
+    kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
+    geo = []
+
+    # Recurse
+    if len(node) > 0:
+        for child in node:
+            subgeo = getsvggeo(child)
+            if subgeo is not None:
+                geo += subgeo
+
+    # Parse
+    elif kind == 'path':
+        print "***PATH***"
+        P = parse_path(node.get('d'))
+        P = path2shapely(P)
+        geo = [P]
+
+    elif kind == 'rect':
+        print "***RECT***"
+        R = svgrect2shapely(node)
+        geo = [R]
+
+    else:
+        print "Unknown kind:", kind
+        geo = None
+
+    # Transformations
+    if 'transform' in node.attrib:
+        trstr = node.get('transform')
+        trlist = parse_svg_transform(trstr)
+        print trlist
+
+        # Transformations are applied in reverse order
+        for tr in trlist[::-1]:
+            if tr[0] == 'translate':
+                geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
+            elif tr[0] == 'scale':
+                geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
+                       for geoi in geo]
+            elif tr[0] == 'rotate':
+                geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
+                       for geoi in geo]
+            elif tr[0] == 'skew':
+                geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
+                       for geoi in geo]
+            elif tr[0] == 'matrix':
+                geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
+            else:
+                raise Exception('Unknown transformation: %s', tr)
+
+    return geo
+
+
+def parse_svg_transform(trstr):
+    """
+    Parses an SVG transform string into a list
+    of transform names and their parameters.
+
+    Possible transformations are:
+
+    * Translate: translate(<tx> [<ty>]), which specifies
+      a translation by tx and ty. If <ty> is not provided,
+      it is assumed to be zero. Result is
+      ['translate', tx, ty]
+
+    * Scale: scale(<sx> [<sy>]), which specifies a scale operation
+      by sx and sy. If <sy> is not provided, it is assumed to be
+      equal to <sx>. Result is: ['scale', sx, sy]
+
+    * Rotate: rotate(<rotate-angle> [<cx> <cy>]), which specifies
+      a rotation by <rotate-angle> degrees about a given point.
+      If optional parameters <cx> and <cy> are not supplied,
+      the rotate is about the origin of the current user coordinate
+      system. Result is: ['rotate', rotate-angle, cx, cy]
+
+    * Skew: skewX(<skew-angle>), which specifies a skew
+      transformation along the x-axis. skewY(<skew-angle>), which
+      specifies a skew transformation along the y-axis.
+      Result is ['skew', angle-x, angle-y]
+
+    * Matrix: matrix(<a> <b> <c> <d> <e> <f>), which specifies a
+      transformation in the form of a transformation matrix of six
+      values. matrix(a,b,c,d,e,f) is equivalent to applying the
+      transformation matrix [a b c d e f]. Result is
+      ['matrix', a, b, c, d, e, f]
+
+    :param trstr: SVG transform string.
+    :type trstr: str
+    :return: List of transforms.
+    :rtype: list
+    """
+    trlist = []
+
+    assert isinstance(trstr, str)
+    trstr = trstr.strip(' ')
+
+    num_re_str = r'[\+\-]?[0-9\.e]+'  # TODO: Negative exponents missing
+    comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
+    translate_re_str = r'translate\s*\(\s*(' + \
+                       num_re_str + r')' + \
+                       r'(?:' + comma_or_space_re_str + \
+                       r'(' + num_re_str + r'))?\s*\)'
+    scale_re_str = r'scale\s*\(\s*(' + \
+                   num_re_str + r')' + \
+                   r'(?:' + comma_or_space_re_str + \
+                   r'(' + num_re_str + r'))?\s*\)'
+    skew_re_str = r'skew([XY])\s*\(\s*(' + \
+                  num_re_str + r')\s*\)'
+    rotate_re_str = r'rotate\s*\(\s*(' + \
+                    num_re_str + r')' + \
+                    r'(?:' + comma_or_space_re_str + \
+                    r'(' + num_re_str + r')' + \
+                    comma_or_space_re_str + \
+                    r'(' + num_re_str + r'))?\*\)'
+    matrix_re_str = r'matrix\s*\(\s*' + \
+                    r'(' + num_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + num_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + num_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + num_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + num_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + num_re_str + r')\s*\)'
+
+    while len(trstr) > 0:
+        match = re.search(r'^' + translate_re_str, trstr)
+        if match:
+            trlist.append([
+                'translate',
+                float(match.group(1)),
+                float(match.group(2)) if match.group else 0.0
+            ])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        match = re.search(r'^' + scale_re_str, trstr)
+        if match:
+            trlist.append([
+                'translate',
+                float(match.group(1)),
+                float(match.group(2)) if match.group else float(match.group(1))
+            ])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        match = re.search(r'^' + skew_re_str, trstr)
+        if match:
+            trlist.append([
+                'skew',
+                float(match.group(2)) if match.group(1) == 'X' else 0.0,
+                float(match.group(2)) if match.group(1) == 'Y' else 0.0
+            ])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        match = re.search(r'^' + rotate_re_str, trstr)
+        if match:
+            trlist.append([
+                'rotate',
+                float(match.group(1)),
+                float(match.group(2)) if match.group(2) else 0.0,
+                float(match.group(3)) if match.group(3) else 0.0
+            ])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        match = re.search(r'^' + matrix_re_str, trstr)
+        if match:
+            trlist.append(['matrix'] + [float(x) for x in match.groups()])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        raise Exception("Don't know how to parse: %s" % trstr)
+
+    return trlist
+
+
+if __name__ == "__main__":
+    tree = ET.parse('tests/svg/drawing.svg')
+    root = tree.getroot()
+    ns = re.search(r'\{(.*)\}', root.tag).group(1)
+    print ns
+    for geo in getsvggeo(root):
+        print geo

+ 126 - 0
tests/svg/drawing.svg

@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="drawing.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.4"
+     inkscape:cx="436.65332"
+     inkscape:cy="798.58794"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="968"
+     inkscape:window-height="759"
+     inkscape:window-x="1949"
+     inkscape:window-y="142"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="path2985"
+       sodipodi:cx="210.11172"
+       sodipodi:cy="201.81374"
+       sodipodi:rx="70.710678"
+       sodipodi:ry="70.710678"
+       d="m 280.8224,201.81374 a 70.710678,70.710678 0 1 1 -141.42135,0 70.710678,70.710678 0 1 1 141.42135,0 z" />
+    <rect
+       style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       id="rect2987"
+       width="82.832512"
+       height="72.73098"
+       x="343.45187"
+       y="127.06245" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 261.70701,300.10548 137.14286,-68.57143 -13.57143,104.28572 105.71429,2.85714 -232.14286,29.28572 z"
+       id="path2991"
+       inkscape:connector-curvature="0" />
+    <g
+       id="g3018"
+       transform="translate(-37.142857,-103.57143)">
+      <rect
+         y="222.01678"
+         x="508.10672"
+         height="72.73098"
+         width="82.832512"
+         id="rect2987-8"
+         style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+      <rect
+         y="177.86595"
+         x="534.49329"
+         height="72.73098"
+         width="82.832512"
+         id="rect2987-4"
+         style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+    </g>
+    <path
+       style="fill:none;stroke:#999999;stroke-width:0.69999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+       d="M 550.71875 258.84375 L 550.71875 286 L 513.59375 286 L 513.59375 358.71875 L 596.40625 358.71875 L 596.40625 331.59375 L 633.5625 331.59375 L 633.5625 258.84375 L 550.71875 258.84375 z "
+       id="rect2987-5" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 276.42857,98.076465 430.71429,83.076464"
+       id="path3037"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 164.28571,391.64789 c 12.85715,-54.28571 55.00001,21.42858 84.28572,22.85715 29.28571,1.42857 30.71429,-14.28572 30.71429,-14.28572"
+       id="path3039"
+       inkscape:connector-curvature="0" />
+    <g
+       id="g3018-3"
+       transform="matrix(0.54511991,0,0,0.54511991,308.96645,74.66094)">
+      <rect
+         y="222.01678"
+         x="508.10672"
+         height="72.73098"
+         width="82.832512"
+         id="rect2987-8-5"
+         style="fill:none;stroke:#999999;stroke-width:1.28412116;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+      <rect
+         y="177.86595"
+         x="534.49329"
+         height="72.73098"
+         width="82.832512"
+         id="rect2987-4-6"
+         style="fill:none;stroke:#999999;stroke-width:1.28412116;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+    </g>
+  </g>
+</svg>