1 # SVG Path specification parser. 2 # This is an adaptation from 'svg.path' by Lennart Regebro (@regebro), 3 # modified so that the parser takes a FontTools Pen object instead of 4 # returning a list of svg.path Path objects. 5 # The original code can be found at: 6 # https://github.com/regebro/svg.path/blob/4f9b6e3/src/svg/path/parser.py 7 # Copyright (c) 2013-2014 Lennart Regebro 8 # License: MIT 9 10 from __future__ import ( 11 print_function, division, absolute_import, unicode_literals) 12 from fontTools.misc.py23 import * 13 from .arc import EllipticalArc 14 import re 15 16 17 COMMANDS = set('MmZzLlHhVvCcSsQqTtAa') 18 UPPERCASE = set('MZLHVCSQTA') 19 20 COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])") 21 FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?") 22 23 24 def _tokenize_path(pathdef): 25 for x in COMMAND_RE.split(pathdef): 26 if x in COMMANDS: 27 yield x 28 for token in FLOAT_RE.findall(x): 29 yield token 30 31 32 def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc): 33 """ Parse SVG path definition (i.e. "d" attribute of <path> elements) 34 and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath 35 methods. 36 37 If 'current_pos' (2-float tuple) is provided, the initial moveTo will 38 be relative to that instead being absolute. 39 40 If the pen has an "arcTo" method, it is called with the original values 41 of the elliptical arc curve commands: 42 43 pen.arcTo(rx, ry, rotation, arc_large, arc_sweep, (x, y)) 44 45 Otherwise, the arcs are approximated by series of cubic Bezier segments 46 ("curveTo"), one every 90 degrees. 47 """ 48 # In the SVG specs, initial movetos are absolute, even if 49 # specified as 'm'. This is the default behavior here as well. 50 # But if you pass in a current_pos variable, the initial moveto 51 # will be relative to that current_pos. This is useful. 52 current_pos = complex(*current_pos) 53 54 elements = list(_tokenize_path(pathdef)) 55 # Reverse for easy use of .pop() 56 elements.reverse() 57 58 start_pos = None 59 command = None 60 last_control = None 61 62 have_arcTo = hasattr(pen, "arcTo") 63 64 while elements: 65 66 if elements[-1] in COMMANDS: 67 # New command. 68 last_command = command # Used by S and T 69 command = elements.pop() 70 absolute = command in UPPERCASE 71 command = command.upper() 72 else: 73 # If this element starts with numbers, it is an implicit command 74 # and we don't change the command. Check that it's allowed: 75 if command is None: 76 raise ValueError("Unallowed implicit command in %s, position %s" % ( 77 pathdef, len(pathdef.split()) - len(elements))) 78 last_command = command # Used by S and T 79 80 if command == 'M': 81 # Moveto command. 82 x = elements.pop() 83 y = elements.pop() 84 pos = float(x) + float(y) * 1j 85 if absolute: 86 current_pos = pos 87 else: 88 current_pos += pos 89 90 # M is not preceded by Z; it's an open subpath 91 if start_pos is not None: 92 pen.endPath() 93 94 pen.moveTo((current_pos.real, current_pos.imag)) 95 96 # when M is called, reset start_pos 97 # This behavior of Z is defined in svg spec: 98 # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand 99 start_pos = current_pos 100 101 # Implicit moveto commands are treated as lineto commands. 102 # So we set command to lineto here, in case there are 103 # further implicit commands after this moveto. 104 command = 'L' 105 106 elif command == 'Z': 107 # Close path 108 if current_pos != start_pos: 109 pen.lineTo((start_pos.real, start_pos.imag)) 110 pen.closePath() 111 current_pos = start_pos 112 start_pos = None 113 command = None # You can't have implicit commands after closing. 114 115 elif command == 'L': 116 x = elements.pop() 117 y = elements.pop() 118 pos = float(x) + float(y) * 1j 119 if not absolute: 120 pos += current_pos 121 pen.lineTo((pos.real, pos.imag)) 122 current_pos = pos 123 124 elif command == 'H': 125 x = elements.pop() 126 pos = float(x) + current_pos.imag * 1j 127 if not absolute: 128 pos += current_pos.real 129 pen.lineTo((pos.real, pos.imag)) 130 current_pos = pos 131 132 elif command == 'V': 133 y = elements.pop() 134 pos = current_pos.real + float(y) * 1j 135 if not absolute: 136 pos += current_pos.imag * 1j 137 pen.lineTo((pos.real, pos.imag)) 138 current_pos = pos 139 140 elif command == 'C': 141 control1 = float(elements.pop()) + float(elements.pop()) * 1j 142 control2 = float(elements.pop()) + float(elements.pop()) * 1j 143 end = float(elements.pop()) + float(elements.pop()) * 1j 144 145 if not absolute: 146 control1 += current_pos 147 control2 += current_pos 148 end += current_pos 149 150 pen.curveTo((control1.real, control1.imag), 151 (control2.real, control2.imag), 152 (end.real, end.imag)) 153 current_pos = end 154 last_control = control2 155 156 elif command == 'S': 157 # Smooth curve. First control point is the "reflection" of 158 # the second control point in the previous path. 159 160 if last_command not in 'CS': 161 # If there is no previous command or if the previous command 162 # was not an C, c, S or s, assume the first control point is 163 # coincident with the current point. 164 control1 = current_pos 165 else: 166 # The first control point is assumed to be the reflection of 167 # the second control point on the previous command relative 168 # to the current point. 169 control1 = current_pos + current_pos - last_control 170 171 control2 = float(elements.pop()) + float(elements.pop()) * 1j 172 end = float(elements.pop()) + float(elements.pop()) * 1j 173 174 if not absolute: 175 control2 += current_pos 176 end += current_pos 177 178 pen.curveTo((control1.real, control1.imag), 179 (control2.real, control2.imag), 180 (end.real, end.imag)) 181 current_pos = end 182 last_control = control2 183 184 elif command == 'Q': 185 control = float(elements.pop()) + float(elements.pop()) * 1j 186 end = float(elements.pop()) + float(elements.pop()) * 1j 187 188 if not absolute: 189 control += current_pos 190 end += current_pos 191 192 pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) 193 current_pos = end 194 last_control = control 195 196 elif command == 'T': 197 # Smooth curve. Control point is the "reflection" of 198 # the second control point in the previous path. 199 200 if last_command not in 'QT': 201 # If there is no previous command or if the previous command 202 # was not an Q, q, T or t, assume the first control point is 203 # coincident with the current point. 204 control = current_pos 205 else: 206 # The control point is assumed to be the reflection of 207 # the control point on the previous command relative 208 # to the current point. 209 control = current_pos + current_pos - last_control 210 211 end = float(elements.pop()) + float(elements.pop()) * 1j 212 213 if not absolute: 214 end += current_pos 215 216 pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) 217 current_pos = end 218 last_control = control 219 220 elif command == 'A': 221 rx = float(elements.pop()) 222 ry = float(elements.pop()) 223 rotation = float(elements.pop()) 224 arc_large = bool(int(elements.pop())) 225 arc_sweep = bool(int(elements.pop())) 226 end = float(elements.pop()) + float(elements.pop()) * 1j 227 228 if not absolute: 229 end += current_pos 230 231 # if the pen supports arcs, pass the values unchanged, otherwise 232 # approximate the arc with a series of cubic bezier curves 233 if have_arcTo: 234 pen.arcTo( 235 rx, 236 ry, 237 rotation, 238 arc_large, 239 arc_sweep, 240 (end.real, end.imag), 241 ) 242 else: 243 arc = arc_class( 244 current_pos, rx, ry, rotation, arc_large, arc_sweep, end 245 ) 246 arc.draw(pen) 247 248 current_pos = end 249 250 # no final Z command, it's an open path 251 if start_pos is not None: 252 pen.endPath() 253