Sat, 07 Nov 2015 20:50:55 +0100
finished refactoring
2 | 1 | # SVG parser in Python |
2 | ||
3 | # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > | |
4 | ||
5 | # This program is free software; you can redistribute it and/or modify | |
6 | # it under the terms of the GNU General Public License as published by | |
7 | # the Free Software Foundation; either version 2 of the License, or | |
8 | # (at your option) any later version. | |
9 | # | |
10 | # This program is distributed in the hope that it will be useful, | |
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | # GNU General Public License for more details. | |
14 | # | |
15 | # You should have received a copy of the GNU General Public License along | |
16 | # with this program; if not, write to the Free Software Foundation, Inc., | |
17 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
18 | ||
19 | from __future__ import absolute_import | |
20 | import sys | |
21 | import os | |
22 | import copy | |
23 | import re | |
24 | import xml.etree.ElementTree as etree | |
25 | import itertools | |
26 | import operator | |
27 | import json | |
28 | from .geometry import * | |
29 | ||
30 | svg_ns = '{http://www.w3.org/2000/svg}' | |
31 | ||
32 | # Regex commonly used | |
33 | number_re = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?' | |
34 | unit_re = r'em|ex|px|in|cm|mm|pt|pc|%' | |
3 | 35 | point_re = r'(?:\d+(?:\.\d*)?|\.\d+),(?:\d+(?:\.\d*)?|\.\d+)' |
2 | 36 | |
37 | # Unit converter | |
38 | unit_convert = { | |
39 | None: 1, # Default unit (same as pixel) | |
40 | 'px': 1, # px: pixel. Default SVG unit | |
41 | 'em': 10, # 1 em = 10 px FIXME | |
42 | 'ex': 5, # 1 ex = 5 px FIXME | |
43 | 'in': 96, # 1 in = 96 px | |
44 | 'cm': 96 / 2.54, # 1 cm = 1/2.54 in | |
45 | 'mm': 96 / 25.4, # 1 mm = 1/25.4 in | |
46 | 'pt': 96 / 72.0, # 1 pt = 1/72 in | |
47 | 'pc': 96 / 6.0, # 1 pc = 1/6 in | |
48 | '%' : 1 / 100.0 # 1 percent | |
49 | } | |
50 | ||
51 | class Transformable: | |
52 | '''Abstract class for objects that can be geometrically drawn & transformed''' | |
53 | def __init__(self, elt=None): | |
54 | # a 'Transformable' is represented as a list of Transformable items | |
55 | self.items = [] | |
56 | self.id = hex(id(self)) | |
57 | # Unit transformation matrix on init | |
58 | self.matrix = Matrix() | |
59 | self.viewport = Point(800, 600) # default viewport is 800x600 | |
60 | if elt is not None: | |
61 | self.id = elt.get('id', self.id) | |
62 | # Parse transform attibute to update self.matrix | |
63 | self.getTransformations(elt) | |
64 | ||
65 | def bbox(self): | |
66 | '''Bounding box''' | |
67 | bboxes = [x.bbox() for x in self.items] | |
68 | xmin = min([b[0].x for b in bboxes]) | |
69 | xmax = max([b[1].x for b in bboxes]) | |
70 | ymin = min([b[0].y for b in bboxes]) | |
71 | ymax = max([b[1].y for b in bboxes]) | |
72 | ||
73 | return (Point(xmin,ymin), Point(xmax,ymax)) | |
74 | ||
75 | # Parse transform field | |
76 | def getTransformations(self, elt): | |
77 | t = elt.get('transform') | |
78 | if t is None: return | |
79 | ||
80 | svg_transforms = [ | |
81 | 'matrix', 'translate', 'scale', 'rotate', 'skewX', 'skewY'] | |
82 | ||
83 | # match any SVG transformation with its parameter (until final parenthese) | |
84 | # [^)]* == anything but a closing parenthese | |
85 | # '|'.join == OR-list of SVG transformations | |
86 | transforms = re.findall( | |
87 | '|'.join([x + '[^)]*\)' for x in svg_transforms]), t) | |
88 | ||
89 | for t in transforms: | |
90 | op, arg = t.split('(') | |
91 | op = op.strip() | |
92 | # Keep only numbers | |
93 | arg = [float(x) for x in re.findall(number_re, arg)] | |
94 | print('transform: ' + op + ' '+ str(arg)) | |
95 | ||
96 | if op == 'matrix': | |
97 | self.matrix *= Matrix(arg) | |
98 | ||
99 | if op == 'translate': | |
100 | tx = arg[0] | |
101 | if len(arg) == 1: ty = 0 | |
102 | else: ty = arg[1] | |
103 | self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) | |
104 | ||
105 | if op == 'scale': | |
106 | sx = arg[0] | |
107 | if len(arg) == 1: sy = sx | |
108 | else: sy = arg[1] | |
109 | self.matrix *= Matrix([sx, 0, 0, sy, 0, 0]) | |
110 | ||
111 | if op == 'rotate': | |
112 | cosa = math.cos(math.radians(arg[0])) | |
113 | sina = math.sin(math.radians(arg[0])) | |
114 | if len(arg) != 1: | |
115 | tx, ty = arg[1:3] | |
116 | self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) | |
117 | self.matrix *= Matrix([cosa, sina, -sina, cosa, 0, 0]) | |
118 | if len(arg) != 1: | |
119 | self.matrix *= Matrix([1, 0, 0, 1, -tx, -ty]) | |
120 | ||
121 | if op == 'skewX': | |
122 | tana = math.tan(math.radians(arg[0])) | |
123 | self.matrix *= Matrix([1, 0, tana, 1, 0, 0]) | |
124 | ||
125 | if op == 'skewY': | |
126 | tana = math.tan(math.radians(arg[0])) | |
127 | self.matrix *= Matrix([1, tana, 0, 1, 0, 0]) | |
128 | ||
129 | def transform(self, matrix=None): | |
130 | for x in self.items: | |
131 | x.transform(self.matrix) | |
132 | ||
133 | def length(self, v, mode='xy'): | |
134 | # Handle empty (non-existing) length element | |
135 | if v is None: | |
136 | return 0 | |
137 | ||
138 | # Get length value | |
139 | m = re.search(number_re, v) | |
140 | if m: value = m.group(0) | |
141 | else: raise TypeError(v + 'is not a valid length') | |
142 | ||
143 | # Get length unit | |
144 | m = re.search(unit_re, v) | |
145 | if m: unit = m.group(0) | |
146 | else: unit = None | |
147 | ||
148 | if unit == '%': | |
149 | if mode == 'x': | |
150 | return float(value) * unit_convert[unit] * self.viewport.x | |
151 | if mode == 'y': | |
152 | return float(value) * unit_convert[unit] * self.viewport.y | |
153 | if mode == 'xy': | |
154 | return float(value) * unit_convert[unit] * self.viewport.x # FIXME | |
155 | ||
156 | return float(value) * unit_convert[unit] | |
157 | ||
158 | def xlength(self, x): | |
159 | return self.length(x, 'x') | |
160 | def ylength(self, y): | |
161 | return self.length(y, 'y') | |
162 | ||
163 | def flatten(self): | |
164 | '''Flatten the SVG objects nested list into a flat (1-D) list, | |
165 | removing Groups''' | |
166 | # http://rightfootin.blogspot.fr/2006/09/more-on-python-flatten.html | |
167 | # Assigning a slice a[i:i+1] with a list actually replaces the a[i] | |
168 | # element with the content of the assigned list | |
169 | i = 0 | |
170 | flat = copy.deepcopy(self.items) | |
171 | while i < len(flat): | |
172 | while isinstance(flat[i], Group): | |
173 | flat[i:i+1] = flat[i].items | |
174 | i += 1 | |
175 | return flat | |
176 | ||
177 | def scale(self, ratio): | |
178 | for x in self.items: | |
179 | x.scale(ratio) | |
180 | return self | |
181 | ||
182 | def translate(self, offset): | |
183 | for x in self.items: | |
184 | x.translate(offset) | |
185 | return self | |
186 | ||
187 | def rotate(self, angle): | |
188 | for x in self.items: | |
189 | x.rotate(angle) | |
190 | return self | |
191 | ||
192 | class Svg(Transformable): | |
193 | '''SVG class: use parse to parse a file''' | |
194 | # class Svg handles the <svg> tag | |
195 | # tag = 'svg' | |
196 | ||
197 | def __init__(self, filename=None): | |
198 | Transformable.__init__(self) | |
199 | if filename: | |
200 | self.parse(filename) | |
201 | ||
202 | def parse(self, filename): | |
203 | self.filename = filename | |
204 | tree = etree.parse(filename) | |
205 | self.root = tree.getroot() | |
206 | if self.root.tag != svg_ns + 'svg': | |
207 | raise TypeError('file %s does not seem to be a valid SVG file', filename) | |
208 | ||
209 | # Create a top Group to group all other items (useful for viewBox elt) | |
210 | top_group = Group() | |
211 | self.items.append(top_group) | |
212 | ||
213 | # SVG dimension | |
214 | width = self.xlength(self.root.get('width')) | |
215 | height = self.ylength(self.root.get('height')) | |
216 | # update viewport | |
217 | top_group.viewport = Point(width, height) | |
218 | ||
219 | # viewBox | |
220 | if self.root.get('viewBox') is not None: | |
221 | viewBox = re.findall(number_re, self.root.get('viewBox')) | |
222 | sx = width / float(viewBox[2]) | |
223 | sy = height / float(viewBox[3]) | |
224 | tx = -float(viewBox[0]) | |
225 | ty = -float(viewBox[1]) | |
226 | top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) | |
227 | ||
228 | # Parse XML elements hierarchically with groups <g> | |
229 | top_group.append(self.root) | |
230 | ||
231 | self.transform() | |
232 | ||
233 | def title(self): | |
234 | t = self.root.find(svg_ns + 'title') | |
235 | if t is not None: | |
236 | return t | |
237 | else: | |
238 | return os.path.splitext(os.path.basename(self.filename))[0] | |
239 | ||
240 | def json(self): | |
241 | return self.items | |
242 | ||
243 | ||
244 | class Group(Transformable): | |
245 | '''Handle svg <g> elements''' | |
246 | # class Group handles the <g> tag | |
247 | tag = 'g' | |
248 | ||
249 | def __init__(self, elt=None): | |
250 | Transformable.__init__(self, elt) | |
251 | ||
252 | def append(self, element): | |
253 | for elt in element: | |
254 | elt_class = svgClass.get(elt.tag, None) | |
255 | if elt_class is None: | |
256 | print('No handler for element %s' % elt.tag) | |
257 | continue | |
258 | # instanciate elt associated class (e.g. <path>: item = Path(elt) | |
259 | item = elt_class(elt) | |
260 | # Apply group matrix to the newly created object | |
261 | item.matrix = self.matrix * item.matrix | |
262 | item.viewport = self.viewport | |
263 | ||
264 | self.items.append(item) | |
265 | # Recursively append if elt is a <g> (group) | |
266 | if elt.tag == svg_ns + 'g': | |
267 | item.append(elt) | |
268 | ||
269 | def __repr__(self): | |
270 | return '<Group ' + self.id + '>: ' + repr(self.items) | |
271 | ||
272 | def json(self): | |
273 | return {'Group ' + self.id : self.items} | |
274 | ||
275 | class Matrix: | |
276 | ''' SVG transformation matrix and its operations | |
277 | a SVG matrix is represented as a list of 6 values [a, b, c, d, e, f] | |
278 | (named vect hereafter) which represent the 3x3 matrix | |
279 | ((a, c, e) | |
280 | (b, d, f) | |
281 | (0, 0, 1)) | |
282 | see http://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace ''' | |
283 | ||
284 | def __init__(self, vect=[1, 0, 0, 1, 0, 0]): | |
285 | # Unit transformation vect by default | |
286 | if len(vect) != 6: | |
287 | raise ValueError("Bad vect size %d" % len(vect)) | |
288 | self.vect = list(vect) | |
289 | ||
290 | def __mul__(self, other): | |
291 | '''Matrix multiplication''' | |
292 | if isinstance(other, Matrix): | |
293 | a = self.vect[0] * other.vect[0] + self.vect[2] * other.vect[1] | |
294 | b = self.vect[1] * other.vect[0] + self.vect[3] * other.vect[1] | |
295 | c = self.vect[0] * other.vect[2] + self.vect[2] * other.vect[3] | |
296 | d = self.vect[1] * other.vect[2] + self.vect[3] * other.vect[3] | |
297 | e = self.vect[0] * other.vect[4] + self.vect[2] * other.vect[5] \ | |
298 | + self.vect[4] | |
299 | f = self.vect[1] * other.vect[4] + self.vect[3] * other.vect[5] \ | |
300 | + self.vect[5] | |
301 | return Matrix([a, b, c, d, e, f]) | |
302 | ||
303 | elif isinstance(other, Point): | |
304 | x = other.x * self.vect[0] + other.y * self.vect[2] + self.vect[4] | |
305 | y = other.x * self.vect[1] + other.y * self.vect[3] + self.vect[5] | |
306 | return Point(x,y) | |
307 | ||
308 | else: | |
309 | return NotImplemented | |
310 | ||
311 | def __str__(self): | |
312 | return str(self.vect) | |
313 | ||
314 | def xlength(self, x): | |
315 | return x * self.vect[0] | |
316 | def ylength(self, y): | |
317 | return y * self.vect[3] | |
318 | ||
319 | ||
320 | COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' | |
321 | ||
322 | class Path(Transformable): | |
323 | '''SVG <path>''' | |
324 | # class Path handles the <path> tag | |
325 | tag = 'path' | |
326 | ||
327 | def __init__(self, elt=None): | |
328 | Transformable.__init__(self, elt) | |
329 | if elt is not None: | |
330 | self.style = elt.get('style') | |
331 | self.parse(elt.get('d')) | |
332 | ||
333 | def parse(self, pathstr): | |
334 | """Parse path string and build elements list""" | |
335 | ||
336 | pathlst = re.findall(number_re + r"|\ *[%s]\ *" % COMMANDS, pathstr) | |
337 | ||
338 | pathlst.reverse() | |
339 | ||
340 | command = None | |
341 | current_pt = Point(0,0) | |
342 | start_pt = None | |
343 | ||
344 | while pathlst: | |
345 | if pathlst[-1].strip() in COMMANDS: | |
346 | last_command = command | |
347 | command = pathlst.pop().strip() | |
348 | absolute = (command == command.upper()) | |
349 | command = command.upper() | |
350 | else: | |
351 | if command is None: | |
352 | raise ValueError("No command found at %d" % len(pathlst)) | |
353 | ||
354 | if command == 'M': | |
355 | # MoveTo | |
356 | x = pathlst.pop() | |
357 | y = pathlst.pop() | |
358 | pt = Point(x, y) | |
359 | if absolute: | |
360 | current_pt = pt | |
361 | else: | |
362 | current_pt += pt | |
363 | start_pt = current_pt | |
364 | ||
365 | self.items.append(MoveTo(current_pt)) | |
366 | ||
367 | # MoveTo with multiple coordinates means LineTo | |
368 | command = 'L' | |
369 | ||
370 | elif command == 'Z': | |
371 | # Close Path | |
372 | l = Segment(current_pt, start_pt) | |
373 | self.items.append(l) | |
374 | ||
375 | ||
376 | elif command in 'LHV': | |
377 | # LineTo, Horizontal & Vertical line | |
378 | # extra coord for H,V | |
379 | if absolute: | |
380 | x,y = current_pt.coord() | |
381 | else: | |
382 | x,y = (0,0) | |
383 | ||
384 | if command in 'LH': | |
385 | x = pathlst.pop() | |
386 | if command in 'LV': | |
387 | y = pathlst.pop() | |
388 | ||
389 | pt = Point(x, y) | |
390 | if not absolute: | |
391 | pt += current_pt | |
392 | ||
393 | self.items.append(Segment(current_pt, pt)) | |
394 | current_pt = pt | |
395 | ||
396 | elif command in 'CQ': | |
397 | dimension = {'Q':3, 'C':4} | |
398 | bezier_pts = [] | |
399 | bezier_pts.append(current_pt) | |
400 | for i in range(1,dimension[command]): | |
401 | x = pathlst.pop() | |
402 | y = pathlst.pop() | |
403 | pt = Point(x, y) | |
404 | if not absolute: | |
405 | pt += current_pt | |
406 | bezier_pts.append(pt) | |
407 | ||
408 | self.items.append(Bezier(bezier_pts)) | |
409 | current_pt = pt | |
410 | ||
411 | elif command in 'TS': | |
412 | # number of points to read | |
413 | nbpts = {'T':1, 'S':2} | |
414 | # the control point, from previous Bezier to mirror | |
415 | ctrlpt = {'T':1, 'S':2} | |
416 | # last command control | |
417 | last = {'T': 'QT', 'S':'CS'} | |
418 | ||
419 | bezier_pts = [] | |
420 | bezier_pts.append(current_pt) | |
421 | ||
422 | if last_command in last[command]: | |
423 | pt0 = self.items[-1].control_point(ctrlpt[command]) | |
424 | else: | |
425 | pt0 = current_pt | |
426 | pt1 = current_pt | |
427 | # Symetrical of pt1 against pt0 | |
428 | bezier_pts.append(pt1 + pt1 - pt0) | |
429 | ||
430 | for i in range(0,nbpts[command]): | |
431 | x = pathlst.pop() | |
432 | y = pathlst.pop() | |
433 | pt = Point(x, y) | |
434 | if not absolute: | |
435 | pt += current_pt | |
436 | bezier_pts.append(pt) | |
437 | ||
438 | self.items.append(Bezier(bezier_pts)) | |
439 | current_pt = pt | |
440 | ||
441 | elif command == 'A': | |
442 | rx = pathlst.pop() | |
443 | ry = pathlst.pop() | |
444 | xrot = pathlst.pop() | |
445 | # Arc flags are not necesarily sepatated numbers | |
446 | flags = pathlst.pop().strip() | |
447 | large_arc_flag = flags[0] | |
448 | if large_arc_flag not in '01': | |
449 | print('Arc parsing failure') | |
450 | break | |
451 | ||
452 | if len(flags) > 1: flags = flags[1:].strip() | |
453 | else: flags = pathlst.pop().strip() | |
454 | sweep_flag = flags[0] | |
455 | if sweep_flag not in '01': | |
456 | print('Arc parsing failure') | |
457 | break | |
458 | ||
459 | if len(flags) > 1: x = flags[1:] | |
460 | else: x = pathlst.pop() | |
461 | y = pathlst.pop() | |
462 | # TODO | |
463 | print('ARC: ' + | |
464 | ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) | |
465 | # self.items.append( | |
466 | # Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) | |
467 | ||
468 | else: | |
469 | pathlst.pop() | |
470 | ||
471 | def __str__(self): | |
472 | return '\n'.join(str(x) for x in self.items) | |
473 | ||
474 | def __repr__(self): | |
475 | return '<Path ' + self.id + '>' | |
476 | ||
477 | def segments(self, precision=0): | |
478 | '''Return a list of segments, each segment is ended by a MoveTo. | |
479 | A segment is a list of Points''' | |
480 | ret = [] | |
481 | # group items separated by MoveTo | |
482 | for moveTo, group in itertools.groupby(self.items, | |
483 | lambda x: isinstance(x, MoveTo)): | |
484 | # Use only non MoveTo item | |
485 | if not moveTo: | |
486 | # Generate segments for each relevant item | |
487 | seg = [x.segments(precision) for x in group] | |
488 | # Merge all segments into one | |
489 | ret.append(list(itertools.chain.from_iterable(seg))) | |
490 | ||
491 | return ret | |
492 | ||
493 | def simplify(self, precision): | |
494 | '''Simplify segment with precision: | |
495 | Remove any point which are ~aligned''' | |
496 | ret = [] | |
497 | for seg in self.segments(precision): | |
498 | ret.append(simplify_segment(seg, precision)) | |
499 | ||
500 | return ret | |
501 | ||
3 | 502 | |
503 | class Polygon(Transformable): | |
504 | '''SVG <polygon>''' | |
505 | # class Path handles the <polygon> tag | |
506 | tag = 'polygon' | |
507 | ||
508 | def __init__(self, elt=None): | |
509 | Transformable.__init__(self, elt) | |
510 | if elt is not None: | |
511 | self.style = elt.get('style') | |
512 | self.parse(elt.get('points')) | |
513 | ||
514 | def parse(self, pathstr): | |
515 | """Parse path string and build elements list""" | |
516 | ||
517 | pathlst = re.findall(point_re, pathstr) | |
518 | ||
519 | #pathlst.reverse() | |
520 | ||
521 | current_pt = None | |
522 | start_pt = None | |
523 | while pathlst: | |
524 | coord = pathlst.pop().split(",") | |
525 | pt = Point(coord[0], coord[1]) | |
526 | ||
527 | if start_pt: | |
528 | current_pt = pt | |
529 | l = Segment(start_pt, current_pt) | |
530 | self.items.append(l) | |
531 | start_pt = current_pt | |
532 | else: | |
533 | start_pt = pt | |
534 | self.items.append(MoveTo(pt)) | |
535 | ||
536 | def __str__(self): | |
537 | return '\n'.join(str(x) for x in self.items) | |
538 | ||
539 | def __repr__(self): | |
540 | return '<Polygon ' + self.id + '>' | |
541 | ||
542 | def segments(self, precision=0): | |
543 | '''Return a list of segments, each segment is ended by a MoveTo. | |
544 | A segment is a list of Points''' | |
545 | ret = [] | |
546 | # group items separated by MoveTo | |
547 | for moveTo, group in itertools.groupby(self.items, | |
548 | lambda x: isinstance(x, MoveTo)): | |
549 | # Use only non MoveTo item | |
550 | if not moveTo: | |
551 | # Generate segments for each relevant item | |
552 | seg = [x.segments(precision) for x in group] | |
553 | # Merge all segments into one | |
554 | ret.append(list(itertools.chain.from_iterable(seg))) | |
555 | ||
556 | return ret | |
557 | ||
558 | def simplify(self, precision): | |
559 | '''Simplify segment with precision: | |
560 | Remove any point which are ~aligned''' | |
561 | ret = [] | |
562 | for seg in self.segments(precision): | |
563 | ret.append(simplify_segment(seg, precision)) | |
564 | ||
565 | return ret | |
566 | ||
567 | ||
2 | 568 | class Ellipse(Transformable): |
569 | '''SVG <ellipse>''' | |
570 | # class Ellipse handles the <ellipse> tag | |
571 | tag = 'ellipse' | |
572 | ||
573 | def __init__(self, elt=None): | |
574 | Transformable.__init__(self, elt) | |
575 | if elt is not None: | |
576 | self.center = Point(self.xlength(elt.get('cx')), | |
577 | self.ylength(elt.get('cy'))) | |
578 | self.rx = self.length(elt.get('rx')) | |
579 | self.ry = self.length(elt.get('ry')) | |
580 | self.style = elt.get('style') | |
581 | ||
582 | def __repr__(self): | |
583 | return '<Ellipse ' + self.id + '>' | |
584 | ||
585 | def bbox(self): | |
586 | '''Bounding box''' | |
587 | pmin = self.center - Point(self.rx, self.ry) | |
588 | pmax = self.center + Point(self.rx, self.ry) | |
589 | return (pmin, pmax) | |
590 | ||
591 | def transform(self, matrix): | |
592 | self.center = self.matrix * self.center | |
593 | self.rx = self.matrix.xlength(self.rx) | |
594 | self.ry = self.matrix.ylength(self.ry) | |
595 | ||
596 | def scale(self, ratio): | |
597 | self.center *= ratio | |
598 | self.rx *= ratio | |
599 | self.ry *= ratio | |
600 | def translate(self, offset): | |
601 | self.center += offset | |
602 | def rotate(self, angle): | |
603 | self.center = self.center.rot(angle) | |
604 | ||
605 | def P(self, t): | |
606 | '''Return a Point on the Ellipse for t in [0..1]''' | |
607 | x = self.center.x + self.rx * math.cos(2 * math.pi * t) | |
608 | y = self.center.y + self.ry * math.sin(2 * math.pi * t) | |
609 | return Point(x,y) | |
610 | ||
611 | def segments(self, precision=0): | |
612 | if max(self.rx, self.ry) < precision: | |
613 | return [[self.center]] | |
614 | ||
615 | p = [(0,self.P(0)), (1, self.P(1))] | |
616 | d = 2 * max(self.rx, self.ry) | |
617 | ||
618 | while d > precision: | |
619 | for (t1,p1),(t2,p2) in zip(p[:-1],p[1:]): | |
620 | t = t1 + (t2 - t1)/2. | |
621 | d = Segment(p1, p2).pdistance(self.P(t)) | |
622 | p.append((t, self.P(t))) | |
623 | p.sort(key=operator.itemgetter(0)) | |
624 | ||
625 | ret = [x for t,x in p] | |
626 | return [ret] | |
627 | ||
628 | def simplify(self, precision): | |
629 | return self | |
630 | ||
631 | # A circle is a special type of ellipse where rx = ry = radius | |
632 | class Circle(Ellipse): | |
633 | '''SVG <circle>''' | |
634 | # class Circle handles the <circle> tag | |
635 | tag = 'circle' | |
636 | ||
637 | def __init__(self, elt=None): | |
638 | if elt is not None: | |
639 | elt.set('rx', elt.get('r')) | |
640 | elt.set('ry', elt.get('r')) | |
641 | Ellipse.__init__(self, elt) | |
642 | ||
643 | def __repr__(self): | |
644 | return '<Circle ' + self.id + '>' | |
645 | ||
646 | class Rect(Transformable): | |
647 | '''SVG <rect>''' | |
648 | # class Rect handles the <rect> tag | |
649 | tag = 'rect' | |
650 | ||
651 | def __init__(self, elt=None): | |
652 | Transformable.__init__(self, elt) | |
653 | if elt is not None: | |
654 | self.P1 = Point(self.xlength(elt.get('x')), | |
655 | self.ylength(elt.get('y'))) | |
656 | ||
657 | self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), | |
658 | self.P1.y + self.ylength(elt.get('height'))) | |
659 | ||
660 | def __repr__(self): | |
661 | return '<Rect ' + self.id + '>' | |
662 | ||
663 | def bbox(self): | |
664 | '''Bounding box''' | |
665 | xmin = min([p.x for p in (self.P1, self.P2)]) | |
666 | xmax = max([p.x for p in (self.P1, self.P2)]) | |
667 | ymin = min([p.y for p in (self.P1, self.P2)]) | |
668 | ymax = max([p.y for p in (self.P1, self.P2)]) | |
669 | ||
670 | return (Point(xmin,ymin), Point(xmax,ymax)) | |
671 | ||
672 | def transform(self, matrix): | |
673 | self.P1 = self.matrix * self.P1 | |
674 | self.P2 = self.matrix * self.P2 | |
675 | ||
676 | def segments(self, precision=0): | |
677 | # A rectangle is built with a segment going thru 4 points | |
678 | ret = [] | |
679 | Pa = Point(self.P1.x, self.P2.y) | |
680 | Pb = Point(self.P2.x, self.P1.y) | |
681 | ||
682 | ret.append([self.P1, Pa, self.P2, Pb, self.P1]) | |
683 | return ret | |
684 | ||
685 | def simplify(self, precision): | |
686 | return self.segments(precision) | |
687 | ||
688 | class Line(Transformable): | |
689 | '''SVG <line>''' | |
690 | # class Line handles the <line> tag | |
691 | tag = 'line' | |
692 | ||
693 | def __init__(self, elt=None): | |
694 | Transformable.__init__(self, elt) | |
695 | if elt is not None: | |
696 | self.P1 = Point(self.xlength(elt.get('x1')), | |
697 | self.ylength(elt.get('y1'))) | |
698 | self.P2 = Point(self.xlength(elt.get('x2')), | |
699 | self.ylength(elt.get('y2'))) | |
700 | self.segment = Segment(self.P1, self.P2) | |
701 | ||
702 | def __repr__(self): | |
703 | return '<Line ' + self.id + '>' | |
704 | ||
705 | def bbox(self): | |
706 | '''Bounding box''' | |
707 | xmin = min([p.x for p in (self.P1, self.P2)]) | |
708 | xmax = max([p.x for p in (self.P1, self.P2)]) | |
709 | ymin = min([p.y for p in (self.P1, self.P2)]) | |
710 | ymax = max([p.y for p in (self.P1, self.P2)]) | |
711 | ||
712 | return (Point(xmin,ymin), Point(xmax,ymax)) | |
713 | ||
714 | def transform(self, matrix): | |
715 | self.P1 = self.matrix * self.P1 | |
716 | self.P2 = self.matrix * self.P2 | |
717 | self.segment = Segment(self.P1, self.P2) | |
718 | ||
719 | def segments(self, precision=0): | |
720 | return [self.segment.segments()] | |
721 | ||
722 | def simplify(self, precision): | |
723 | return self.segments(precision) | |
724 | ||
725 | # overwrite JSONEncoder for svg classes which have defined a .json() method | |
726 | class JSONEncoder(json.JSONEncoder): | |
727 | def default(self, obj): | |
728 | if not isinstance(obj, tuple(svgClass.values() + [Svg])): | |
729 | return json.JSONEncoder.default(self, obj) | |
730 | ||
731 | if not hasattr(obj, 'json'): | |
732 | return repr(obj) | |
733 | ||
734 | return obj.json() | |
735 | ||
736 | ## Code executed on module load ## | |
737 | ||
738 | # SVG tag handler classes are initialized here | |
739 | # (classes must be defined before) | |
740 | import inspect | |
741 | svgClass = {} | |
742 | # Register all classes with attribute 'tag' in svgClass dict | |
743 | for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): | |
744 | tag = getattr(cls, 'tag', None) | |
745 | if tag: | |
746 | svgClass[svg_ns + tag] = cls | |
747 |