Fri, 28 Jul 2017 16:05:08 +0200
Added module watcher for easier development (automatic module reload)
16 | 1 | #!/usr/bin/env python |
2 | """ | |
3 | simplepath.py | |
4 | functions for digesting paths into a simple list structure | |
5 | ||
6 | Copyright (C) 2005 Aaron Spike, aaron@ekips.org | |
7 | ||
8 | This program is free software; you can redistribute it and/or modify | |
9 | it under the terms of the GNU General Public License as published by | |
10 | the Free Software Foundation; either version 2 of the License, or | |
11 | (at your option) any later version. | |
12 | ||
13 | This program is distributed in the hope that it will be useful, | |
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | GNU General Public License for more details. | |
17 | ||
18 | You should have received a copy of the GNU General Public License | |
19 | along with this program; if not, write to the Free Software | |
20 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
21 | ||
22 | """ | |
23 | import re, math | |
24 | ||
25 | def lexPath(d): | |
26 | """ | |
27 | returns and iterator that breaks path data | |
28 | identifies command and parameter tokens | |
29 | """ | |
30 | offset = 0 | |
31 | length = len(d) | |
32 | delim = re.compile(r'[ \t\r\n,]+') | |
33 | command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]') | |
34 | parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') | |
35 | while 1: | |
36 | m = delim.match(d, offset) | |
37 | if m: | |
38 | offset = m.end() | |
39 | if offset >= length: | |
40 | break | |
41 | m = command.match(d, offset) | |
42 | if m: | |
43 | yield [d[offset:m.end()], True] | |
44 | offset = m.end() | |
45 | continue | |
46 | m = parameter.match(d, offset) | |
47 | if m: | |
48 | yield [d[offset:m.end()], False] | |
49 | offset = m.end() | |
50 | continue | |
51 | #TODO: create new exception | |
52 | raise Exception, 'Invalid path data!' | |
53 | ''' | |
54 | pathdefs = {commandfamily: | |
55 | [ | |
56 | implicitnext, | |
57 | #params, | |
58 | [casts,cast,cast], | |
59 | [coord type,x,y,0] | |
60 | ]} | |
61 | ''' | |
62 | pathdefs = { | |
63 | 'M':['L', 2, [float, float], ['x','y']], | |
64 | 'L':['L', 2, [float, float], ['x','y']], | |
65 | 'H':['H', 1, [float], ['x']], | |
66 | 'V':['V', 1, [float], ['y']], | |
67 | 'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']], | |
68 | 'S':['S', 4, [float, float, float, float], ['x','y','x','y']], | |
69 | 'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']], | |
70 | 'T':['T', 2, [float, float], ['x','y']], | |
71 | 'A':['A', 7, [float, float, float, int, int, float, float], [0,0,0,0,0,'x','y']], | |
72 | 'Z':['L', 0, [], []] | |
73 | } | |
74 | def parsePath(d): | |
75 | """ | |
76 | Parse SVG path and return an array of segments. | |
77 | Removes all shorthand notation. | |
78 | Converts coordinates to absolute. | |
79 | """ | |
80 | retval = [] | |
81 | lexer = lexPath(d) | |
82 | ||
83 | pen = (0.0,0.0) | |
84 | subPathStart = pen | |
85 | lastControl = pen | |
86 | lastCommand = '' | |
87 | ||
88 | while 1: | |
89 | try: | |
90 | token, isCommand = lexer.next() | |
91 | except StopIteration: | |
92 | break | |
93 | params = [] | |
94 | needParam = True | |
95 | if isCommand: | |
96 | if not lastCommand and token.upper() != 'M': | |
97 | raise Exception, 'Invalid path, must begin with moveto.' | |
98 | else: | |
99 | command = token | |
100 | else: | |
101 | #command was omited | |
102 | #use last command's implicit next command | |
103 | needParam = False | |
104 | if lastCommand: | |
105 | if lastCommand.isupper(): | |
106 | command = pathdefs[lastCommand][0] | |
107 | else: | |
108 | command = pathdefs[lastCommand.upper()][0].lower() | |
109 | else: | |
110 | raise Exception, 'Invalid path, no initial command.' | |
111 | numParams = pathdefs[command.upper()][1] | |
112 | while numParams > 0: | |
113 | if needParam: | |
114 | try: | |
115 | token, isCommand = lexer.next() | |
116 | if isCommand: | |
117 | raise Exception, 'Invalid number of parameters' | |
118 | except StopIteration: | |
119 | raise Exception, 'Unexpected end of path' | |
120 | cast = pathdefs[command.upper()][2][-numParams] | |
121 | param = cast(token) | |
122 | if command.islower(): | |
123 | if pathdefs[command.upper()][3][-numParams]=='x': | |
124 | param += pen[0] | |
125 | elif pathdefs[command.upper()][3][-numParams]=='y': | |
126 | param += pen[1] | |
127 | params.append(param) | |
128 | needParam = True | |
129 | numParams -= 1 | |
130 | #segment is now absolute so | |
131 | outputCommand = command.upper() | |
132 | ||
133 | #Flesh out shortcut notation | |
134 | if outputCommand in ('H','V'): | |
135 | if outputCommand == 'H': | |
136 | params.append(pen[1]) | |
137 | if outputCommand == 'V': | |
138 | params.insert(0,pen[0]) | |
139 | outputCommand = 'L' | |
140 | if outputCommand in ('S','T'): | |
141 | params.insert(0,pen[1]+(pen[1]-lastControl[1])) | |
142 | params.insert(0,pen[0]+(pen[0]-lastControl[0])) | |
143 | if outputCommand == 'S': | |
144 | outputCommand = 'C' | |
145 | if outputCommand == 'T': | |
146 | outputCommand = 'Q' | |
147 | ||
148 | #current values become "last" values | |
149 | if outputCommand == 'M': | |
150 | subPathStart = tuple(params[0:2]) | |
151 | pen = subPathStart | |
152 | if outputCommand == 'Z': | |
153 | pen = subPathStart | |
154 | else: | |
155 | pen = tuple(params[-2:]) | |
156 | ||
157 | if outputCommand in ('Q','C'): | |
158 | lastControl = tuple(params[-4:-2]) | |
159 | else: | |
160 | lastControl = pen | |
161 | lastCommand = command | |
162 | ||
163 | retval.append([outputCommand,params]) | |
164 | return retval | |
165 | ||
166 | def formatPath(a): | |
167 | """Format SVG path data from an array""" | |
168 | return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a]) | |
169 | ||
170 | def translatePath(p, x, y): | |
171 | for cmd,params in p: | |
172 | defs = pathdefs[cmd] | |
173 | for i in range(defs[1]): | |
174 | if defs[3][i] == 'x': | |
175 | params[i] += x | |
176 | elif defs[3][i] == 'y': | |
177 | params[i] += y | |
178 | ||
179 | def scalePath(p, x, y): | |
180 | for cmd,params in p: | |
181 | defs = pathdefs[cmd] | |
182 | for i in range(defs[1]): | |
183 | if defs[3][i] == 'x': | |
184 | params[i] *= x | |
185 | elif defs[3][i] == 'y': | |
186 | params[i] *= y | |
187 | ||
188 | def rotatePath(p, a, cx = 0, cy = 0): | |
189 | if a == 0: | |
190 | return p | |
191 | for cmd,params in p: | |
192 | defs = pathdefs[cmd] | |
193 | for i in range(defs[1]): | |
194 | if defs[3][i] == 'x': | |
195 | x = params[i] - cx | |
196 | y = params[i + 1] - cy | |
197 | r = math.sqrt((x**2) + (y**2)) | |
198 | if r != 0: | |
199 | theta = math.atan2(y, x) + a | |
200 | params[i] = (r * math.cos(theta)) + cx | |
201 | params[i + 1] = (r * math.sin(theta)) + cy | |
202 |