11# SVG Path specification parser
22
3+ from typing import Generator , Tuple , Union
34import re
45from svg .path import path
56
@@ -33,14 +34,14 @@ class InvalidPathError(ValueError):
3334}
3435
3536
36- def strip_array (arg_array ) :
37+ def strip_array (arg_array : bytearray ) -> None :
3738 """Strips whitespace and commas"""
3839 # EBNF wsp:(#x20 | #x9 | #xD | #xA) + comma: 0x2C
3940 while arg_array and arg_array [0 ] in (0x20 , 0x9 , 0xD , 0xA , 0x2C ):
4041 arg_array [0 :1 ] = b""
4142
4243
43- def pop_number (arg_array ) :
44+ def pop_number (arg_array : bytearray ) -> float :
4445 res = FLOAT_RE .search (arg_array )
4546 if not res or not res .group ():
4647 raise InvalidPathError (f"Expected a number, got '{ arg_array } '." )
@@ -53,27 +54,28 @@ def pop_number(arg_array):
5354 return number
5455
5556
56- def pop_unsigned_number (arg_array ) :
57+ def pop_unsigned_number (arg_array : bytearray ) -> float :
5758 number = pop_number (arg_array )
5859 if number < 0 :
5960 raise InvalidPathError (f"Expected a non-negative number, got '{ number } '." )
6061 return number
6162
6263
63- def pop_coordinate_pair (arg_array ) :
64+ def pop_coordinate_pair (arg_array : bytearray ) -> complex :
6465 x = pop_number (arg_array )
6566 y = pop_number (arg_array )
6667 return complex (x , y )
6768
6869
69- def pop_flag (arg_array ) :
70+ def pop_flag (arg_array : bytearray ) -> Union [ bool , None ] :
7071 flag = arg_array [0 ]
7172 arg_array [0 :1 ] = b""
7273 strip_array (arg_array )
7374 if flag == 48 : # ASCII 0
7475 return False
7576 if flag == 49 : # ASCII 1
7677 return True
78+ return None
7779
7880
7981FIELD_POPPERS = {
@@ -84,9 +86,9 @@ def pop_flag(arg_array):
8486}
8587
8688
87- def _commandify_path (pathdef ) :
89+ def _commandify_path (pathdef : str ) -> Generator [ Tuple [ str , ...], None , None ] :
8890 """Splits path into commands and arguments"""
89- token = None
91+ token : Union [ Tuple [ str , ...], None ] = None
9092 for x in COMMAND_RE .split (pathdef ):
9193 x = x .strip ()
9294 if x in COMMANDS :
@@ -101,10 +103,14 @@ def _commandify_path(pathdef):
101103 if token is None :
102104 raise InvalidPathError (f"Path does not start with a command: { pathdef } " )
103105 token += (x ,)
104- yield token
106+ # Logically token cannot be None, but mypy cannot deduce this.
107+ if token is not None :
108+ yield token
105109
106110
107- def _tokenize_path (pathdef ):
111+ def _tokenize_path (
112+ pathdef : str ,
113+ ) -> Generator [Tuple [Union [str , complex , float , bool , None ], ...], None , None ]:
108114 for command , args in _commandify_path (pathdef ):
109115 # Shortcut this for the close command, that doesn't have arguments:
110116 if command in ("z" , "Z" ):
@@ -138,18 +144,20 @@ def _tokenize_path(pathdef):
138144 command = "L"
139145
140146
141- def parse_path (pathdef ) :
147+ def parse_path (pathdef : str ) -> path . Path :
142148 segments = path .Path ()
143149 start_pos = None
144- last_command = None
145- current_pos = 0
150+ last_command = "No last command"
151+ current_pos = 0j
146152
147153 for token in _tokenize_path (pathdef ):
148154 command = token [0 ]
155+ assert isinstance (command , str )
149156 relative = command .islower ()
150157 command = command .upper ()
151158 if command == "M" :
152159 pos = token [1 ]
160+ assert isinstance (pos , complex )
153161 if relative :
154162 current_pos += pos
155163 else :
@@ -160,18 +168,21 @@ def parse_path(pathdef):
160168 elif command == "Z" :
161169 # For Close commands the "relative" argument just preserves case,
162170 # it has no different in behavior.
171+ assert isinstance (start_pos , complex )
163172 segments .append (path .Close (current_pos , start_pos , relative = relative ))
164173 current_pos = start_pos
165174
166175 elif command == "L" :
167176 pos = token [1 ]
177+ assert isinstance (pos , complex )
168178 if relative :
169179 pos += current_pos
170180 segments .append (path .Line (current_pos , pos , relative = relative ))
171181 current_pos = pos
172182
173183 elif command == "H" :
174184 hpos = token [1 ]
185+ assert isinstance (hpos , float )
175186 if relative :
176187 hpos += current_pos .real
177188 pos = complex (hpos , current_pos .imag )
@@ -182,6 +193,7 @@ def parse_path(pathdef):
182193
183194 elif command == "V" :
184195 vpos = token [1 ]
196+ assert isinstance (vpos , float )
185197 if relative :
186198 vpos += current_pos .imag
187199 pos = complex (current_pos .real , vpos )
@@ -192,8 +204,11 @@ def parse_path(pathdef):
192204
193205 elif command == "C" :
194206 control1 = token [1 ]
207+ assert isinstance (control1 , complex )
195208 control2 = token [2 ]
209+ assert isinstance (control2 , complex )
196210 end = token [3 ]
211+ assert isinstance (end , complex )
197212
198213 if relative :
199214 control1 += current_pos
@@ -211,7 +226,9 @@ def parse_path(pathdef):
211226 # Smooth curve. First control point is the "reflection" of
212227 # the second control point in the previous path.
213228 control2 = token [1 ]
229+ assert isinstance (control2 , complex )
214230 end = token [2 ]
231+ assert isinstance (end , complex )
215232
216233 if relative :
217234 control2 += current_pos
@@ -221,6 +238,7 @@ def parse_path(pathdef):
221238 # The first control point is assumed to be the reflection of
222239 # the second control point on the previous command relative
223240 # to the current point.
241+ assert isinstance (segments [- 1 ], path .CubicBezier )
224242 control1 = current_pos + current_pos - segments [- 1 ].control2
225243 else :
226244 # If there is no previous command or if the previous command
@@ -237,7 +255,9 @@ def parse_path(pathdef):
237255
238256 elif command == "Q" :
239257 control = token [1 ]
258+ assert isinstance (control , complex )
240259 end = token [2 ]
260+ assert isinstance (end , complex )
241261
242262 if relative :
243263 control += current_pos
@@ -252,6 +272,7 @@ def parse_path(pathdef):
252272 # Smooth curve. Control point is the "reflection" of
253273 # the second control point in the previous path.
254274 end = token [1 ]
275+ assert isinstance (end , complex )
255276
256277 if relative :
257278 end += current_pos
@@ -260,6 +281,7 @@ def parse_path(pathdef):
260281 # The control point is assumed to be the reflection of
261282 # the control point on the previous command relative
262283 # to the current point.
284+ assert isinstance (segments [- 1 ], path .QuadraticBezier )
263285 control = current_pos + current_pos - segments [- 1 ].control
264286 else :
265287 # If there is no previous command or if the previous command
@@ -277,11 +299,17 @@ def parse_path(pathdef):
277299 elif command == "A" :
278300 # For some reason I implemented the Arc with a complex radius.
279301 # That doesn't really make much sense, but... *shrugs*
302+ assert isinstance (token [1 ], float )
303+ assert isinstance (token [2 ], float )
280304 radius = complex (token [1 ], token [2 ])
281305 rotation = token [3 ]
306+ assert isinstance (rotation , float )
282307 arc = token [4 ]
308+ assert isinstance (arc , (bool , int ))
283309 sweep = token [5 ]
310+ assert isinstance (sweep , (bool , int ))
284311 end = token [6 ]
312+ assert isinstance (end , complex )
285313
286314 if relative :
287315 end += current_pos
0 commit comments