Wed, 20 Jan 2021 10:17:01 +0100
updated pronterface.py losing my own changes
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) | |
12 | 251 | if elt is not None: |
252 | self.style = elt.get('style') | |
253 | else: | |
254 | self.style = '' | |
2 | 255 | |
256 | def append(self, element): | |
257 | for elt in element: | |
258 | elt_class = svgClass.get(elt.tag, None) | |
259 | if elt_class is None: | |
260 | print('No handler for element %s' % elt.tag) | |
261 | continue | |
262 | # instanciate elt associated class (e.g. <path>: item = Path(elt) | |
263 | item = elt_class(elt) | |
264 | # Apply group matrix to the newly created object | |
265 | item.matrix = self.matrix * item.matrix | |
266 | item.viewport = self.viewport | |
267 | ||
12 | 268 | # inherit style from group |
269 | if item.style == '': | |
270 | item.style = self.style | |
271 | ||
2 | 272 | self.items.append(item) |
273 | # Recursively append if elt is a <g> (group) | |
274 | if elt.tag == svg_ns + 'g': | |
275 | item.append(elt) | |
276 | ||
277 | def __repr__(self): | |
278 | return '<Group ' + self.id + '>: ' + repr(self.items) | |
279 | ||
280 | def json(self): | |
281 | return {'Group ' + self.id : self.items} | |
282 | ||
283 | class Matrix: | |
284 | ''' SVG transformation matrix and its operations | |
285 | a SVG matrix is represented as a list of 6 values [a, b, c, d, e, f] | |
286 | (named vect hereafter) which represent the 3x3 matrix | |
287 | ((a, c, e) | |
288 | (b, d, f) | |
289 | (0, 0, 1)) | |
290 | see http://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace ''' | |
291 | ||
292 | def __init__(self, vect=[1, 0, 0, 1, 0, 0]): | |
293 | # Unit transformation vect by default | |
294 | if len(vect) != 6: | |
295 | raise ValueError("Bad vect size %d" % len(vect)) | |
296 | self.vect = list(vect) | |
297 | ||
298 | def __mul__(self, other): | |
299 | '''Matrix multiplication''' | |
300 | if isinstance(other, Matrix): | |
301 | a = self.vect[0] * other.vect[0] + self.vect[2] * other.vect[1] | |
302 | b = self.vect[1] * other.vect[0] + self.vect[3] * other.vect[1] | |
303 | c = self.vect[0] * other.vect[2] + self.vect[2] * other.vect[3] | |
304 | d = self.vect[1] * other.vect[2] + self.vect[3] * other.vect[3] | |
305 | e = self.vect[0] * other.vect[4] + self.vect[2] * other.vect[5] \ | |
306 | + self.vect[4] | |
307 | f = self.vect[1] * other.vect[4] + self.vect[3] * other.vect[5] \ | |
308 | + self.vect[5] | |
309 | return Matrix([a, b, c, d, e, f]) | |
310 | ||
311 | elif isinstance(other, Point): | |
312 | x = other.x * self.vect[0] + other.y * self.vect[2] + self.vect[4] | |
313 | y = other.x * self.vect[1] + other.y * self.vect[3] + self.vect[5] | |
314 | return Point(x,y) | |
315 | ||
316 | else: | |
317 | return NotImplemented | |
318 | ||
319 | def __str__(self): | |
320 | return str(self.vect) | |
321 | ||
322 | def xlength(self, x): | |
323 | return x * self.vect[0] | |
324 | def ylength(self, y): | |
325 | return y * self.vect[3] | |
326 | ||
327 | ||
328 | COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' | |
329 | ||
330 | class Path(Transformable): | |
331 | '''SVG <path>''' | |
332 | # class Path handles the <path> tag | |
333 | tag = 'path' | |
334 | ||
335 | def __init__(self, elt=None): | |
336 | Transformable.__init__(self, elt) | |
337 | if elt is not None: | |
338 | self.style = elt.get('style') | |
339 | self.parse(elt.get('d')) | |
340 | ||
341 | def parse(self, pathstr): | |
342 | """Parse path string and build elements list""" | |
343 | ||
344 | pathlst = re.findall(number_re + r"|\ *[%s]\ *" % COMMANDS, pathstr) | |
345 | ||
346 | pathlst.reverse() | |
347 | ||
348 | command = None | |
349 | current_pt = Point(0,0) | |
350 | start_pt = None | |
351 | ||
352 | while pathlst: | |
353 | if pathlst[-1].strip() in COMMANDS: | |
354 | last_command = command | |
355 | command = pathlst.pop().strip() | |
356 | absolute = (command == command.upper()) | |
357 | command = command.upper() | |
358 | else: | |
359 | if command is None: | |
360 | raise ValueError("No command found at %d" % len(pathlst)) | |
361 | ||
362 | if command == 'M': | |
363 | # MoveTo | |
364 | x = pathlst.pop() | |
365 | y = pathlst.pop() | |
366 | pt = Point(x, y) | |
367 | if absolute: | |
368 | current_pt = pt | |
369 | else: | |
370 | current_pt += pt | |
371 | start_pt = current_pt | |
372 | ||
373 | self.items.append(MoveTo(current_pt)) | |
374 | ||
375 | # MoveTo with multiple coordinates means LineTo | |
376 | command = 'L' | |
377 | ||
378 | elif command == 'Z': | |
379 | # Close Path | |
380 | l = Segment(current_pt, start_pt) | |
381 | self.items.append(l) | |
382 | ||
383 | ||
384 | elif command in 'LHV': | |
385 | # LineTo, Horizontal & Vertical line | |
386 | # extra coord for H,V | |
387 | if absolute: | |
388 | x,y = current_pt.coord() | |
389 | else: | |
390 | x,y = (0,0) | |
391 | ||
392 | if command in 'LH': | |
393 | x = pathlst.pop() | |
394 | if command in 'LV': | |
395 | y = pathlst.pop() | |
396 | ||
397 | pt = Point(x, y) | |
398 | if not absolute: | |
399 | pt += current_pt | |
400 | ||
401 | self.items.append(Segment(current_pt, pt)) | |
402 | current_pt = pt | |
403 | ||
404 | elif command in 'CQ': | |
405 | dimension = {'Q':3, 'C':4} | |
406 | bezier_pts = [] | |
407 | bezier_pts.append(current_pt) | |
408 | for i in range(1,dimension[command]): | |
409 | x = pathlst.pop() | |
410 | y = pathlst.pop() | |
411 | pt = Point(x, y) | |
412 | if not absolute: | |
413 | pt += current_pt | |
414 | bezier_pts.append(pt) | |
415 | ||
416 | self.items.append(Bezier(bezier_pts)) | |
417 | current_pt = pt | |
418 | ||
419 | elif command in 'TS': | |
420 | # number of points to read | |
421 | nbpts = {'T':1, 'S':2} | |
422 | # the control point, from previous Bezier to mirror | |
423 | ctrlpt = {'T':1, 'S':2} | |
424 | # last command control | |
425 | last = {'T': 'QT', 'S':'CS'} | |
426 | ||
427 | bezier_pts = [] | |
428 | bezier_pts.append(current_pt) | |
429 | ||
430 | if last_command in last[command]: | |
431 | pt0 = self.items[-1].control_point(ctrlpt[command]) | |
432 | else: | |
433 | pt0 = current_pt | |
434 | pt1 = current_pt | |
435 | # Symetrical of pt1 against pt0 | |
436 | bezier_pts.append(pt1 + pt1 - pt0) | |
437 | ||
438 | for i in range(0,nbpts[command]): | |
439 | x = pathlst.pop() | |
440 | y = pathlst.pop() | |
441 | pt = Point(x, y) | |
442 | if not absolute: | |
443 | pt += current_pt | |
444 | bezier_pts.append(pt) | |
445 | ||
446 | self.items.append(Bezier(bezier_pts)) | |
447 | current_pt = pt | |
448 | ||
449 | elif command == 'A': | |
450 | rx = pathlst.pop() | |
451 | ry = pathlst.pop() | |
452 | xrot = pathlst.pop() | |
453 | # Arc flags are not necesarily sepatated numbers | |
454 | flags = pathlst.pop().strip() | |
455 | large_arc_flag = flags[0] | |
456 | if large_arc_flag not in '01': | |
457 | print('Arc parsing failure') | |
458 | break | |
459 | ||
460 | if len(flags) > 1: flags = flags[1:].strip() | |
461 | else: flags = pathlst.pop().strip() | |
462 | sweep_flag = flags[0] | |
463 | if sweep_flag not in '01': | |
464 | print('Arc parsing failure') | |
465 | break | |
466 | ||
467 | if len(flags) > 1: x = flags[1:] | |
468 | else: x = pathlst.pop() | |
469 | y = pathlst.pop() | |
470 | # TODO | |
471 | print('ARC: ' + | |
472 | ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) | |
473 | # self.items.append( | |
474 | # Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) | |
475 | ||
476 | else: | |
477 | pathlst.pop() | |
478 | ||
479 | def __str__(self): | |
480 | return '\n'.join(str(x) for x in self.items) | |
481 | ||
482 | def __repr__(self): | |
483 | return '<Path ' + self.id + '>' | |
484 | ||
485 | def segments(self, precision=0): | |
486 | '''Return a list of segments, each segment is ended by a MoveTo. | |
487 | A segment is a list of Points''' | |
488 | ret = [] | |
489 | # group items separated by MoveTo | |
490 | for moveTo, group in itertools.groupby(self.items, | |
491 | lambda x: isinstance(x, MoveTo)): | |
492 | # Use only non MoveTo item | |
493 | if not moveTo: | |
494 | # Generate segments for each relevant item | |
495 | seg = [x.segments(precision) for x in group] | |
496 | # Merge all segments into one | |
497 | ret.append(list(itertools.chain.from_iterable(seg))) | |
498 | ||
499 | return ret | |
500 | ||
501 | def simplify(self, precision): | |
502 | '''Simplify segment with precision: | |
503 | Remove any point which are ~aligned''' | |
504 | ret = [] | |
505 | for seg in self.segments(precision): | |
506 | ret.append(simplify_segment(seg, precision)) | |
507 | ||
508 | return ret | |
509 | ||
3 | 510 | |
511 | class Polygon(Transformable): | |
512 | '''SVG <polygon>''' | |
513 | # class Path handles the <polygon> tag | |
514 | tag = 'polygon' | |
515 | ||
516 | def __init__(self, elt=None): | |
517 | Transformable.__init__(self, elt) | |
518 | if elt is not None: | |
519 | self.style = elt.get('style') | |
520 | self.parse(elt.get('points')) | |
521 | ||
522 | def parse(self, pathstr): | |
523 | """Parse path string and build elements list""" | |
524 | ||
525 | pathlst = re.findall(point_re, pathstr) | |
526 | ||
527 | #pathlst.reverse() | |
528 | ||
529 | current_pt = None | |
530 | start_pt = None | |
531 | while pathlst: | |
532 | coord = pathlst.pop().split(",") | |
533 | pt = Point(coord[0], coord[1]) | |
534 | ||
535 | if start_pt: | |
536 | current_pt = pt | |
537 | l = Segment(start_pt, current_pt) | |
538 | self.items.append(l) | |
539 | start_pt = current_pt | |
540 | else: | |
541 | start_pt = pt | |
542 | self.items.append(MoveTo(pt)) | |
543 | ||
544 | def __str__(self): | |
545 | return '\n'.join(str(x) for x in self.items) | |
546 | ||
547 | def __repr__(self): | |
548 | return '<Polygon ' + self.id + '>' | |
549 | ||
550 | def segments(self, precision=0): | |
551 | '''Return a list of segments, each segment is ended by a MoveTo. | |
552 | A segment is a list of Points''' | |
553 | ret = [] | |
554 | # group items separated by MoveTo | |
555 | for moveTo, group in itertools.groupby(self.items, | |
556 | lambda x: isinstance(x, MoveTo)): | |
557 | # Use only non MoveTo item | |
558 | if not moveTo: | |
559 | # Generate segments for each relevant item | |
560 | seg = [x.segments(precision) for x in group] | |
561 | # Merge all segments into one | |
562 | ret.append(list(itertools.chain.from_iterable(seg))) | |
563 | ||
564 | return ret | |
565 | ||
566 | def simplify(self, precision): | |
567 | '''Simplify segment with precision: | |
568 | Remove any point which are ~aligned''' | |
569 | ret = [] | |
570 | for seg in self.segments(precision): | |
571 | ret.append(simplify_segment(seg, precision)) | |
572 | ||
573 | return ret | |
574 | ||
575 | ||
2 | 576 | class Ellipse(Transformable): |
577 | '''SVG <ellipse>''' | |
578 | # class Ellipse handles the <ellipse> tag | |
579 | tag = 'ellipse' | |
580 | ||
581 | def __init__(self, elt=None): | |
582 | Transformable.__init__(self, elt) | |
583 | if elt is not None: | |
584 | self.center = Point(self.xlength(elt.get('cx')), | |
585 | self.ylength(elt.get('cy'))) | |
586 | self.rx = self.length(elt.get('rx')) | |
587 | self.ry = self.length(elt.get('ry')) | |
588 | self.style = elt.get('style') | |
589 | ||
590 | def __repr__(self): | |
591 | return '<Ellipse ' + self.id + '>' | |
592 | ||
593 | def bbox(self): | |
594 | '''Bounding box''' | |
595 | pmin = self.center - Point(self.rx, self.ry) | |
596 | pmax = self.center + Point(self.rx, self.ry) | |
597 | return (pmin, pmax) | |
598 | ||
599 | def transform(self, matrix): | |
600 | self.center = self.matrix * self.center | |
601 | self.rx = self.matrix.xlength(self.rx) | |
602 | self.ry = self.matrix.ylength(self.ry) | |
603 | ||
604 | def scale(self, ratio): | |
605 | self.center *= ratio | |
606 | self.rx *= ratio | |
607 | self.ry *= ratio | |
608 | def translate(self, offset): | |
609 | self.center += offset | |
610 | def rotate(self, angle): | |
611 | self.center = self.center.rot(angle) | |
612 | ||
613 | def P(self, t): | |
614 | '''Return a Point on the Ellipse for t in [0..1]''' | |
615 | x = self.center.x + self.rx * math.cos(2 * math.pi * t) | |
616 | y = self.center.y + self.ry * math.sin(2 * math.pi * t) | |
617 | return Point(x,y) | |
618 | ||
619 | def segments(self, precision=0): | |
620 | if max(self.rx, self.ry) < precision: | |
621 | return [[self.center]] | |
622 | ||
623 | p = [(0,self.P(0)), (1, self.P(1))] | |
624 | d = 2 * max(self.rx, self.ry) | |
625 | ||
626 | while d > precision: | |
627 | for (t1,p1),(t2,p2) in zip(p[:-1],p[1:]): | |
628 | t = t1 + (t2 - t1)/2. | |
629 | d = Segment(p1, p2).pdistance(self.P(t)) | |
630 | p.append((t, self.P(t))) | |
631 | p.sort(key=operator.itemgetter(0)) | |
632 | ||
633 | ret = [x for t,x in p] | |
634 | return [ret] | |
635 | ||
636 | def simplify(self, precision): | |
637 | return self | |
638 | ||
639 | # A circle is a special type of ellipse where rx = ry = radius | |
640 | class Circle(Ellipse): | |
641 | '''SVG <circle>''' | |
642 | # class Circle handles the <circle> tag | |
643 | tag = 'circle' | |
644 | ||
645 | def __init__(self, elt=None): | |
646 | if elt is not None: | |
647 | elt.set('rx', elt.get('r')) | |
648 | elt.set('ry', elt.get('r')) | |
649 | Ellipse.__init__(self, elt) | |
650 | ||
651 | def __repr__(self): | |
652 | return '<Circle ' + self.id + '>' | |
653 | ||
654 | class Rect(Transformable): | |
655 | '''SVG <rect>''' | |
656 | # class Rect handles the <rect> tag | |
657 | tag = 'rect' | |
658 | ||
659 | def __init__(self, elt=None): | |
660 | Transformable.__init__(self, elt) | |
661 | if elt is not None: | |
662 | self.P1 = Point(self.xlength(elt.get('x')), | |
663 | self.ylength(elt.get('y'))) | |
664 | ||
665 | self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), | |
666 | self.P1.y + self.ylength(elt.get('height'))) | |
667 | ||
668 | def __repr__(self): | |
669 | return '<Rect ' + self.id + '>' | |
670 | ||
671 | def bbox(self): | |
672 | '''Bounding box''' | |
673 | xmin = min([p.x for p in (self.P1, self.P2)]) | |
674 | xmax = max([p.x for p in (self.P1, self.P2)]) | |
675 | ymin = min([p.y for p in (self.P1, self.P2)]) | |
676 | ymax = max([p.y for p in (self.P1, self.P2)]) | |
677 | ||
678 | return (Point(xmin,ymin), Point(xmax,ymax)) | |
679 | ||
680 | def transform(self, matrix): | |
681 | self.P1 = self.matrix * self.P1 | |
682 | self.P2 = self.matrix * self.P2 | |
683 | ||
684 | def segments(self, precision=0): | |
685 | # A rectangle is built with a segment going thru 4 points | |
686 | ret = [] | |
687 | Pa = Point(self.P1.x, self.P2.y) | |
688 | Pb = Point(self.P2.x, self.P1.y) | |
689 | ||
690 | ret.append([self.P1, Pa, self.P2, Pb, self.P1]) | |
691 | return ret | |
692 | ||
693 | def simplify(self, precision): | |
694 | return self.segments(precision) | |
695 | ||
696 | class Line(Transformable): | |
697 | '''SVG <line>''' | |
698 | # class Line handles the <line> tag | |
699 | tag = 'line' | |
700 | ||
701 | def __init__(self, elt=None): | |
702 | Transformable.__init__(self, elt) | |
703 | if elt is not None: | |
704 | self.P1 = Point(self.xlength(elt.get('x1')), | |
705 | self.ylength(elt.get('y1'))) | |
706 | self.P2 = Point(self.xlength(elt.get('x2')), | |
707 | self.ylength(elt.get('y2'))) | |
708 | self.segment = Segment(self.P1, self.P2) | |
709 | ||
710 | def __repr__(self): | |
711 | return '<Line ' + self.id + '>' | |
712 | ||
713 | def bbox(self): | |
714 | '''Bounding box''' | |
715 | xmin = min([p.x for p in (self.P1, self.P2)]) | |
716 | xmax = max([p.x for p in (self.P1, self.P2)]) | |
717 | ymin = min([p.y for p in (self.P1, self.P2)]) | |
718 | ymax = max([p.y for p in (self.P1, self.P2)]) | |
719 | ||
720 | return (Point(xmin,ymin), Point(xmax,ymax)) | |
721 | ||
722 | def transform(self, matrix): | |
723 | self.P1 = self.matrix * self.P1 | |
724 | self.P2 = self.matrix * self.P2 | |
725 | self.segment = Segment(self.P1, self.P2) | |
726 | ||
727 | def segments(self, precision=0): | |
728 | return [self.segment.segments()] | |
729 | ||
730 | def simplify(self, precision): | |
731 | return self.segments(precision) | |
732 | ||
733 | # overwrite JSONEncoder for svg classes which have defined a .json() method | |
734 | class JSONEncoder(json.JSONEncoder): | |
735 | def default(self, obj): | |
736 | if not isinstance(obj, tuple(svgClass.values() + [Svg])): | |
737 | return json.JSONEncoder.default(self, obj) | |
738 | ||
739 | if not hasattr(obj, 'json'): | |
740 | return repr(obj) | |
741 | ||
742 | return obj.json() | |
743 | ||
744 | ## Code executed on module load ## | |
745 | ||
746 | # SVG tag handler classes are initialized here | |
747 | # (classes must be defined before) | |
748 | import inspect | |
749 | svgClass = {} | |
750 | # Register all classes with attribute 'tag' in svgClass dict | |
751 | for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): | |
752 | tag = getattr(cls, 'tag', None) | |
753 | if tag: | |
754 | svgClass[svg_ns + tag] = cls | |
755 |