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

This commit is contained in:
Marius Stanciu
2020-09-23 11:50:00 +03:00
parent c6a552d25a
commit 09aafe5601
5 changed files with 134 additions and 102 deletions

View File

@@ -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 22.09.2020
- fixed an error in importing SVG that has a single line - fixed an error in importing SVG that has a single line

View File

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

View File

@@ -73,7 +73,7 @@ def svgparse_viewbox(root):
return w / v_w 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 Converts an svg.path.Path into a Shapely
Polygon or LinearString. 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 res: Resolution (minimum step along path)
:param units: FlatCAM units :param units: FlatCAM units
:type units: str :type units: str
:param factor: correction factor due of virtual units
:type factor: float
:return: Shapely geometry object :return: Shapely geometry object
:rtype : Polygon :rtype : Polygon
:rtype : LineString :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): if len(points) == 0 or points[-1] != (x, y):
points.append((x, y)) points.append((x, y))
end = component.end end = component.end
points.append((end.real, end.imag)) points.append((factor * end.real, factor * end.imag))
continue continue
# Arc, CubicBezier or QuadraticBezier # Arc, CubicBezier or QuadraticBezier
@@ -131,9 +133,9 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
point = component.point(i * frac) point = component.point(i * frac)
x, y = point.real, point.imag x, y = point.real, point.imag
if len(points) == 0 or points[-1] != (x, y): if len(points) == 0 or points[-1] != (x, y):
points.append((x, y)) points.append((factor * x, factor * y))
end = component.point(1.0) end = component.point(1.0)
points.append((end.real, end.imag)) points.append((factor * end.real, factor * end.imag))
continue continue
# Move # Move
@@ -148,7 +150,7 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
closed = False closed = False
start = component.start start = component.start
x, y = start.real, start.imag x, y = start.real, start.imag
points = [(x, y)] points = [(factor * x, factor * y)]
continue continue
closed = False closed = False
@@ -169,6 +171,7 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
if points: if points:
rings.append(points) rings.append(points)
try: try:
rings = MultiLineString(rings) rings = MultiLineString(rings)
except Exception as e: except Exception as e:
@@ -198,7 +201,7 @@ def path2shapely(path, object_type, res=1.0, units='MM'):
return geometry return geometry
def svgrect2shapely(rect, n_points=32): def svgrect2shapely(rect, n_points=32, factor=1.0):
""" """
Converts an SVG rect into Shapely geometry. Converts an SVG rect into Shapely geometry.
@@ -206,22 +209,27 @@ def svgrect2shapely(rect, n_points=32):
:type rect: xml.etree.ElementTree.Element :type rect: xml.etree.ElementTree.Element
:param n_points: number of points to approximate rectangles corners when having rounded corners :param n_points: number of points to approximate rectangles corners when having rounded corners
:type n_points: int :type n_points: int
:param factor: correction factor due of virtual units
:type factor: float
:return: shapely.geometry.polygon.LinearRing :return: shapely.geometry.polygon.LinearRing
""" """
w = svgparselength(rect.get('width'))[0] w = svgparselength(rect.get('width'))[0]
h = svgparselength(rect.get('height'))[0] h = svgparselength(rect.get('height'))[0]
x_obj = rect.get('x') x_obj = rect.get('x')
if x_obj is not None: if x_obj is not None:
x = svgparselength(x_obj)[0] x = svgparselength(x_obj)[0] * factor
else: else:
x = 0 x = 0
y_obj = rect.get('y') y_obj = rect.get('y')
if y_obj is not None: if y_obj is not None:
y = svgparselength(y_obj)[0] y = svgparselength(y_obj)[0] * factor
else: else:
y = 0 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 if rxstr is None and rystr is None: # Sharp corners
pts = [ pts = [
@@ -268,7 +276,7 @@ def svgrect2shapely(rect, n_points=32):
# return LinearRing(pts) # 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. Converts an SVG circle into Shapely geometry.
@@ -282,29 +290,30 @@ def svgcircle2shapely(circle, n_points=64):
# cx = float(circle.get('cx')) # cx = float(circle.get('cx'))
# cy = float(circle.get('cy')) # cy = float(circle.get('cy'))
# r = float(circle.get('r')) # r = float(circle.get('r'))
cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet cx = svgparselength(circle.get('cx'))[0] * factor # TODO: No units support yet
cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet cy = svgparselength(circle.get('cy'))[0] * factor # TODO: No units support yet
r = svgparselength(circle.get('r'))[0] # 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) 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 Converts an SVG ellipse into Shapely geometry
:param ellipse: Ellipse Element :param ellipse: Ellipse Element
:type ellipse: xml.etree.ElementTree.Element :type ellipse: xml.etree.ElementTree.Element
:param n_points: Number of discrete points in output. :param n_points: Number of discrete points in output.
:return: Shapely representation of the ellipse. :type n_points: int
:rtype: shapely.geometry.polygon.LinearRing :return: Shapely representation of the ellipse.
:rtype: shapely.geometry.polygon.LinearRing
""" """
cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet cx = svgparselength(ellipse.get('cx'))[0] * factor # TODO: No units support yet
cy = svgparselength(ellipse.get('cy'))[0] # 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 rx = svgparselength(ellipse.get('rx'))[0] * factor # TODO: No units support yet
ry = svgparselength(ellipse.get('ry'))[0] # 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 t = np.arange(n_points, dtype=float) / n_points
x = cx + rx * np.cos(2 * np.pi * t) x = cx + rx * np.cos(2 * np.pi * t)
@@ -315,32 +324,43 @@ def svgellipse2shapely(ellipse, n_points=64):
# return LinearRing(pts) # return LinearRing(pts)
def svgline2shapely(line): def svgline2shapely(line, factor=1.0):
""" """
:param line: Line element :param line: Line element
:type line: xml.etree.ElementTree.Element :type line: xml.etree.ElementTree.Element
:return: Shapely representation on the line. :param factor: correction factor due of virtual units
:rtype: shapely.geometry.polygon.LinearRing :type factor: float
:return: Shapely representation on the line.
:rtype: shapely.geometry.polygon.LineString
""" """
x1 = svgparselength(line.get('x1'))[0] x1 = svgparselength(line.get('x1'))[0] * factor
y1 = svgparselength(line.get('y1'))[0] y1 = svgparselength(line.get('y1'))[0] * factor
x2 = svgparselength(line.get('x2'))[0] x2 = svgparselength(line.get('x2'))[0] * factor
y2 = svgparselength(line.get('y2'))[0] y2 = svgparselength(line.get('y2'))[0] * factor
return LineString([(x1, y1), (x2, y2)]) 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') ptliststr = polyline.get('points')
points = parse_svg_point_list(ptliststr) points = parse_svg_point_list(ptliststr, factor)
return LineString(points) 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. Convert a SVG polygon to a Shapely Polygon.
@@ -348,17 +368,19 @@ def svgpolygon2shapely(polygon, n_points=64):
:type polygon: :type polygon:
:param n_points: circle resolution; nr of points to b e used to approximate a circle :param n_points: circle resolution; nr of points to b e used to approximate a circle
:type n_points: int :type n_points: int
:param factor: correction factor due of virtual units
:type factor: float
:return: Shapely Polygon :return: Shapely Polygon
""" """
ptliststr = polygon.get('points') 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 Polygon(points).buffer(0, resolution=n_points)
# return LinearRing(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 Extracts and flattens all geometry from an SVG node
into a list of Shapely geometry. 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 object_type:
:param root: :param root:
:param units: FlatCAM units :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 :return: List of Shapely geometry
:rtype: list :rtype: list
""" """
@@ -381,61 +404,59 @@ def getsvggeo(node, object_type, root=None, units='MM', res=64):
# Recurse # Recurse
if len(node) > 0: if len(node) > 0:
for child in node: 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: if subgeo is not None:
geo += subgeo 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: else:
factor = svgparse_viewbox(node) log.warning("Unknown kind: " + kind)
# Parse geo = None
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
# ignore transformation for unknown kind # ignore transformation for unknown kind
if geo is not None: if geo is not None:
@@ -565,13 +586,15 @@ def getsvgtext(node, object_type, units='MM'):
return geo 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" Returns a list of coordinate pairs extracted from the "points"
attribute in SVG polygons and polyline's. attribute in SVG polygons and polyline's.
:param ptliststr: "points" attribute string in polygon or polyline. :param ptliststr: "points" attribute string in polygon or polyline.
:return: List of tuples with coordinates. :param factor: correction factor due of virtual units
:type factor: float
:return: List of tuples with coordinates.
""" """
pairs = [] pairs = []
@@ -584,9 +607,9 @@ def parse_svg_point_list(ptliststr):
val = float(ptliststr[pos:match.start()]) val = float(ptliststr[pos:match.start()])
if i % 2 == 1: if i % 2 == 1:
pairs.append((last, val)) pairs.append((factor * last, factor * val))
else: else:
last = val last = val * factor
pos = match.end() pos = match.end()
i += 1 i += 1
@@ -594,7 +617,7 @@ def parse_svg_point_list(ptliststr):
# Check for last element # Check for last element
val = float(ptliststr[pos:]) val = float(ptliststr[pos:])
if i % 2 == 1: if i % 2 == 1:
pairs.append((last, val)) pairs.append((factor * last, factor * val))
else: else:
log.warning("Incomplete coordinates.") log.warning("Incomplete coordinates.")

View File

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

View File

@@ -1116,7 +1116,9 @@ class Geometry(object):
units = self.app.defaults['units'] if units is None else units units = self.app.defaults['units'] if units is None else units
res = self.app.defaults['geometry_circle_steps'] 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: if flip:
geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]