Tue, 26 Sep 2017 19:08:02 +0200
SVG options: offset, original scale
SVG Bugfix: "scale" does now really fit to dimensions
2 | 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]] |