svgparse.py 12 KB

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