├── tests ├── __init__.py ├── W3C_SVG_11_TestSuite │ └── .gitignore ├── testsuite.py ├── parse.py └── README.md ├── .gitignore ├── __init__.py ├── examples ├── README.md ├── circle-transform.svg ├── rect.svg ├── ellipse.svg ├── line.svg ├── circle.svg ├── footprint.svg └── cc-by-sa.svg ├── svg ├── __init__.py ├── geometry.py └── svg.py ├── README.md ├── svg_test.py └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .svg import * 2 | -------------------------------------------------------------------------------- /tests/W3C_SVG_11_TestSuite/.gitignore: -------------------------------------------------------------------------------- 1 | harness/ 2 | images/ 3 | png/ 4 | resources/ 5 | svg/ 6 | svgweb/ 7 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Most of those examples are from [SVGround](SVGround.fr), a great resource for SVG (in French). 2 | -------------------------------------------------------------------------------- /svg/__init__.py: -------------------------------------------------------------------------------- 1 | #__all__ = ['geometry', 'svg'] 2 | 3 | from .svg import * 4 | 5 | def parse(filename): 6 | f = svg.Svg(filename) 7 | return f 8 | 9 | -------------------------------------------------------------------------------- /examples/circle-transform.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/testsuite.py: -------------------------------------------------------------------------------- 1 | import os 2 | import svg 3 | 4 | path = os.path.abspath(os.path.dirname(__file__)) + '/W3C_SVG_11_TestSuite/svg/' 5 | 6 | def test_files(): 7 | for filename in os.listdir(path): 8 | if filename[-3:] == 'svg': 9 | yield svg.parse, path+filename 10 | 11 | if __name__=="__main__": 12 | import nose 13 | nose.main() 14 | -------------------------------------------------------------------------------- /examples/rect.svg: -------------------------------------------------------------------------------- 1 | Mon premier dessin SVG 2 | 3 | -------------------------------------------------------------------------------- /tests/parse.py: -------------------------------------------------------------------------------- 1 | # Parse all W3C SVG testsuite files 2 | # use it with 3 | ## python parse.py | grep ^"No handler" | sort | uniq -c | sort -n 4 | # to get all unhandled elements sorted by occurence. 5 | 6 | import os 7 | import sys 8 | sys.path.append('..') #FIXME 9 | import svg 10 | 11 | path = 'W3C_SVG_11_TestSuite/svg/' 12 | 13 | for f in os.listdir(path): 14 | if os.path.splitext(f)[1] == '.svg': 15 | svg.parse(path + f) 16 | 17 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | SVG Tests 2 | ========= 3 | 4 | W3C SVG test suite 5 | ------------------ 6 | Download the [W3C SVG 1.1 Test suite (14MB)]( http://www.w3.org/Graphics/SVG/Test/20110816/archives/W3C_SVG_11_TestSuite.tar.gz). 7 | 8 | Extract it: 9 | 10 | tar -xvzf W3C_SVG_11_TestSuite.tar.gz -C W3C_SVG_11_TestSuite 11 | 12 | Test is using [Python nose](http://nose.readthedocs.org/en/latest/index.html). Install it for your environment to run the test. 13 | 14 | nosetests testsuite.py 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SVG parser library 2 | ================== 3 | 4 | This is a SVG parser library written in Python. 5 | 6 | Capabilities: 7 | - Parse SVG XML 8 | - apply any transformation (svg transform) 9 | - Explode SVG Path into basic elements (Line, Bezier, ...) 10 | - Interpolate SVG Path as a series of segments 11 | - Able to simplify segments given a precision using Ramer-Douglas-Peucker algorithm 12 | 13 | Not (yet) supported: 14 | - SVG Path Arc ('A') 15 | - Non-linear transformation drawing (SkewX, ...) 16 | 17 | License: GPLv2+ 18 | -------------------------------------------------------------------------------- /examples/ellipse.svg: -------------------------------------------------------------------------------- 1 | Des ellipses de toutes les couleurs avec SVG 2 | 3 | -------------------------------------------------------------------------------- /examples/line.svg: -------------------------------------------------------------------------------- 1 | Des lignes de toutes les couleurs avec SVG 2 | 3 | -------------------------------------------------------------------------------- /examples/circle.svg: -------------------------------------------------------------------------------- 1 | Des cercles, des cercles, encore des cercles… 2 | 3 | 4 | -------------------------------------------------------------------------------- /svg_test.py: -------------------------------------------------------------------------------- 1 | import sys, os, math 2 | import cairo 3 | 4 | import svg 5 | 6 | def draw_with_cairo(cr, drawing): 7 | for d in drawing: 8 | if isinstance(d, svg.Path): 9 | for elt in d.items: 10 | if isinstance(elt, svg.MoveTo): 11 | x,y = elt.dest.coord() 12 | cr.move_to(x,y) 13 | elif isinstance(elt, svg.Line): 14 | x,y = elt.end.coord() 15 | cr.line_to(x,y) 16 | elif isinstance(elt, svg.Bezier): 17 | if elt.dimension == 3: 18 | a,c = elt.pts[1:] 19 | b = c 20 | else: 21 | a,b,c = elt.pts[1:] 22 | cr.curve_to(a.x, a.y, b.x, b.y, c.x, c.y) 23 | if isinstance(d, svg.Circle): 24 | cx, cy = d.center.coord() 25 | cr.move_to(cx+d.radius, cy) 26 | cr.arc(cx, cy, d.radius, 0, 2*math.pi) 27 | if isinstance(d, svg.Rect): 28 | x1,y1 = d.P1.coord() 29 | x2,y2 = d.P2.coord() 30 | width = x2 - x1 31 | height = y2 - y1 32 | cr.rectangle(x1, y1, width, height) 33 | 34 | 35 | def draw_with_segments(cr, drawing): 36 | for d in drawing: 37 | if hasattr(d, 'segments'): 38 | for l in d.segments(1): 39 | x,y = l[0].coord() 40 | cr.move_to(x,y) 41 | for pt in l[1:]: 42 | x,y = pt.coord() 43 | cr.line_to(x,y) 44 | else: 45 | print("Unsupported SVG element") 46 | 47 | f = svg.parse(sys.argv[1]) 48 | 49 | a,b = f.bbox() 50 | 51 | width, height = (a+b).coord() 52 | surface = cairo.SVGSurface("test.svg", width, height) 53 | cr = cairo.Context(surface) 54 | 55 | cr.set_source_rgb(0,0,0) 56 | cr.set_line_width(1) 57 | 58 | #draw_with_cairo(cr, f.flatten()) 59 | draw_with_segments(cr, f.flatten()) 60 | 61 | cr.stroke() 62 | 63 | surface.write_to_png('test.png') 64 | cr.show_page() 65 | surface.finish() 66 | -------------------------------------------------------------------------------- /examples/footprint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /svg/geometry.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > 2 | 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | ''' 18 | This module contains all the geometric classes and functions not directly 19 | related to SVG parsing. It can be reused outside the scope of SVG. 20 | ''' 21 | 22 | import math 23 | import numbers 24 | import operator 25 | 26 | class Point: 27 | def __init__(self, x=None, y=None): 28 | '''A Point is defined either by a tuple/list of length 2 or 29 | by 2 coordinates 30 | >>> Point(1,2) 31 | (1.000,2.000) 32 | >>> Point((1,2)) 33 | (1.000,2.000) 34 | >>> Point([1,2]) 35 | (1.000,2.000) 36 | >>> Point('1', '2') 37 | (1.000,2.000) 38 | >>> Point(('1', None)) 39 | (1.000,0.000) 40 | ''' 41 | if (isinstance(x, tuple) or isinstance(x, list)) and len(x) == 2: 42 | x,y = x 43 | 44 | # Handle empty parameter(s) which should be interpreted as 0 45 | if x is None: x = 0 46 | if y is None: y = 0 47 | 48 | try: 49 | self.x = float(x) 50 | self.y = float(y) 51 | except: 52 | raise TypeError("A Point is defined by 2 numbers or a tuple") 53 | 54 | def __add__(self, other): 55 | '''Add 2 points by adding coordinates. 56 | Try to convert other to Point if necessary 57 | >>> Point(1,2) + Point(3,2) 58 | (4.000,4.000) 59 | >>> Point(1,2) + (3,2) 60 | (4.000,4.000)''' 61 | if not isinstance(other, Point): 62 | try: other = Point(other) 63 | except: return NotImplemented 64 | return Point(self.x + other.x, self.y + other.y) 65 | 66 | def __sub__(self, other): 67 | '''Substract two Points. 68 | >>> Point(1,2) - Point(3,2) 69 | (-2.000,0.000) 70 | ''' 71 | if not isinstance(other, Point): 72 | try: other = Point(other) 73 | except: return NotImplemented 74 | return Point(self.x - other.x, self.y - other.y) 75 | 76 | def __mul__(self, other): 77 | '''Multiply a Point with a constant. 78 | >>> 2 * Point(1,2) 79 | (2.000,4.000) 80 | >>> Point(1,2) * Point(1,2) #doctest:+IGNORE_EXCEPTION_DETAIL 81 | Traceback (most recent call last): 82 | ... 83 | TypeError: 84 | ''' 85 | if not isinstance(other, numbers.Real): 86 | return NotImplemented 87 | return Point(self.x * other, self.y * other) 88 | def __rmul__(self, other): 89 | return self.__mul__(other) 90 | 91 | def __eq__(self, other): 92 | '''Test equality 93 | >>> Point(1,2) == (1,2) 94 | True 95 | >>> Point(1,2) == Point(2,1) 96 | False 97 | ''' 98 | if not isinstance(other, Point): 99 | try: other = Point(other) 100 | except: return NotImplemented 101 | return (self.x == other.x) and (self.y == other.y) 102 | 103 | def __repr__(self): 104 | return '(' + format(self.x,'.3f') + ',' + format( self.y,'.3f') + ')' 105 | 106 | def __str__(self): 107 | return self.__repr__(); 108 | 109 | def coord(self): 110 | '''Return the point tuple (x,y)''' 111 | return (self.x, self.y) 112 | 113 | def length(self): 114 | '''Vector length, Pythagoras theorem''' 115 | return math.sqrt(self.x ** 2 + self.y ** 2) 116 | 117 | def rot(self, angle): 118 | '''Rotate vector [Origin,self] ''' 119 | if not isinstance(angle, Angle): 120 | try: angle = Angle(angle) 121 | except: return NotImplemented 122 | x = self.x * angle.cos - self.y * angle.sin 123 | y = self.x * angle.sin + self.y * angle.cos 124 | return Point(x,y) 125 | 126 | 127 | class Angle: 128 | '''Define a trigonometric angle [of a vector] ''' 129 | def __init__(self, arg): 130 | if isinstance(arg, numbers.Real): 131 | # We precompute sin and cos for rotations 132 | self.angle = arg 133 | self.cos = math.cos(self.angle) 134 | self.sin = math.sin(self.angle) 135 | elif isinstance(arg, Point): 136 | # Point angle is the trigonometric angle of the vector [origin, Point] 137 | pt = arg 138 | try: 139 | self.cos = pt.x/pt.length() 140 | self.sin = pt.y/pt.length() 141 | except ZeroDivisionError: 142 | self.cos = 1 143 | self.sin = 0 144 | 145 | self.angle = math.acos(self.cos) 146 | if self.sin < 0: 147 | self.angle = -self.angle 148 | else: 149 | raise TypeError("Angle is defined by a number or a Point") 150 | 151 | def __neg__(self): 152 | return Angle(Point(self.cos, -self.sin)) 153 | 154 | class Segment: 155 | '''A segment is an object defined by 2 points''' 156 | def __init__(self, start, end): 157 | self.start = start 158 | self.end = end 159 | 160 | def __str__(self): 161 | return 'Segment from ' + str(self.start) + ' to ' + str(self.end) 162 | 163 | def segments(self, precision=0): 164 | ''' Segments is simply the segment start -> end''' 165 | return [self.start, self.end] 166 | 167 | def length(self): 168 | '''Segment length, Pythagoras theorem''' 169 | s = self.end - self.start 170 | return math.sqrt(s.x ** 2 + s.y ** 2) 171 | 172 | def pdistance(self, p): 173 | '''Perpendicular distance between this Segment and a given Point p''' 174 | if not isinstance(p, Point): 175 | return NotImplemented 176 | 177 | if self.start == self.end: 178 | # Distance from a Point to another Point is length of a segment 179 | return Segment(self.start, p).length() 180 | 181 | s = self.end - self.start 182 | if s.x == 0: 183 | # Vertical Segment => pdistance is the difference of abscissa 184 | return abs(self.start.x - p.x) 185 | else: 186 | # That's 2-D perpendicular distance formulae (ref: Wikipedia) 187 | slope = s.y/s.x 188 | # intercept: Crossing with ordinate y-axis 189 | intercept = self.start.y - (slope * self.start.x) 190 | return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1) 191 | 192 | 193 | def bbox(self): 194 | xmin = min(self.start.x, self.end.x) 195 | xmax = max(self.start.x, self.end.x) 196 | ymin = min(self.start.y, self.end.y) 197 | ymax = max(self.start.y, self.end.y) 198 | 199 | return (Point(xmin,ymin),Point(xmax,ymax)) 200 | 201 | def transform(self, matrix): 202 | self.start = matrix * self.start 203 | self.end = matrix * self.end 204 | 205 | def scale(self, ratio): 206 | self.start *= ratio 207 | self.end *= ratio 208 | def translate(self, offset): 209 | self.start += offset 210 | self.end += offset 211 | def rotate(self, angle): 212 | self.start = self.start.rot(angle) 213 | self.end = self.end.rot(angle) 214 | 215 | class Bezier: 216 | '''Bezier curve class 217 | A Bezier curve is defined by its control points 218 | Its dimension is equal to the number of control points 219 | Note that SVG only support dimension 3 and 4 Bezier curve, respectively 220 | Quadratic and Cubic Bezier curve''' 221 | def __init__(self, pts): 222 | self.pts = list(pts) 223 | self.dimension = len(pts) 224 | 225 | def __str__(self): 226 | return 'Bezier' + str(self.dimension) + \ 227 | ' : ' + ", ".join([str(x) for x in self.pts]) 228 | 229 | def control_point(self, n): 230 | if n >= self.dimension: 231 | raise LookupError('Index is larger than Bezier curve dimension') 232 | else: 233 | return self.pts[n] 234 | 235 | def rlength(self): 236 | '''Rough Bezier length: length of control point segments''' 237 | pts = list(self.pts) 238 | l = 0.0 239 | p1 = pts.pop() 240 | while pts: 241 | p2 = pts.pop() 242 | l += Segment(p1, p2).length() 243 | p1 = p2 244 | return l 245 | 246 | def bbox(self): 247 | return self.rbbox() 248 | 249 | def rbbox(self): 250 | '''Rough bounding box: return the bounding box (P1,P2) of the Bezier 251 | _control_ points''' 252 | xmin = min([p.x for p in self.pts]) 253 | xmax = max([p.x for p in self.pts]) 254 | ymin = min([p.y for p in self.pts]) 255 | ymax = max([p.y for p in self.pts]) 256 | 257 | return (Point(xmin,ymin), Point(xmax,ymax)) 258 | 259 | def segments(self, precision=0): 260 | '''Return a polyline approximation ("segments") of the Bezier curve 261 | precision is the minimum significative length of a segment''' 262 | segments = [] 263 | # n is the number of Bezier points to draw according to precision 264 | if precision != 0: 265 | n = int(self.rlength() / precision) + 1 266 | else: 267 | n = 1000 268 | if n < 10: n = 10 269 | if n > 1000 : n = 1000 270 | 271 | for t in range(0, n+1): 272 | segments.append(self._bezierN(float(t)/n)) 273 | return segments 274 | 275 | def _bezier1(self, p0, p1, t): 276 | '''Bezier curve, one dimension 277 | Compute the Point corresponding to a linear Bezier curve between 278 | p0 and p1 at "time" t ''' 279 | pt = p0 + t * (p1 - p0) 280 | return pt 281 | 282 | def _bezierN(self, t): 283 | '''Bezier curve, Nth dimension 284 | Compute the point of the Nth dimension Bezier curve at "time" t''' 285 | # We reduce the N Bezier control points by computing the linear Bezier 286 | # point of each control point segment, creating N-1 control points 287 | # until we reach one single point 288 | res = list(self.pts) 289 | # We store the resulting Bezier points in res[], recursively 290 | for n in range(self.dimension, 1, -1): 291 | # For each control point of nth dimension, 292 | # compute linear Bezier point a t 293 | for i in range(0,n-1): 294 | res[i] = self._bezier1(res[i], res[i+1], t) 295 | return res[0] 296 | 297 | def transform(self, matrix): 298 | self.pts = [matrix * x for x in self.pts] 299 | 300 | def scale(self, ratio): 301 | self.pts = [x * ratio for x in self.pts] 302 | def translate(self, offset): 303 | self.pts = [x + offset for x in self.pts] 304 | def rotate(self, angle): 305 | self.pts = [x.rot(angle) for x in self.pts] 306 | 307 | class MoveTo: 308 | def __init__(self, dest): 309 | self.dest = dest 310 | 311 | def bbox(self): 312 | return (self.dest, self.dest) 313 | 314 | def transform(self, matrix): 315 | self.dest = matrix * self.dest 316 | 317 | def scale(self, ratio): 318 | self.dest *= ratio 319 | def translate(self, offset): 320 | self.dest += offset 321 | def rotate(self, angle): 322 | self.dest = self.dest.rot(angle) 323 | 324 | 325 | def simplify_segment(segment, epsilon): 326 | '''Ramer-Douglas-Peucker algorithm''' 327 | if len(segment) < 3 or epsilon <= 0: 328 | return segment[:] 329 | 330 | l = Segment(segment[0], segment[-1]) # Longest segment 331 | 332 | # Find the furthest point from the segment 333 | index, maxDist = max([(i, l.pdistance(p)) for i,p in enumerate(segment)], 334 | key=operator.itemgetter(1)) 335 | 336 | if maxDist > epsilon: 337 | # Recursively call with segment splited in 2 on its furthest point 338 | r1 = simplify_segment(segment[:index+1], epsilon) 339 | r2 = simplify_segment(segment[index:], epsilon) 340 | # Remove redundant 'middle' Point 341 | return r1[:-1] + r2 342 | else: 343 | return [segment[0], segment[-1]] 344 | -------------------------------------------------------------------------------- /examples/cc-by-sa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 22 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 59 | 65 | 70 | 71 | 74 | 75 | 84 | 85 | 88 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | 100 | 103 | 107 | 108 | 112 | 113 | 114 | 115 | 118 | 122 | 123 | 127 | 128 | 129 | 130 | 133 | 134 | 143 | 144 | 147 | 150 | 151 | 154 | 155 | 156 | 157 | 158 | 159 | 161 | 171 | 172 | 174 | 177 | 178 | 187 | 188 | 189 | 190 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /svg/svg.py: -------------------------------------------------------------------------------- 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 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 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 elements''' 245 | # class Group handles the 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. : 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 (group) 265 | if elt.tag == svg_ns + 'g': 266 | item.append(elt) 267 | 268 | def __repr__(self): 269 | return ': ' + 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 ''' 323 | # class Path handles the 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 '' 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 ''' 503 | # class Ellipse handles the 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 '' 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 ''' 567 | # class Circle handles the 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 '' 578 | 579 | class Rect(Transformable): 580 | '''SVG ''' 581 | # class Rect handles the 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 '' 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 ''' 623 | # class Line handles the 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 '' 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 | 681 | --------------------------------------------------------------------------------