diff --git a/CHANGELOG.md b/CHANGELOG.md index edb1e435..6805dec6 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/appParsers/ParseGerber.py b/appParsers/ParseGerber.py index d4e336c7..fda244d5 100644 --- a/appParsers/ParseGerber.py +++ b/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] diff --git a/appParsers/ParseSVG.py b/appParsers/ParseSVG.py index 37f51954..e9ae8968 100644 --- a/appParsers/ParseSVG.py +++ b/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 + # 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: - 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) - - else: - log.warning("Unknown kind: " + kind) - geo = None + 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.") diff --git a/appTools/ToolQRCode.py b/appTools/ToolQRCode.py index 924f6687..a9cbbd5d 100644 --- a/appTools/ToolQRCode.py +++ b/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] diff --git a/camlib.py b/camlib.py index 39976466..51e473f0 100644 --- a/camlib.py +++ b/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]