svgparse.py 14 KB

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