Jelajahi Sumber

- added support for virtual units in SVG parser; warning: it may require the support for units which is not implemented yet

Marius Stanciu 5 tahun lalu
induk
melakukan
09aafe5601
5 mengubah file dengan 134 tambahan dan 102 penghapusan
  1. 4 0
      CHANGELOG.md
  2. 3 2
      appParsers/ParseGerber.py
  3. 121 98
      appParsers/ParseSVG.py
  4. 3 1
      appTools/ToolQRCode.py
  5. 3 1
      camlib.py

+ 4 - 0
CHANGELOG.md

@@ -7,6 +7,10 @@ CHANGELOG for FlatCAM beta
 
 =================================================
 
+23.09.2020
+
+- added support for virtual units in SVG parser; warning: it may require the support for units which is not implemented yet
+
 22.09.2020
 
 - fixed an error in importing SVG that has a single line

+ 3 - 2
appParsers/ParseGerber.py

@@ -17,7 +17,7 @@ from lxml import etree as ET
 import ezdxf
 
 from appParsers.ParseDXF import *
-from appParsers.ParseSVG import svgparselength, getsvggeo
+from appParsers.ParseSVG import svgparselength, getsvggeo, svgparse_viewbox
 
 import gettext
 import builtins
@@ -1812,7 +1812,8 @@ class Gerber(Geometry):
 
         units = self.app.defaults['units'] if units is None else units
         res = self.app.defaults['gerber_circle_steps']
-        geos = getsvggeo(svg_root, 'gerber', units=units, res=res)
+        factor = svgparse_viewbox(svg_root)
+        geos = getsvggeo(svg_root, 'gerber', units=units, res=res, factor=factor)
         if flip:
             geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
 

+ 121 - 98
appParsers/ParseSVG.py

@@ -73,7 +73,7 @@ def svgparse_viewbox(root):
     return w / v_w
 
 
-def path2shapely(path, object_type, res=1.0, units='MM'):
+def path2shapely(path, object_type, res=1.0, units='MM', factor=1.0):
     """
     Converts an svg.path.Path into a Shapely
     Polygon or LinearString.
@@ -83,6 +83,8 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
     :param res:         Resolution (minimum step along path)
     :param units:       FlatCAM units
     :type units:        str
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
     :return:            Shapely geometry object
     :rtype :            Polygon
     :rtype :            LineString
@@ -102,7 +104,7 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
             if len(points) == 0 or points[-1] != (x, y):
                 points.append((x, y))
             end = component.end
-            points.append((end.real, end.imag))
+            points.append((factor * end.real, factor * end.imag))
             continue
 
         # Arc, CubicBezier or QuadraticBezier
@@ -131,9 +133,9 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
                 point = component.point(i * frac)
                 x, y = point.real, point.imag
                 if len(points) == 0 or points[-1] != (x, y):
-                    points.append((x, y))
+                    points.append((factor * x, factor * y))
             end = component.point(1.0)
-            points.append((end.real, end.imag))
+            points.append((factor * end.real, factor * end.imag))
             continue
 
         # Move
@@ -148,7 +150,7 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
                     closed = False
                     start = component.start
                     x, y = start.real, start.imag
-                    points = [(x, y)]
+                    points = [(factor * x, factor * y)]
             continue
 
         closed = False
@@ -169,6 +171,7 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
 
     if points:
         rings.append(points)
+
     try:
         rings = MultiLineString(rings)
     except Exception as e:
@@ -198,7 +201,7 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
     return geometry
 
 
-def svgrect2shapely(rect, n_points=32):
+def svgrect2shapely(rect, n_points=32, factor=1.0):
     """
     Converts an SVG rect into Shapely geometry.
 
@@ -206,22 +209,27 @@ def svgrect2shapely(rect, n_points=32):
     :type rect:         xml.etree.ElementTree.Element
     :param n_points:    number of points to approximate rectangles corners when having rounded corners
     :type n_points:     int
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
     :return:            shapely.geometry.polygon.LinearRing
     """
     w = svgparselength(rect.get('width'))[0]
     h = svgparselength(rect.get('height'))[0]
+
     x_obj = rect.get('x')
     if x_obj is not None:
-        x = svgparselength(x_obj)[0]
+        x = svgparselength(x_obj)[0] * factor
     else:
         x = 0
+
     y_obj = rect.get('y')
     if y_obj is not None:
-        y = svgparselength(y_obj)[0]
+        y = svgparselength(y_obj)[0] * factor
     else:
         y = 0
-    rxstr = rect.get('rx')
-    rystr = rect.get('ry')
+
+    rxstr = rect.get('rx') * factor
+    rystr = rect.get('ry') * factor
 
     if rxstr is None and rystr is None:  # Sharp corners
         pts = [
@@ -268,7 +276,7 @@ def svgrect2shapely(rect, n_points=32):
     # return LinearRing(pts)
 
 
-def svgcircle2shapely(circle, n_points=64):
+def svgcircle2shapely(circle, n_points=64, factor=1.0):
     """
     Converts an SVG circle into Shapely geometry.
 
@@ -282,29 +290,30 @@ def svgcircle2shapely(circle, n_points=64):
     # cx = float(circle.get('cx'))
     # cy = float(circle.get('cy'))
     # r = float(circle.get('r'))
-    cx = svgparselength(circle.get('cx'))[0]  # TODO: No units support yet
-    cy = svgparselength(circle.get('cy'))[0]  # TODO: No units support yet
-    r = svgparselength(circle.get('r'))[0]  # TODO: No units support yet
+    cx = svgparselength(circle.get('cx'))[0] * factor  # TODO: No units support yet
+    cy = svgparselength(circle.get('cy'))[0] * factor  # TODO: No units support yet
+    r = svgparselength(circle.get('r'))[0] * factor  # TODO: No units support yet
 
     return Point(cx, cy).buffer(r, resolution=n_points)
 
 
-def svgellipse2shapely(ellipse, n_points=64):
+def svgellipse2shapely(ellipse, n_points=64, factor=1.0):
     """
     Converts an SVG ellipse into Shapely geometry
 
-    :param ellipse: Ellipse Element
-    :type ellipse: xml.etree.ElementTree.Element
-    :param n_points: Number of discrete points in output.
-    :return: Shapely representation of the ellipse.
-    :rtype: shapely.geometry.polygon.LinearRing
+    :param ellipse:     Ellipse Element
+    :type ellipse:      xml.etree.ElementTree.Element
+    :param n_points:    Number of discrete points in output.
+    :type n_points:     int
+    :return:            Shapely representation of the ellipse.
+    :rtype:             shapely.geometry.polygon.LinearRing
     """
 
-    cx = svgparselength(ellipse.get('cx'))[0]  # TODO: No units support yet
-    cy = svgparselength(ellipse.get('cy'))[0]  # TODO: No units support yet
+    cx = svgparselength(ellipse.get('cx'))[0] * factor  # TODO: No units support yet
+    cy = svgparselength(ellipse.get('cy'))[0] * factor  # TODO: No units support yet
 
-    rx = svgparselength(ellipse.get('rx'))[0]  # TODO: No units support yet
-    ry = svgparselength(ellipse.get('ry'))[0]  # TODO: No units support yet
+    rx = svgparselength(ellipse.get('rx'))[0] * factor  # TODO: No units support yet
+    ry = svgparselength(ellipse.get('ry'))[0] * factor  # TODO: No units support yet
 
     t = np.arange(n_points, dtype=float) / n_points
     x = cx + rx * np.cos(2 * np.pi * t)
@@ -315,32 +324,43 @@ def svgellipse2shapely(ellipse, n_points=64):
     # return LinearRing(pts)
 
 
-def svgline2shapely(line):
+def svgline2shapely(line, factor=1.0):
     """
 
-    :param line: Line element
-    :type line: xml.etree.ElementTree.Element
-    :return: Shapely representation on the line.
-    :rtype: shapely.geometry.polygon.LinearRing
+    :param line:        Line element
+    :type line:         xml.etree.ElementTree.Element
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
+    :return:            Shapely representation on the line.
+    :rtype:             shapely.geometry.polygon.LineString
     """
 
-    x1 = svgparselength(line.get('x1'))[0]
-    y1 = svgparselength(line.get('y1'))[0]
-    x2 = svgparselength(line.get('x2'))[0]
-    y2 = svgparselength(line.get('y2'))[0]
+    x1 = svgparselength(line.get('x1'))[0] * factor
+    y1 = svgparselength(line.get('y1'))[0] * factor
+    x2 = svgparselength(line.get('x2'))[0] * factor
+    y2 = svgparselength(line.get('y2'))[0] * factor
 
     return LineString([(x1, y1), (x2, y2)])
 
 
-def svgpolyline2shapely(polyline):
+def svgpolyline2shapely(polyline, factor=1.0):
+    """
+
+    :param polyline:    Polyline element
+    :type polyline:     xml.etree.ElementTree.Element
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
+    :return:            Shapely representation of the PolyLine
+    :rtype:             shapely.geometry.polygon.LineString
+    """
 
     ptliststr = polyline.get('points')
-    points = parse_svg_point_list(ptliststr)
+    points = parse_svg_point_list(ptliststr, factor)
 
     return LineString(points)
 
 
-def svgpolygon2shapely(polygon, n_points=64):
+def svgpolygon2shapely(polygon, n_points=64, factor=1.0):
     """
     Convert a SVG polygon to a Shapely Polygon.
 
@@ -348,17 +368,19 @@ def svgpolygon2shapely(polygon, n_points=64):
     :type polygon:
     :param n_points:    circle resolution; nr of points to b e used to approximate a circle
     :type n_points:     int
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
     :return:            Shapely Polygon
     """
 
     ptliststr = polygon.get('points')
-    points = parse_svg_point_list(ptliststr)
+    points = parse_svg_point_list(ptliststr, factor)
 
     return Polygon(points).buffer(0, resolution=n_points)
     # return LinearRing(points)
 
 
-def getsvggeo(node, object_type, root=None, units='MM', res=64):
+def getsvggeo(node, object_type, root=None, units='MM', res=64, factor=1.0):
     """
     Extracts and flattens all geometry from an SVG node
     into a list of Shapely geometry.
@@ -367,8 +389,9 @@ def getsvggeo(node, object_type, root=None, units='MM', res=64):
     :param object_type:
     :param root:
     :param units:       FlatCAM units
-    :param res:         resolution to be used for circles bufferring
-
+    :param res:         resolution to be used for circles buffering
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
     :return:            List of Shapely geometry
     :rtype:             list
     """
@@ -381,61 +404,59 @@ def getsvggeo(node, object_type, root=None, units='MM', res=64):
     # Recurse
     if len(node) > 0:
         for child in node:
-            subgeo = getsvggeo(child, object_type, root=root, units=units, res=res)
+            subgeo = getsvggeo(child, object_type, root=root, units=units, res=res, factor=factor)
             if subgeo is not None:
                 geo += subgeo
-    else:
-        factor = svgparse_viewbox(node)
-        # Parse
-        if kind == 'path':
-            log.debug("***PATH***")
-            P = parse_path(node.get('d'))
-            P = path2shapely(P, object_type, units=units)
-            # for path, the resulting geometry is already a list so no need to create a new one
-            geo = P
-
-        elif kind == 'rect':
-            log.debug("***RECT***")
-            R = svgrect2shapely(node, n_points=res)
-            geo = [R]
-
-        elif kind == 'circle':
-            log.debug("***CIRCLE***")
-            C = svgcircle2shapely(node, n_points=res)
-            geo = [C]
-
-        elif kind == 'ellipse':
-            log.debug("***ELLIPSE***")
-            E = svgellipse2shapely(node, n_points=res)
-            geo = [E]
-
-        elif kind == 'polygon':
-            log.debug("***POLYGON***")
-            poly = svgpolygon2shapely(node, n_points=res)
-            geo = [poly]
-
-        elif kind == 'line':
-            log.debug("***LINE***")
-            line = svgline2shapely(node)
-            geo = [line]
-
-        elif kind == 'polyline':
-            log.debug("***POLYLINE***")
-            pline = svgpolyline2shapely(node)
-            geo = [pline]
-
-        elif kind == 'use':
-            log.debug('***USE***')
-            # href= is the preferred name for this[1], but inkscape still generates xlink:href=.
-            # [1] https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use#Attributes
-            href = node.attrib['href'] if 'href' in node.attrib else node.attrib['{http://www.w3.org/1999/xlink}href']
-            ref = root.find(".//*[@id='%s']" % href.replace('#', ''))
-            if ref is not None:
-                geo = getsvggeo(ref, object_type, root=root, units=units, res=res)
+    # Parse
+    elif kind == 'path':
+        log.debug("***PATH***")
+        P = parse_path(node.get('d'))
+        P = path2shapely(P, object_type, units=units, factor=factor)
+        # for path, the resulting geometry is already a list so no need to create a new one
+        geo = P
+
+    elif kind == 'rect':
+        log.debug("***RECT***")
+        R = svgrect2shapely(node, n_points=res, factor=factor)
+        geo = [R]
+
+    elif kind == 'circle':
+        log.debug("***CIRCLE***")
+        C = svgcircle2shapely(node, n_points=res, factor=factor)
+        geo = [C]
+
+    elif kind == 'ellipse':
+        log.debug("***ELLIPSE***")
+        E = svgellipse2shapely(node, n_points=res, factor=factor)
+        geo = [E]
+
+    elif kind == 'polygon':
+        log.debug("***POLYGON***")
+        poly = svgpolygon2shapely(node, n_points=res, factor=factor)
+        geo = [poly]
+
+    elif kind == 'line':
+        log.debug("***LINE***")
+        line = svgline2shapely(node, factor=factor)
+        geo = [line]
+
+    elif kind == 'polyline':
+        log.debug("***POLYLINE***")
+        pline = svgpolyline2shapely(node, factor=factor)
+        geo = [pline]
+
+    elif kind == 'use':
+        log.debug('***USE***')
+        # href= is the preferred name for this[1], but inkscape still generates xlink:href=.
+        # [1] https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use#Attributes
+        href = node.attrib['href'] if 'href' in node.attrib else node.attrib['{http://www.w3.org/1999/xlink}href']
+        ref = root.find(".//*[@id='%s']" % href.replace('#', ''))
+        if ref is not None:
+            geo = getsvggeo(ref, object_type, root=root, units=units, res=res, factor=factor)
 
-        else:
-            log.warning("Unknown kind: " + kind)
-            geo = None
+    else:
+        log.warning("Unknown kind: " + kind)
+        geo = None
 
     # ignore transformation for unknown kind
     if geo is not None:
@@ -565,13 +586,15 @@ def getsvgtext(node, object_type, units='MM'):
     return geo
 
 
-def parse_svg_point_list(ptliststr):
+def parse_svg_point_list(ptliststr, factor):
     """
     Returns a list of coordinate pairs extracted from the "points"
     attribute in SVG polygons and polyline's.
 
-    :param ptliststr: "points" attribute string in polygon or polyline.
-    :return: List of tuples with coordinates.
+    :param ptliststr:       "points" attribute string in polygon or polyline.
+    :param factor:          correction factor due of virtual units
+    :type factor:           float
+    :return:                List of tuples with coordinates.
     """
 
     pairs = []
@@ -584,9 +607,9 @@ def parse_svg_point_list(ptliststr):
         val = float(ptliststr[pos:match.start()])
 
         if i % 2 == 1:
-            pairs.append((last, val))
+            pairs.append((factor * last, factor * val))
         else:
-            last = val
+            last = val * factor
 
         pos = match.end()
         i += 1
@@ -594,7 +617,7 @@ def parse_svg_point_list(ptliststr):
     # Check for last element
     val = float(ptliststr[pos:])
     if i % 2 == 1:
-        pairs.append((last, val))
+        pairs.append((factor * last, factor * val))
     else:
         log.warning("Incomplete coordinates.")
 

+ 3 - 1
appTools/ToolQRCode.py

@@ -429,7 +429,9 @@ class QRCode(AppTool):
         h = svgparselength(svg_root.get('height'))[0]  # TODO: No units support yet
         units = self.app.defaults['units'] if units is None else units
         res = self.app.defaults['geometry_circle_steps']
-        geos = getsvggeo(svg_root, object_type, units=units, res=res)
+        factor = svgparse_viewbox(svg_root)
+
+        geos = getsvggeo(svg_root, object_type, units=units, res=res, factor=factor)
 
         if flip:
             geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]

+ 3 - 1
camlib.py

@@ -1116,7 +1116,9 @@ class Geometry(object):
 
         units = self.app.defaults['units'] if units is None else units
         res = self.app.defaults['geometry_circle_steps']
-        geos = getsvggeo(svg_root, object_type, units=units, res=res)
+        factor = svgparse_viewbox(svg_root)
+
+        geos = getsvggeo(svg_root, object_type, units=units, res=res, factor=factor)
         if flip:
             geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]