svgparse.py 10 KB

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