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