svgparse.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 12/18/2015 #
  6. # MIT Licence #
  7. ############################################################
  8. import xml.etree.ElementTree as ET
  9. import re
  10. import itertools
  11. from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
  12. from shapely.geometry import LinearRing, LineString
  13. from shapely.affinity import translate, rotate, scale, skew, affine_transform
  14. def path2shapely(path, res=1.0):
  15. """
  16. Converts an svg.path.Path into a Shapely
  17. LinearRing or LinearString.
  18. :rtype : LinearRing
  19. :rtype : LineString
  20. :param path: svg.path.Path instance
  21. :param res: Resolution (minimum step along path)
  22. :return: Shapely geometry object
  23. """
  24. points = []
  25. for component in path:
  26. # Line
  27. if isinstance(component, Line):
  28. start = component.start
  29. x, y = start.real, start.imag
  30. if len(points) == 0 or points[-1] != (x, y):
  31. points.append((x, y))
  32. end = component.end
  33. points.append((end.real, end.imag))
  34. continue
  35. # Arc, CubicBezier or QuadraticBezier
  36. if isinstance(component, Arc) or \
  37. isinstance(component, CubicBezier) or \
  38. isinstance(component, QuadraticBezier):
  39. length = component.length(res / 10.0)
  40. steps = int(length / res + 0.5)
  41. frac = 1.0 / steps
  42. print length, steps, frac
  43. for i in range(steps):
  44. point = component.point(i * frac)
  45. x, y = point.real, point.imag
  46. if len(points) == 0 or points[-1] != (x, y):
  47. points.append((x, y))
  48. end = component.point(1.0)
  49. points.append((end.real, end.imag))
  50. continue
  51. print "I don't know what this is:", component
  52. continue
  53. if path.closed:
  54. return LinearRing(points)
  55. else:
  56. return LineString(points)
  57. def svgrect2shapely(rect):
  58. w = float(rect.get('width'))
  59. h = float(rect.get('height'))
  60. x = float(rect.get('x'))
  61. y = float(rect.get('y'))
  62. pts = [
  63. (x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
  64. ]
  65. return LinearRing(pts)
  66. def getsvggeo(node):
  67. """
  68. Extracts and flattens all geometry from an SVG node
  69. into a list of Shapely geometry.
  70. :param node:
  71. :return:
  72. """
  73. kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
  74. geo = []
  75. # Recurse
  76. if len(node) > 0:
  77. for child in node:
  78. subgeo = getsvggeo(child)
  79. if subgeo is not None:
  80. geo += subgeo
  81. # Parse
  82. elif kind == 'path':
  83. print "***PATH***"
  84. P = parse_path(node.get('d'))
  85. P = path2shapely(P)
  86. geo = [P]
  87. elif kind == 'rect':
  88. print "***RECT***"
  89. R = svgrect2shapely(node)
  90. geo = [R]
  91. else:
  92. print "Unknown kind:", kind
  93. geo = None
  94. # Transformations
  95. if 'transform' in node.attrib:
  96. trstr = node.get('transform')
  97. trlist = parse_svg_transform(trstr)
  98. print trlist
  99. # Transformations are applied in reverse order
  100. for tr in trlist[::-1]:
  101. if tr[0] == 'translate':
  102. geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
  103. elif tr[0] == 'scale':
  104. geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
  105. for geoi in geo]
  106. elif tr[0] == 'rotate':
  107. geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
  108. for geoi in geo]
  109. elif tr[0] == 'skew':
  110. geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
  111. for geoi in geo]
  112. elif tr[0] == 'matrix':
  113. geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
  114. else:
  115. raise Exception('Unknown transformation: %s', tr)
  116. return geo
  117. def parse_svg_transform(trstr):
  118. """
  119. Parses an SVG transform string into a list
  120. of transform names and their parameters.
  121. Possible transformations are:
  122. * Translate: translate(<tx> [<ty>]), which specifies
  123. a translation by tx and ty. If <ty> is not provided,
  124. it is assumed to be zero. Result is
  125. ['translate', tx, ty]
  126. * Scale: scale(<sx> [<sy>]), which specifies a scale operation
  127. by sx and sy. If <sy> is not provided, it is assumed to be
  128. equal to <sx>. Result is: ['scale', sx, sy]
  129. * Rotate: rotate(<rotate-angle> [<cx> <cy>]), which specifies
  130. a rotation by <rotate-angle> degrees about a given point.
  131. If optional parameters <cx> and <cy> are not supplied,
  132. the rotate is about the origin of the current user coordinate
  133. system. Result is: ['rotate', rotate-angle, cx, cy]
  134. * Skew: skewX(<skew-angle>), which specifies a skew
  135. transformation along the x-axis. skewY(<skew-angle>), which
  136. specifies a skew transformation along the y-axis.
  137. Result is ['skew', angle-x, angle-y]
  138. * Matrix: matrix(<a> <b> <c> <d> <e> <f>), which specifies a
  139. transformation in the form of a transformation matrix of six
  140. values. matrix(a,b,c,d,e,f) is equivalent to applying the
  141. transformation matrix [a b c d e f]. Result is
  142. ['matrix', a, b, c, d, e, f]
  143. :param trstr: SVG transform string.
  144. :type trstr: str
  145. :return: List of transforms.
  146. :rtype: list
  147. """
  148. trlist = []
  149. assert isinstance(trstr, str)
  150. trstr = trstr.strip(' ')
  151. num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing
  152. comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
  153. translate_re_str = r'translate\s*\(\s*(' + \
  154. num_re_str + r')' + \
  155. r'(?:' + comma_or_space_re_str + \
  156. r'(' + num_re_str + r'))?\s*\)'
  157. scale_re_str = r'scale\s*\(\s*(' + \
  158. num_re_str + r')' + \
  159. r'(?:' + comma_or_space_re_str + \
  160. r'(' + num_re_str + r'))?\s*\)'
  161. skew_re_str = r'skew([XY])\s*\(\s*(' + \
  162. num_re_str + r')\s*\)'
  163. rotate_re_str = r'rotate\s*\(\s*(' + \
  164. num_re_str + r')' + \
  165. r'(?:' + comma_or_space_re_str + \
  166. r'(' + num_re_str + r')' + \
  167. comma_or_space_re_str + \
  168. r'(' + num_re_str + r'))?\*\)'
  169. matrix_re_str = r'matrix\s*\(\s*' + \
  170. r'(' + num_re_str + r')' + comma_or_space_re_str + \
  171. r'(' + num_re_str + r')' + comma_or_space_re_str + \
  172. r'(' + num_re_str + r')' + comma_or_space_re_str + \
  173. r'(' + num_re_str + r')' + comma_or_space_re_str + \
  174. r'(' + num_re_str + r')' + comma_or_space_re_str + \
  175. r'(' + num_re_str + r')\s*\)'
  176. while len(trstr) > 0:
  177. match = re.search(r'^' + translate_re_str, trstr)
  178. if match:
  179. trlist.append([
  180. 'translate',
  181. float(match.group(1)),
  182. float(match.group(2)) if match.group else 0.0
  183. ])
  184. trstr = trstr[len(match.group(0)):].strip(' ')
  185. continue
  186. match = re.search(r'^' + scale_re_str, trstr)
  187. if match:
  188. trlist.append([
  189. 'translate',
  190. float(match.group(1)),
  191. float(match.group(2)) if match.group else float(match.group(1))
  192. ])
  193. trstr = trstr[len(match.group(0)):].strip(' ')
  194. continue
  195. match = re.search(r'^' + skew_re_str, trstr)
  196. if match:
  197. trlist.append([
  198. 'skew',
  199. float(match.group(2)) if match.group(1) == 'X' else 0.0,
  200. float(match.group(2)) if match.group(1) == 'Y' else 0.0
  201. ])
  202. trstr = trstr[len(match.group(0)):].strip(' ')
  203. continue
  204. match = re.search(r'^' + rotate_re_str, trstr)
  205. if match:
  206. trlist.append([
  207. 'rotate',
  208. float(match.group(1)),
  209. float(match.group(2)) if match.group(2) else 0.0,
  210. float(match.group(3)) if match.group(3) else 0.0
  211. ])
  212. trstr = trstr[len(match.group(0)):].strip(' ')
  213. continue
  214. match = re.search(r'^' + matrix_re_str, trstr)
  215. if match:
  216. trlist.append(['matrix'] + [float(x) for x in match.groups()])
  217. trstr = trstr[len(match.group(0)):].strip(' ')
  218. continue
  219. raise Exception("Don't know how to parse: %s" % trstr)
  220. return trlist
  221. if __name__ == "__main__":
  222. tree = ET.parse('tests/svg/drawing.svg')
  223. root = tree.getroot()
  224. ns = re.search(r'\{(.*)\}', root.tag).group(1)
  225. print ns
  226. for geo in getsvggeo(root):
  227. print geo