chiark / gitweb /
Support multple modification in a single transform tag. Add rotate(r,x,y) transform.
[cura.git] / Cura / util / svg.py
1 from __future__ import absolute_import
2 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
3
4 import math
5 import re
6 import sys
7 import numpy
8 from xml.etree import ElementTree
9
10 def applyTransformString(matrix, transform):
11         if len(re.findall('\)', transform)) > 1:
12                 print transform
13         while transform != '':
14                 if transform[0] == ',':
15                         transform = transform[1:].strip()
16                 s = transform.find('(')
17                 e = transform.find(')')
18                 if s < 0 or e < 0:
19                         print 'Unknown transform: %s' % (transform)
20                         return matrix
21                 tag = transform[:s]
22                 data = map(float, re.split('[ \t,]+', transform[s+1:e].strip()))
23                 if tag == 'matrix' and len(data) == 6:
24                         matrix = numpy.matrix([[data[0],data[1],0],[data[2],data[3],0],[data[4],data[5],1]], numpy.float64) * matrix
25                 elif tag == 'translate' and len(data) == 1:
26                         matrix = numpy.matrix([[1,0,data[0]],[0,1,0],[0,0,1]], numpy.float64) * matrix
27                 elif tag == 'translate' and len(data) == 2:
28                         matrix = numpy.matrix([[1,0,0],[0,1,0],[data[0],data[1],1]], numpy.float64) * matrix
29                 elif tag == 'scale' and len(data) == 1:
30                         matrix = numpy.matrix([[data[0],0,0],[0,data[0],0],[0,0,1]], numpy.float64) * matrix
31                 elif tag == 'scale' and len(data) == 2:
32                         matrix = numpy.matrix([[data[0],0,0],[0,data[1],0],[0,0,1]], numpy.float64) * matrix
33                 elif tag == 'rotate' and len(data) == 1:
34                         r = math.radians(data[0])
35                         matrix = numpy.matrix([[math.cos(r),math.sin(r),0],[-math.sin(r),math.cos(r),0],[0,0,1]], numpy.float64) * matrix
36                 elif tag == 'rotate' and len(data) == 3:
37                         matrix = numpy.matrix([[1,0,0],[0,1,0],[data[1],data[2],1]], numpy.float64) * matrix
38                         r = math.radians(data[0])
39                         matrix = numpy.matrix([[math.cos(r),math.sin(r),0],[-math.sin(r),math.cos(r),0],[0,0,1]], numpy.float64) * matrix
40                         matrix = numpy.matrix([[1,0,0],[0,1,0],[-data[1],-data[2],1]], numpy.float64) * matrix
41                 elif tag == 'skewX' and len(data) == 1:
42                         matrix = numpy.matrix([[1,0,0],[math.tan(data[0]),1,0],[0,0,1]], numpy.float64) * matrix
43                 elif tag == 'skewY' and len(data) == 1:
44                         matrix = numpy.matrix([[1,math.tan(data[0]),0],[0,1,0],[0,0,1]], numpy.float64) * matrix
45                 else:
46                         print 'Unknown transform: %s' % (transform)
47                         return matrix
48                 transform = transform[e+1:].strip()
49         return matrix
50
51 def toFloat(f):
52         f = re.search('^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?', f).group(0)
53         return float(f)
54
55 class Path(object):
56         LINE = 0
57         ARC = 1
58         CURVE = 2
59
60         def __init__(self, x, y, matrix):
61                 self._matrix = matrix
62                 self._relMatrix = numpy.matrix([[matrix[0,0],matrix[0,1]], [matrix[1,0],matrix[1,1]]])
63                 self._startPoint = complex(x, y)
64                 self._points = []
65
66         def addLineTo(self, x, y):
67                 self._points.append({'type': Path.LINE, 'p': complex(x, y)})
68
69         def addArcTo(self, x, y, rot, rx, ry, large, sweep):
70                 self._points.append({
71                         'type': Path.ARC,
72                         'p': complex(x, y),
73                         'rot': rot,
74                         'radius': complex(rx, ry),
75                         'large': large,
76                         'sweep': sweep
77                 })
78
79         def addCurveTo(self, x, y, cp1x, cp1y, cp2x, cp2y):
80                 self._points.append({
81                         'type': Path.CURVE,
82                         'p': complex(x, y),
83                         'cp1': complex(cp1x, cp1y),
84                         'cp2': complex(cp2x, cp2y)
85                 })
86
87         def closePath(self):
88                 self._points.append({'type': Path.LINE, 'p': self._startPoint})
89
90         def getPoints(self, accuracy = 1):
91                 pointList = [self._m(self._startPoint)]
92                 p1 = self._startPoint
93                 for p in self._points:
94                         if p['type'] == Path.LINE:
95                                 p1 = p['p']
96                                 pointList.append(self._m(p1))
97                         elif p['type'] == Path.ARC:
98                                 p2 = p['p']
99                                 rot = math.radians(p['rot'])
100                                 r = p['radius']
101
102                                 #http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
103                                 diff = (p1 - p2) / 2
104                                 p1alt = diff #TODO: apply rot
105                                 p2alt = -diff #TODO: apply rot
106                                 rx2 = r.real*r.real
107                                 ry2 = r.imag*r.imag
108                                 x1alt2 = p1alt.real*p1alt.real
109                                 y1alt2 = p1alt.imag*p1alt.imag
110
111                                 f = x1alt2 / rx2 + y1alt2 / ry2
112                                 if f >= 1.0:
113                                         r *= math.sqrt(f+0.000001)
114                                         rx2 = r.real*r.real
115                                         ry2 = r.imag*r.imag
116
117                                 f = math.sqrt((rx2*ry2 - rx2*y1alt2 - ry2*x1alt2) / (rx2*y1alt2+ry2*x1alt2))
118                                 if p['large'] == p['sweep']:
119                                         f = -f
120                                 cAlt = f * complex(r.real*p1alt.imag/r.imag, -r.imag*p1alt.real/r.real)
121
122                                 c = cAlt + (p1 + p2) / 2 #TODO: apply rot
123
124                                 a1 = math.atan2((p1alt.imag - cAlt.imag) / r.imag, (p1alt.real - cAlt.real) / r.real)
125                                 a2 = math.atan2((p2alt.imag - cAlt.imag) / r.imag, (p2alt.real - cAlt.real) / r.real)
126
127                                 large = abs(a2 - a1) > math.pi
128                                 if large != p['large']:
129                                         if a1 < a2:
130                                                 a1 += math.pi * 2
131                                         else:
132                                                 a2 += math.pi * 2
133
134                                 pCenter = self._m(c + complex(math.cos(a1 + 0.5*(a2-a1)) * r.real, math.sin(a1 + 0.5*(a2-a1)) * r.imag))
135                                 dist = abs(pCenter - self._m(p1)) + abs(pCenter - self._m(p2))
136                                 segments = int(dist / accuracy) + 1
137                                 for n in xrange(1, segments):
138                                         pointList.append(self._m(c + complex(math.cos(a1 + n*(a2-a1)/segments) * r.real, math.sin(a1 + n*(a2-a1)/segments) * r.imag)))
139
140                                 pointList.append(self._m(p2))
141                                 p1 = p2
142                         elif p['type'] == Path.CURVE:
143                                 p1_ = self._m(p1)
144                                 p2 = self._m(p['p'])
145                                 cp1 = self._m(p['cp1'])
146                                 cp2 = self._m(p['cp2'])
147
148                                 pCenter = p1_*0.5*0.5*0.5 + cp1*3.0*0.5*0.5*0.5 + cp2*3.0*0.5*0.5*0.5 + p2*0.5*0.5*0.5
149                                 dist = abs(pCenter - p1_) + abs(pCenter - p2)
150                                 segments = int(dist / accuracy) + 1
151                                 for n in xrange(1, segments):
152                                         f = n / float(segments)
153                                         g = 1.0-f
154                                         point = p1_*g*g*g + cp1*3.0*g*g*f + cp2*3.0*g*f*f + p2*f*f*f
155                                         pointList.append(point)
156
157                                 pointList.append(p2)
158                                 p1 = p['p']
159
160                 return pointList
161
162         def getSVGPath(self):
163                 p0 = self._m(self._startPoint)
164                 ret = 'M %f %f ' % (p0.real, p0.imag)
165                 for p in self._points:
166                         if p['type'] == Path.LINE:
167                                 p0 = self._m(p['p'])
168                                 ret += 'L %f %f' % (p0.real, p0.imag)
169                         elif p['type'] == Path.ARC:
170                                 p0 = self._m(p['p'])
171                                 radius = p['radius']
172                                 ret += 'A %f %f 0 %d %d %f %f' % (radius.real, radius.imag, 1 if p['large'] else 0, 1 if p['sweep'] else 0, p0.real, p0.imag)
173                         elif p['type'] == Path.CURVE:
174                                 p0 = self._m(p['p'])
175                                 cp1 = self._m(p['cp1'])
176                                 cp2 = self._m(p['cp2'])
177                                 ret += 'C %f %f %f %f %f %f' % (cp1.real, cp1.imag, cp2.real, cp2.imag, p0.real, p0.imag)
178
179                 return ret
180
181         def _m(self, p):
182                 tmp = numpy.matrix([p.real, p.imag, 1], numpy.float64) * self._matrix
183                 return complex(tmp[0,0], tmp[0,1])
184         def _r(self, p):
185                 tmp = numpy.matrix([p.real, p.imag], numpy.float64) * self._relMatrix
186                 return complex(tmp[0,0], tmp[0,1])
187
188 class SVG(object):
189         def __init__(self, filename):
190                 self.tagProcess = {}
191                 self.tagProcess['rect'] = self._processRectTag
192                 self.tagProcess['line'] = self._processLineTag
193                 self.tagProcess['polyline'] = self._processPolylineTag
194                 self.tagProcess['polygon'] = self._processPolygonTag
195                 self.tagProcess['elipse'] = self._processPolygonTag
196                 self.tagProcess['circle'] = self._processCircleTag
197                 self.tagProcess['ellipse'] = self._processEllipseTag
198                 self.tagProcess['path'] = self._processPathTag
199                 self.tagProcess['use'] = self._processUseTag
200                 self.tagProcess['g'] = self._processGTag
201                 self.tagProcess['a'] = self._processGTag
202                 self.tagProcess['svg'] = self._processGTag
203                 self.tagProcess['text'] = None #No text implementation yet
204                 self.tagProcess['image'] = None
205                 self.tagProcess['metadata'] = None
206                 self.tagProcess['defs'] = None
207                 self.tagProcess['style'] = None
208                 self.tagProcess['marker'] = None
209                 self.tagProcess['desc'] = None
210                 self.tagProcess['filter'] = None
211                 self.tagProcess['linearGradient'] = None
212                 self.tagProcess['radialGradient'] = None
213                 self.tagProcess['pattern'] = None
214                 self.tagProcess['title'] = None
215                 self.tagProcess['animate'] = None
216                 self.tagProcess['animateColor'] = None
217                 self.tagProcess['animateTransform'] = None
218                 self.tagProcess['set'] = None
219                 self.tagProcess['script'] = None
220
221                 #From Inkscape
222                 self.tagProcess['namedview'] = None
223                 #From w3c testsuite
224                 self.tagProcess['SVGTestCase'] = None
225
226                 self.paths = []
227                 f = open(filename, "r")
228                 self._xml = ElementTree.parse(f)
229                 self._recursiveCount = 0
230                 self._processGTag(self._xml.getroot(), numpy.matrix(numpy.identity(3, numpy.float64)))
231                 self._xml = None
232                 f.close()
233
234         def _processGTag(self, tag, baseMatrix):
235                 for e in tag:
236                         if e.get('transform') is None:
237                                 matrix = baseMatrix
238                         else:
239                                 matrix = applyTransformString(baseMatrix, e.get('transform'))
240                         tagName = e.tag[e.tag.find('}')+1:]
241                         if not tagName in self.tagProcess:
242                                 print 'unknown tag: %s' % (tagName)
243                         elif self.tagProcess[tagName] is not None:
244                                 self.tagProcess[tagName](e, matrix)
245
246         def _processUseTag(self, tag, baseMatrix):
247                 if self._recursiveCount > 16:
248                         return
249                 self._recursiveCount += 1
250                 id = tag.get('{http://www.w3.org/1999/xlink}href')
251                 if id[0] == '#':
252                         for e in self._xml.findall(".//*[@id='%s']" % (id[1:])):
253                                 if e.get('transform') is None:
254                                         matrix = baseMatrix
255                                 else:
256                                         matrix = applyTransformString(baseMatrix, e.get('transform'))
257                                 tagName = e.tag[e.tag.find('}')+1:]
258                                 if not tagName in self.tagProcess:
259                                         print 'unknown tag: %s' % (tagName)
260                                 elif self.tagProcess[tagName] is not None:
261                                         self.tagProcess[tagName](e, matrix)
262                 self._recursiveCount -= 1
263
264         def _processLineTag(self, tag, matrix):
265                 x1 = toFloat(tag.get('x1', '0'))
266                 y1 = toFloat(tag.get('y1', '0'))
267                 x2 = toFloat(tag.get('x2', '0'))
268                 y2 = toFloat(tag.get('y2', '0'))
269                 p = Path(x1, y1, matrix)
270                 p.addLineTo(x2, y2)
271                 self.paths.append(p)
272
273         def _processPolylineTag(self, tag, matrix):
274                 values = map(toFloat, re.split('[, \t]+', tag.get('points', '').strip()))
275                 p = Path(values[0], values[1], matrix)
276                 for n in xrange(2, len(values)-1, 2):
277                         p.addLineTo(values[n], values[n+1])
278                 self.paths.append(p)
279
280         def _processPolygonTag(self, tag, matrix):
281                 values = map(toFloat, re.split('[, \t]+', tag.get('points', '').strip()))
282                 p = Path(values[0], values[1], matrix)
283                 for n in xrange(2, len(values)-1, 2):
284                         p.addLineTo(values[n], values[n+1])
285                 p.closePath()
286                 self.paths.append(p)
287
288         def _processCircleTag(self, tag, matrix):
289                 cx = toFloat(tag.get('cx', '0'))
290                 cy = toFloat(tag.get('cy', '0'))
291                 r = toFloat(tag.get('r', '0'))
292                 p = Path(cx-r, cy, matrix)
293                 p.addArcTo(cx+r, cy, 0, r, r, False, False)
294                 p.addArcTo(cx-r, cy, 0, r, r, False, False)
295                 self.paths.append(p)
296
297         def _processEllipseTag(self, tag, matrix):
298                 cx = toFloat(tag.get('cx', '0'))
299                 cy = toFloat(tag.get('cy', '0'))
300                 rx = toFloat(tag.get('rx', '0'))
301                 ry = toFloat(tag.get('rx', '0'))
302                 p = Path(cx-rx, cy, matrix)
303                 p.addArcTo(cx+rx, cy, 0, rx, ry, False, False)
304                 p.addArcTo(cx-rx, cy, 0, rx, ry, False, False)
305                 self.paths.append(p)
306
307         def _processRectTag(self, tag, matrix):
308                 x = toFloat(tag.get('x', '0'))
309                 y = toFloat(tag.get('y', '0'))
310                 width = toFloat(tag.get('width', '0'))
311                 height = toFloat(tag.get('height', '0'))
312                 if width <= 0 or height <= 0:
313                         return
314                 rx = tag.get('rx')
315                 ry = tag.get('ry')
316                 if rx is not None or ry is not None:
317                         if ry is None:
318                                 ry = rx
319                         if rx is None:
320                                 rx = ry
321                         rx = float(rx)
322                         ry = float(ry)
323                         if rx > width / 2:
324                                 rx = width / 2
325                         if ry > height / 2:
326                                 ry = height / 2
327                 else:
328                         rx = 0.0
329                         ry = 0.0
330
331                 if rx > 0 and ry > 0:
332                         p = Path(x+rx, y, matrix)
333                         p.addLineTo(x+width-rx, y)
334                         p.addArcTo(x+width,y+ry, 0, rx, ry, False, True)
335                         p.addLineTo(x+width, y+height-ry)
336                         p.addArcTo(x+width-rx,y+height, 0, rx, ry, False, True)
337                         p.addLineTo(x+rx, y+height)
338                         p.addArcTo(x,y+height-ry, 0, rx, ry, False, True)
339                         p.addLineTo(x, y+ry)
340                         p.addArcTo(x+rx,y, 0, rx, ry, False, True)
341                         self.paths.append(p)
342                 else:
343                         p = Path(x, y, matrix)
344                         p.addLineTo(x,y+height)
345                         p.addLineTo(x+width,y+height)
346                         p.addLineTo(x+width,y)
347                         p.closePath()
348                         self.paths.append(p)
349
350         def _processPathTag(self, tag, matrix):
351                 pathString = tag.get('d', '').replace(',', ' ')
352                 x = 0
353                 y = 0
354                 c2x = 0
355                 c2y = 0
356                 path = None
357                 for command in re.findall('[a-df-zA-DF-Z][^a-df-zA-DF-Z]*', pathString):
358                         params = re.split(' +', command[1:].strip())
359                         if len(params) > 0 and params[0] == '':
360                                 params = params[1:]
361                         if len(params) > 0 and params[-1] == '':
362                                 params = params[:-1]
363                         params = map(toFloat, params)
364                         command = command[0]
365
366                         if command == 'm':
367                                 x += params[0]
368                                 y += params[1]
369                                 path = Path(x, y, matrix)
370                                 self.paths.append(path)
371                                 params = params[2:]
372                                 while len(params) > 1:
373                                         x += params[0]
374                                         y += params[1]
375                                         params = params[2:]
376                                         path.addLineTo(x, y)
377                                 c2x, c2y = x, y
378                         elif command == 'M':
379                                 x = params[0]
380                                 y = params[1]
381                                 path = Path(x, y, matrix)
382                                 self.paths.append(path)
383                                 params = params[2:]
384                                 while len(params) > 1:
385                                         x = params[0]
386                                         y = params[1]
387                                         params = params[2:]
388                                         path.addLineTo(x, y)
389                                 c2x, c2y = x, y
390                         elif command == 'l':
391                                 while len(params) > 1:
392                                         x += params[0]
393                                         y += params[1]
394                                         params = params[2:]
395                                         path.addLineTo(x, y)
396                                 c2x, c2y = x, y
397                         elif command == 'L':
398                                 while len(params) > 1:
399                                         x = params[0]
400                                         y = params[1]
401                                         params = params[2:]
402                                         path.addLineTo(x, y)
403                                 c2x, c2y = x, y
404                         elif command == 'h':
405                                 x += params[0]
406                                 path.addLineTo(x, y)
407                                 c2x, c2y = x, y
408                         elif command == 'H':
409                                 x = params[0]
410                                 path.addLineTo(x, y)
411                                 c2x, c2y = x, y
412                         elif command == 'v':
413                                 y += params[0]
414                                 path.addLineTo(x, y)
415                                 c2x, c2y = x, y
416                         elif command == 'V':
417                                 y = params[0]
418                                 path.addLineTo(x, y)
419                                 c2x, c2y = x, y
420                         elif command == 'a':
421                                 while len(params) > 6:
422                                         x += params[5]
423                                         y += params[6]
424                                         path.addArcTo(x, y, params[2], params[0], params[1], params[3] > 0, params[4] > 0)
425                                         params = params[7:]
426                                 c2x, c2y = x, y
427                         elif command == 'A':
428                                 while len(params) > 6:
429                                         x = params[5]
430                                         y = params[6]
431                                         path.addArcTo(x, y, params[2], params[0], params[1], params[3] > 0, params[4] > 0)
432                                         params = params[7:]
433                                 c2x, c2y = x, y
434                         elif command == 'c':
435                                 while len(params) > 5:
436                                         c1x = x + params[0]
437                                         c1y = y + params[1]
438                                         c2x = x + params[2]
439                                         c2y = y + params[3]
440                                         x += params[4]
441                                         y += params[5]
442                                         path.addCurveTo(x, y, c1x, c1y, c2x, c2y)
443                                         params = params[6:]
444                         elif command == 'C':
445                                 while len(params) > 5:
446                                         c1x = params[0]
447                                         c1y = params[1]
448                                         c2x = params[2]
449                                         c2y = params[3]
450                                         x = params[4]
451                                         y = params[5]
452                                         path.addCurveTo(x, y, c1x, c1y, c2x, c2y)
453                                         params = params[6:]
454                         elif command == 's':
455                                 while len(params) > 3:
456                                         c1x = x - (c2x - x)
457                                         c1y = y - (c2y - y)
458                                         c2x = x + params[0]
459                                         c2y = y + params[1]
460                                         x += params[2]
461                                         y += params[3]
462                                         path.addCurveTo(x, y, c1x, c1y, c2x, c2y)
463                                         params = params[4:]
464                         elif command == 'S':
465                                 while len(params) > 3:
466                                         c1x = x - (c2x - x)
467                                         c1y = y - (c2y - y)
468                                         c2x = params[0]
469                                         c2y = params[1]
470                                         x = params[2]
471                                         y = params[3]
472                                         path.addCurveTo(x, y, c1x, c1y, c2x, c2y)
473                                         params = params[4:]
474                         elif command == 'q':
475                                 while len(params) > 3:
476                                         c1x = x + params[0]
477                                         c1y = y + params[1]
478                                         c2x = c1x
479                                         c2y = c1y
480                                         x += params[2]
481                                         y += params[3]
482                                         path.addCurveTo(x, y, c1x, c1y, c2x, c2y)
483                                         params = params[4:]
484                         elif command == 'Q':
485                                 while len(params) > 3:
486                                         c1x = params[0]
487                                         c1y = params[1]
488                                         c2x = c1x
489                                         c2y = c1y
490                                         x = params[2]
491                                         y = params[3]
492                                         path.addCurveTo(x, y, c1x, c1y, c2x, c2y)
493                                         params = params[4:]
494                         elif command == 't':
495                                 while len(params) > 1:
496                                         c1x = x - (c2x - x)
497                                         c1y = y - (c2y - y)
498                                         c2x = c1x
499                                         c2y = c1y
500                                         x += params[0]
501                                         y += params[1]
502                                         path.addCurveTo(x, y, c1x, c1y, c2x, c2y)
503                                         params = params[2:]
504                         elif command == 'T':
505                                 while len(params) > 1:
506                                         c1x = x - (c2x - x)
507                                         c1y = y - (c2y - y)
508                                         c2x = c1x
509                                         c2y = c1y
510                                         x = params[0]
511                                         y = params[1]
512                                         path.addCurveTo(x, y, c1x, c1y, c2x, c2y)
513                                         params = params[2:]
514                         elif command == 'z' or command == 'Z':
515                                 path.closePath()
516                                 x = path._startPoint.real
517                                 y = path._startPoint.imag
518                         else:
519                                 print 'Unknown path command:', command, params
520
521
522 if __name__ == '__main__':
523         for n in xrange(1, len(sys.argv)):
524                 print 'File: %s' % (sys.argv[n])
525                 svg = SVG(sys.argv[n])
526
527         f = open("test_export.html", "w")
528
529         f.write("<!DOCTYPE html><html><body>\n")
530         f.write("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" style='width:%dpx;height:%dpx'>\n" % (1000, 1000))
531         f.write("<g fill-rule='evenodd' style=\"fill: gray; stroke:black;stroke-width:2\">\n")
532         f.write("<path d=\"")
533         for path in svg.paths:
534                 points = path.getPoints()
535                 f.write("M %f %f " % (points[0].real, points[0].imag))
536                 for point in points[1:]:
537                         f.write("L %f %f " % (point.real, point.imag))
538         f.write("\"/>")
539         f.write("</g>\n")
540
541         f.write("<g style=\"fill: none; stroke:red;stroke-width:1\">\n")
542         f.write("<path d=\"")
543         for path in svg.paths:
544                 f.write(path.getSVGPath())
545         f.write("\"/>")
546         f.write("</g>\n")
547
548         f.write("</svg>\n")
549         f.write("</body></html>")
550         f.close()
551