|
|
@@ -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
|