svgparse.py 11 KB

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