ParseSVG.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  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 (w/ rounded corners) #
  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. from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
  23. # from svg.path.path import Move
  24. # from svg.path.path import Close
  25. import svg.path
  26. from shapely.geometry import LineString, MultiLineString, Point
  27. from shapely.affinity import skew, affine_transform, rotate
  28. import numpy as np
  29. from appParsers.ParseFont import *
  30. log = logging.getLogger('base2')
  31. def svgparselength(lengthstr):
  32. """
  33. Parse an SVG length string into a float and a units
  34. string, if any.
  35. :param lengthstr: SVG length string.
  36. :return: Number and units pair.
  37. :rtype: tuple(float, str|None)
  38. """
  39. integer_re_str = r'[+-]?[0-9]+'
  40. number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
  41. r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
  42. length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?'
  43. if lengthstr:
  44. match = re.search(length_re_str, lengthstr)
  45. if match:
  46. return float(match.group(1)), match.group(2)
  47. else:
  48. return 0, 0
  49. return
  50. def svgparse_viewbox(root):
  51. val = root.get('viewBox')
  52. if val is None:
  53. return 1.0
  54. res = [float(x) for x in val.split()] or [float(x) for x in val.split(',')]
  55. w = svgparselength(root.get('width'))[0]
  56. # h = svgparselength(root.get('height'))[0]
  57. v_w = res[2]
  58. # v_h = res[3]
  59. return w / v_w
  60. def path2shapely(path, object_type, res=1.0, units='MM', factor=1.0):
  61. """
  62. Converts an svg.path.Path into a Shapely
  63. Polygon or LinearString.
  64. :param path: svg.path.Path instance
  65. :param object_type:
  66. :param res: Resolution (minimum step along path)
  67. :param units: FlatCAM units
  68. :type units: str
  69. :param factor: correction factor due of virtual units
  70. :type factor: float
  71. :return: Shapely geometry object
  72. :rtype : Polygon
  73. :rtype : LineString
  74. """
  75. points = []
  76. geometry = []
  77. rings = []
  78. closed = False
  79. for component in path:
  80. # Line
  81. if isinstance(component, Line):
  82. start = component.start
  83. x, y = start.real, start.imag
  84. if len(points) == 0 or points[-1] != (x, y):
  85. points.append((x, y))
  86. end = component.end
  87. points.append((factor * end.real, factor * end.imag))
  88. continue
  89. # Arc, CubicBezier or QuadraticBezier
  90. if isinstance(component, Arc) or \
  91. isinstance(component, CubicBezier) or \
  92. isinstance(component, QuadraticBezier):
  93. # How many points to use in the discrete representation.
  94. length = component.length(res / 10.0)
  95. # steps = int(length / res + 0.5)
  96. steps = int(length) * 2
  97. if units == 'IN':
  98. steps *= 25
  99. # solve error when step is below 1,
  100. # it may cause other problems, but LineString needs at least two points
  101. # later edit: made the minimum nr of steps to be 10; left it like that to see that steps can be 0
  102. if steps == 0 or steps < 10:
  103. steps = 10
  104. frac = 1.0 / steps
  105. # print length, steps, frac
  106. for i in range(steps):
  107. point = component.point(i * frac)
  108. x, y = point.real, point.imag
  109. if len(points) == 0 or points[-1] != (x, y):
  110. points.append((factor * x, factor * y))
  111. end = component.point(1.0)
  112. points.append((factor * end.real, factor * end.imag))
  113. continue
  114. # Move
  115. if isinstance(component, svg.path.Move):
  116. if not points:
  117. continue
  118. else:
  119. rings.append(points)
  120. if closed is False:
  121. points = []
  122. else:
  123. closed = False
  124. start = component.start
  125. x, y = start.real, start.imag
  126. points = [(factor * x, factor * y)]
  127. continue
  128. closed = False
  129. # Close
  130. if isinstance(component, svg.path.Close):
  131. if not points:
  132. continue
  133. else:
  134. rings.append(points)
  135. points = []
  136. closed = True
  137. continue
  138. log.warning("I don't know what this is: %s" % str(component))
  139. continue
  140. # if there are still points in points then add them to the last ring
  141. if points:
  142. rings.append(points)
  143. try:
  144. rings = MultiLineString(rings)
  145. except Exception as e:
  146. log.debug("ParseSVG.path2shapely() MString --> %s" % str(e))
  147. return None
  148. if len(rings) > 0:
  149. if len(rings) == 1 and not isinstance(rings, MultiLineString):
  150. # Polygons are closed and require more than 2 points
  151. if Point(rings[0][0]).almost_equals(Point(rings[0][-1])) and len(rings[0]) > 2:
  152. geo_element = Polygon(rings[0])
  153. else:
  154. geo_element = LineString(rings[0])
  155. else:
  156. try:
  157. geo_element = Polygon(rings[0], rings[1:])
  158. except Exception:
  159. coords = []
  160. for line in rings:
  161. coords.append(line.coords[0])
  162. coords.append(line.coords[1])
  163. try:
  164. geo_element = Polygon(coords)
  165. except Exception:
  166. geo_element = LineString(coords)
  167. geometry.append(geo_element)
  168. return geometry
  169. def svgrect2shapely(rect, n_points=32, factor=1.0):
  170. """
  171. Converts an SVG rect into Shapely geometry.
  172. :param rect: Rect Element
  173. :type rect: xml.etree.ElementTree.Element
  174. :param n_points: number of points to approximate rectangles corners when having rounded corners
  175. :type n_points: int
  176. :param factor: correction factor due of virtual units
  177. :type factor: float
  178. :return: shapely.geometry.polygon.LinearRing
  179. """
  180. w = svgparselength(rect.get('width'))[0]
  181. h = svgparselength(rect.get('height'))[0]
  182. x_obj = rect.get('x')
  183. if x_obj is not None:
  184. x = svgparselength(x_obj)[0] * factor
  185. else:
  186. x = 0
  187. y_obj = rect.get('y')
  188. if y_obj is not None:
  189. y = svgparselength(y_obj)[0] * factor
  190. else:
  191. y = 0
  192. rxstr = rect.get('rx')
  193. rxstr = rxstr * factor if rxstr else rxstr
  194. rystr = rect.get('ry')
  195. rystr = rystr * factor if rystr else rystr
  196. if rxstr is None and rystr is None: # Sharp corners
  197. pts = [
  198. (x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
  199. ]
  200. else: # Rounded corners
  201. rx = 0.0 if rxstr is None else svgparselength(rxstr)[0]
  202. ry = 0.0 if rystr is None else svgparselength(rystr)[0]
  203. n_points = int(n_points / 4 + 0.5)
  204. t = np.arange(n_points, dtype=float) / n_points / 4
  205. x_ = (x + w - rx) + rx * np.cos(2 * np.pi * (t + 0.75))
  206. y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.75))
  207. lower_right = [(x_[i], y_[i]) for i in range(n_points)]
  208. x_ = (x + w - rx) + rx * np.cos(2 * np.pi * t)
  209. y_ = (y + h - ry) + ry * np.sin(2 * np.pi * t)
  210. upper_right = [(x_[i], y_[i]) for i in range(n_points)]
  211. x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.25))
  212. y_ = (y + h - ry) + ry * np.sin(2 * np.pi * (t + 0.25))
  213. upper_left = [(x_[i], y_[i]) for i in range(n_points)]
  214. x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.5))
  215. y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.5))
  216. lower_left = [(x_[i], y_[i]) for i in range(n_points)]
  217. pts = [(x + rx, y), (x - rx + w, y)] + \
  218. lower_right + \
  219. [(x + w, y + ry), (x + w, y + h - ry)] + \
  220. upper_right + \
  221. [(x + w - rx, y + h), (x + rx, y + h)] + \
  222. upper_left + \
  223. [(x, y + h - ry), (x, y + ry)] + \
  224. lower_left
  225. return Polygon(pts).buffer(0)
  226. # return LinearRing(pts)
  227. def svgcircle2shapely(circle, n_points=64, factor=1.0):
  228. """
  229. Converts an SVG circle into Shapely geometry.
  230. :param circle: Circle Element
  231. :type circle: xml.etree.ElementTree.Element
  232. :param n_points: circle resolution; nr of points to b e used to approximate a circle
  233. :type n_points: int
  234. :param factor:
  235. :type factor: float
  236. :return: Shapely representation of the circle.
  237. :rtype: shapely.geometry.polygon.LinearRing
  238. """
  239. # cx = float(circle.get('cx'))
  240. # cy = float(circle.get('cy'))
  241. # r = float(circle.get('r'))
  242. cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet
  243. cx = cx * factor if cx else cx
  244. cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet
  245. cy = cy * factor if cy else cy
  246. r = svgparselength(circle.get('r'))[0] # TODO: No units support yet
  247. r = r * factor if r else r
  248. return Point(cx, cy).buffer(r, resolution=n_points)
  249. def svgellipse2shapely(ellipse, n_points=64, factor=1.0):
  250. """
  251. Converts an SVG ellipse into Shapely geometry
  252. :param ellipse: Ellipse Element
  253. :type ellipse: xml.etree.ElementTree.Element
  254. :param n_points: Number of discrete points in output.
  255. :type n_points: int
  256. :param factor:
  257. :type factor: float
  258. :return: Shapely representation of the ellipse.
  259. :rtype: shapely.geometry.polygon.LinearRing
  260. """
  261. cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet
  262. cx = cx * factor if cx else cx
  263. cy = svgparselength(ellipse.get('cy'))[0] # TODO: No units support yet
  264. cy = cy * factor if cy else cy
  265. rx = svgparselength(ellipse.get('rx'))[0] # TODO: No units support yet
  266. rx = rx * factor if rx else rx
  267. ry = svgparselength(ellipse.get('ry'))[0] # TODO: No units support yet
  268. ry = ry * factor if ry else ry
  269. t = np.arange(n_points, dtype=float) / n_points
  270. x = cx + rx * np.cos(2 * np.pi * t)
  271. y = cy + ry * np.sin(2 * np.pi * t)
  272. pts = [(x[i], y[i]) for i in range(n_points)]
  273. return Polygon(pts).buffer(0)
  274. # return LinearRing(pts)
  275. def svgline2shapely(line, factor=1.0):
  276. """
  277. :param line: Line element
  278. :type line: xml.etree.ElementTree.Element
  279. :param factor: correction factor due of virtual units
  280. :type factor: float
  281. :return: Shapely representation on the line.
  282. :rtype: shapely.geometry.polygon.LineString
  283. """
  284. x1 = svgparselength(line.get('x1'))[0] * factor
  285. y1 = svgparselength(line.get('y1'))[0] * factor
  286. x2 = svgparselength(line.get('x2'))[0] * factor
  287. y2 = svgparselength(line.get('y2'))[0] * factor
  288. return LineString([(x1, y1), (x2, y2)])
  289. def svgpolyline2shapely(polyline, factor=1.0):
  290. """
  291. :param polyline: Polyline element
  292. :type polyline: xml.etree.ElementTree.Element
  293. :param factor: correction factor due of virtual units
  294. :type factor: float
  295. :return: Shapely representation of the PolyLine
  296. :rtype: shapely.geometry.polygon.LineString
  297. """
  298. ptliststr = polyline.get('points')
  299. points = parse_svg_point_list(ptliststr, factor)
  300. return LineString(points)
  301. def svgpolygon2shapely(polygon, n_points=64, factor=1.0):
  302. """
  303. Convert a SVG polygon to a Shapely Polygon.
  304. :param polygon:
  305. :type polygon:
  306. :param n_points: circle resolution; nr of points to b e used to approximate a circle
  307. :type n_points: int
  308. :param factor: correction factor due of virtual units
  309. :type factor: float
  310. :return: Shapely Polygon
  311. """
  312. ptliststr = polygon.get('points')
  313. points = parse_svg_point_list(ptliststr, factor)
  314. return Polygon(points).buffer(0, resolution=n_points)
  315. # return LinearRing(points)
  316. def getsvggeo(node, object_type, root=None, units='MM', res=64, factor=1.0):
  317. """
  318. Extracts and flattens all geometry from an SVG node
  319. into a list of Shapely geometry.
  320. :param node: xml.etree.ElementTree.Element
  321. :param object_type:
  322. :param root:
  323. :param units: FlatCAM units
  324. :param res: resolution to be used for circles buffering
  325. :param factor: correction factor due of virtual units
  326. :type factor: float
  327. :return: List of Shapely geometry
  328. :rtype: list
  329. """
  330. if root is None:
  331. root = node
  332. kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
  333. geo = []
  334. # Recurse
  335. if len(node) > 0:
  336. for child in node:
  337. subgeo = getsvggeo(child, object_type, root=root, units=units, res=res, factor=factor)
  338. if subgeo is not None:
  339. geo += subgeo
  340. # Parse
  341. elif kind == 'path':
  342. log.debug("***PATH***")
  343. P = parse_path(node.get('d'))
  344. P = path2shapely(P, object_type, units=units, factor=factor)
  345. # for path, the resulting geometry is already a list so no need to create a new one
  346. geo = P
  347. elif kind == 'rect':
  348. log.debug("***RECT***")
  349. R = svgrect2shapely(node, n_points=res, factor=factor)
  350. geo = [R]
  351. elif kind == 'circle':
  352. log.debug("***CIRCLE***")
  353. C = svgcircle2shapely(node, n_points=res, factor=factor)
  354. geo = [C]
  355. elif kind == 'ellipse':
  356. log.debug("***ELLIPSE***")
  357. E = svgellipse2shapely(node, n_points=res, factor=factor)
  358. geo = [E]
  359. elif kind == 'polygon':
  360. log.debug("***POLYGON***")
  361. poly = svgpolygon2shapely(node, n_points=res, factor=factor)
  362. geo = [poly]
  363. elif kind == 'line':
  364. log.debug("***LINE***")
  365. line = svgline2shapely(node, factor=factor)
  366. geo = [line]
  367. elif kind == 'polyline':
  368. log.debug("***POLYLINE***")
  369. pline = svgpolyline2shapely(node, factor=factor)
  370. geo = [pline]
  371. elif kind == 'use':
  372. log.debug('***USE***')
  373. # href= is the preferred name for this[1], but inkscape still generates xlink:href=.
  374. # [1] https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use#Attributes
  375. href = node.attrib['href'] if 'href' in node.attrib else node.attrib['{http://www.w3.org/1999/xlink}href']
  376. ref = root.find(".//*[@id='%s']" % href.replace('#', ''))
  377. if ref is not None:
  378. geo = getsvggeo(ref, object_type, root=root, units=units, res=res, factor=factor)
  379. else:
  380. log.warning("Unknown kind: " + kind)
  381. geo = None
  382. # ignore transformation for unknown kind
  383. if geo is not None:
  384. # Transformations
  385. if 'transform' in node.attrib:
  386. trstr = node.get('transform')
  387. trlist = parse_svg_transform(trstr)
  388. # log.debug(trlist)
  389. # Transformations are applied in reverse order
  390. for tr in trlist[::-1]:
  391. if tr[0] == 'translate':
  392. geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
  393. elif tr[0] == 'scale':
  394. geo = [scale(geoi, tr[1], tr[2], origin=(0, 0))
  395. for geoi in geo]
  396. elif tr[0] == 'rotate':
  397. geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
  398. for geoi in geo]
  399. elif tr[0] == 'skew':
  400. geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
  401. for geoi in geo]
  402. elif tr[0] == 'matrix':
  403. geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
  404. else:
  405. raise Exception('Unknown transformation: %s', tr)
  406. return geo
  407. def getsvgtext(node, object_type, units='MM'):
  408. """
  409. Extracts and flattens all geometry from an SVG node
  410. into a list of Shapely geometry.
  411. :param node: xml.etree.ElementTree.Element
  412. :param object_type:
  413. :param units: FlatCAM units
  414. :return: List of Shapely geometry
  415. :rtype: list
  416. """
  417. kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
  418. geo = []
  419. # Recurse
  420. if len(node) > 0:
  421. for child in node:
  422. subgeo = getsvgtext(child, object_type, units=units)
  423. if subgeo is not None:
  424. geo += subgeo
  425. # Parse
  426. elif kind == 'tspan':
  427. current_attrib = node.attrib
  428. txt = node.text
  429. style_dict = {}
  430. parrent_attrib = node.getparent().attrib
  431. style = parrent_attrib['style']
  432. try:
  433. style_list = style.split(';')
  434. for css in style_list:
  435. style_dict[css.rpartition(':')[0]] = css.rpartition(':')[-1]
  436. pos_x = float(current_attrib['x'])
  437. pos_y = float(current_attrib['y'])
  438. # should have used the instance from FlatCAMApp.App but how? without reworking everything ...
  439. pf = ParseFont()
  440. pf.get_fonts_by_types()
  441. font_name = style_dict['font-family'].replace("'", '')
  442. if style_dict['font-style'] == 'italic' and style_dict['font-weight'] == 'bold':
  443. font_type = 'bi'
  444. elif style_dict['font-weight'] == 'bold':
  445. font_type = 'bold'
  446. elif style_dict['font-style'] == 'italic':
  447. font_type = 'italic'
  448. else:
  449. font_type = 'regular'
  450. # value of 2.2 should have been 2.83 (conversion value from pixels to points)
  451. # but the dimensions from Inkscape did not corelate with the ones after importing in FlatCAM
  452. # so I adjusted this
  453. font_size = svgparselength(style_dict['font-size'])[0] * 2.2
  454. geo = [pf.font_to_geometry(txt,
  455. font_name=font_name,
  456. font_size=font_size,
  457. font_type=font_type,
  458. units=units,
  459. coordx=pos_x,
  460. coordy=pos_y)
  461. ]
  462. geo = [(scale(g, 1.0, -1.0)) for g in geo]
  463. except Exception as e:
  464. log.debug(str(e))
  465. else:
  466. geo = None
  467. # ignore transformation for unknown kind
  468. if geo is not None:
  469. # Transformations
  470. if 'transform' in node.attrib:
  471. trstr = node.get('transform')
  472. trlist = parse_svg_transform(trstr)
  473. # log.debug(trlist)
  474. # Transformations are applied in reverse order
  475. for tr in trlist[::-1]:
  476. if tr[0] == 'translate':
  477. geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
  478. elif tr[0] == 'scale':
  479. geo = [scale(geoi, tr[1], tr[2], origin=(0, 0))
  480. for geoi in geo]
  481. elif tr[0] == 'rotate':
  482. geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
  483. for geoi in geo]
  484. elif tr[0] == 'skew':
  485. geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
  486. for geoi in geo]
  487. elif tr[0] == 'matrix':
  488. geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
  489. else:
  490. raise Exception('Unknown transformation: %s', tr)
  491. return geo
  492. def parse_svg_point_list(ptliststr, factor):
  493. """
  494. Returns a list of coordinate pairs extracted from the "points"
  495. attribute in SVG polygons and polyline's.
  496. :param ptliststr: "points" attribute string in polygon or polyline.
  497. :param factor: correction factor due of virtual units
  498. :type factor: float
  499. :return: List of tuples with coordinates.
  500. """
  501. pairs = []
  502. last = None
  503. pos = 0
  504. i = 0
  505. for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr.strip(' ')):
  506. val = float(ptliststr[pos:match.start()])
  507. if i % 2 == 1:
  508. pairs.append((factor * last, factor * val))
  509. else:
  510. last = val * factor
  511. pos = match.end()
  512. i += 1
  513. # Check for last element
  514. val = float(ptliststr[pos:])
  515. if i % 2 == 1:
  516. pairs.append((factor * last, factor * val))
  517. else:
  518. log.warning("Incomplete coordinates.")
  519. return pairs
  520. def parse_svg_transform(trstr):
  521. """
  522. Parses an SVG transform string into a list
  523. of transform names and their parameters.
  524. Possible transformations are:
  525. * Translate: translate(<tx> [<ty>]), which specifies
  526. a translation by tx and ty. If <ty> is not provided,
  527. it is assumed to be zero. Result is
  528. ['translate', tx, ty]
  529. * Scale: scale(<sx> [<sy>]), which specifies a scale operation
  530. by sx and sy. If <sy> is not provided, it is assumed to be
  531. equal to <sx>. Result is: ['scale', sx, sy]
  532. * Rotate: rotate(<rotate-angle> [<cx> <cy>]), which specifies
  533. a rotation by <rotate-angle> degrees about a given point.
  534. If optional parameters <cx> and <cy> are not supplied,
  535. the rotate is about the origin of the current user coordinate
  536. system. Result is: ['rotate', rotate-angle, cx, cy]
  537. * Skew: skewX(<skew-angle>), which specifies a skew
  538. transformation along the x-axis. skewY(<skew-angle>), which
  539. specifies a skew transformation along the y-axis.
  540. Result is ['skew', angle-x, angle-y]
  541. * Matrix: matrix(<a> <b> <c> <d> <e> <f>), which specifies a
  542. transformation in the form of a transformation matrix of six
  543. values. matrix(a,b,c,d,e,f) is equivalent to applying the
  544. transformation matrix [a b c d e f]. Result is
  545. ['matrix', a, b, c, d, e, f]
  546. Note: All parameters to the transformations are "numbers",
  547. i.e. no units present.
  548. :param trstr: SVG transform string.
  549. :type trstr: str
  550. :return: List of transforms.
  551. :rtype: list
  552. """
  553. trlist = []
  554. assert isinstance(trstr, str)
  555. trstr = trstr.strip(' ')
  556. integer_re_str = r'[+-]?[0-9]+'
  557. number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
  558. r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
  559. # num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing
  560. comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
  561. translate_re_str = r'translate\s*\(\s*(' + \
  562. number_re_str + r')(?:' + \
  563. comma_or_space_re_str + \
  564. r'(' + number_re_str + r'))?\s*\)'
  565. scale_re_str = r'scale\s*\(\s*(' + \
  566. number_re_str + r')' + \
  567. r'(?:' + comma_or_space_re_str + \
  568. r'(' + number_re_str + r'))?\s*\)'
  569. skew_re_str = r'skew([XY])\s*\(\s*(' + \
  570. number_re_str + r')\s*\)'
  571. rotate_re_str = r'rotate\s*\(\s*(' + \
  572. number_re_str + r')' + \
  573. r'(?:' + comma_or_space_re_str + \
  574. r'(' + number_re_str + r')' + \
  575. comma_or_space_re_str + \
  576. r'(' + number_re_str + r'))?\s*\)'
  577. matrix_re_str = r'matrix\s*\(\s*' + \
  578. r'(' + number_re_str + r')' + comma_or_space_re_str + \
  579. r'(' + number_re_str + r')' + comma_or_space_re_str + \
  580. r'(' + number_re_str + r')' + comma_or_space_re_str + \
  581. r'(' + number_re_str + r')' + comma_or_space_re_str + \
  582. r'(' + number_re_str + r')' + comma_or_space_re_str + \
  583. r'(' + number_re_str + r')\s*\)'
  584. while len(trstr) > 0:
  585. match = re.search(r'^' + translate_re_str, trstr)
  586. if match:
  587. trlist.append([
  588. 'translate',
  589. float(match.group(1)),
  590. float(match.group(2)) if (match.group(2) is not None) else 0.0
  591. ])
  592. trstr = trstr[len(match.group(0)):].strip(' ')
  593. continue
  594. match = re.search(r'^' + scale_re_str, trstr)
  595. if match:
  596. trlist.append([
  597. 'scale',
  598. float(match.group(1)),
  599. float(match.group(2)) if (match.group(2) is not None) else float(match.group(1))
  600. ])
  601. trstr = trstr[len(match.group(0)):].strip(' ')
  602. continue
  603. match = re.search(r'^' + skew_re_str, trstr)
  604. if match:
  605. trlist.append([
  606. 'skew',
  607. float(match.group(2)) if match.group(1) == 'X' else 0.0,
  608. float(match.group(2)) if match.group(1) == 'Y' else 0.0
  609. ])
  610. trstr = trstr[len(match.group(0)):].strip(' ')
  611. continue
  612. match = re.search(r'^' + rotate_re_str, trstr)
  613. if match:
  614. trlist.append([
  615. 'rotate',
  616. float(match.group(1)),
  617. float(match.group(2)) if match.group(2) else 0.0,
  618. float(match.group(3)) if match.group(3) else 0.0
  619. ])
  620. trstr = trstr[len(match.group(0)):].strip(' ')
  621. continue
  622. match = re.search(r'^' + matrix_re_str, trstr)
  623. if match:
  624. trlist.append(['matrix'] + [float(x) for x in match.groups()])
  625. trstr = trstr[len(match.group(0)):].strip(' ')
  626. continue
  627. # raise Exception("Don't know how to parse: %s" % trstr)
  628. log.error("[ERROR] Don't know how to parse: %s" % trstr)
  629. return trlist
  630. # if __name__ == "__main__":
  631. # tree = ET.parse('tests/svg/drawing.svg')
  632. # root = tree.getroot()
  633. # ns = re.search(r'\{(.*)\}', root.tag).group(1)
  634. # print(ns)
  635. # for geo in getsvggeo(root):
  636. # print(geo)