1*e1fe3e4aSElliott Hughes# SVG Path specification parser. 2*e1fe3e4aSElliott Hughes# This is an adaptation from 'svg.path' by Lennart Regebro (@regebro), 3*e1fe3e4aSElliott Hughes# modified so that the parser takes a FontTools Pen object instead of 4*e1fe3e4aSElliott Hughes# returning a list of svg.path Path objects. 5*e1fe3e4aSElliott Hughes# The original code can be found at: 6*e1fe3e4aSElliott Hughes# https://github.com/regebro/svg.path/blob/4f9b6e3/src/svg/path/parser.py 7*e1fe3e4aSElliott Hughes# Copyright (c) 2013-2014 Lennart Regebro 8*e1fe3e4aSElliott Hughes# License: MIT 9*e1fe3e4aSElliott Hughes 10*e1fe3e4aSElliott Hughesfrom .arc import EllipticalArc 11*e1fe3e4aSElliott Hughesimport re 12*e1fe3e4aSElliott Hughes 13*e1fe3e4aSElliott Hughes 14*e1fe3e4aSElliott HughesCOMMANDS = set("MmZzLlHhVvCcSsQqTtAa") 15*e1fe3e4aSElliott HughesARC_COMMANDS = set("Aa") 16*e1fe3e4aSElliott HughesUPPERCASE = set("MZLHVCSQTA") 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott HughesCOMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])") 19*e1fe3e4aSElliott Hughes 20*e1fe3e4aSElliott Hughes# https://www.w3.org/TR/css-syntax-3/#number-token-diagram 21*e1fe3e4aSElliott Hughes# but -6.e-5 will be tokenized as "-6" then "-5" and confuse parsing 22*e1fe3e4aSElliott HughesFLOAT_RE = re.compile( 23*e1fe3e4aSElliott Hughes r"[-+]?" # optional sign 24*e1fe3e4aSElliott Hughes r"(?:" 25*e1fe3e4aSElliott Hughes r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?" # int/float 26*e1fe3e4aSElliott Hughes r"|" 27*e1fe3e4aSElliott Hughes r"(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)" # float with leading dot (e.g. '.42') 28*e1fe3e4aSElliott Hughes r")" 29*e1fe3e4aSElliott Hughes) 30*e1fe3e4aSElliott HughesBOOL_RE = re.compile("^[01]") 31*e1fe3e4aSElliott HughesSEPARATOR_RE = re.compile(f"[, \t]") 32*e1fe3e4aSElliott Hughes 33*e1fe3e4aSElliott Hughes 34*e1fe3e4aSElliott Hughesdef _tokenize_path(pathdef): 35*e1fe3e4aSElliott Hughes arc_cmd = None 36*e1fe3e4aSElliott Hughes for x in COMMAND_RE.split(pathdef): 37*e1fe3e4aSElliott Hughes if x in COMMANDS: 38*e1fe3e4aSElliott Hughes arc_cmd = x if x in ARC_COMMANDS else None 39*e1fe3e4aSElliott Hughes yield x 40*e1fe3e4aSElliott Hughes continue 41*e1fe3e4aSElliott Hughes 42*e1fe3e4aSElliott Hughes if arc_cmd: 43*e1fe3e4aSElliott Hughes try: 44*e1fe3e4aSElliott Hughes yield from _tokenize_arc_arguments(x) 45*e1fe3e4aSElliott Hughes except ValueError as e: 46*e1fe3e4aSElliott Hughes raise ValueError(f"Invalid arc command: '{arc_cmd}{x}'") from e 47*e1fe3e4aSElliott Hughes else: 48*e1fe3e4aSElliott Hughes for token in FLOAT_RE.findall(x): 49*e1fe3e4aSElliott Hughes yield token 50*e1fe3e4aSElliott Hughes 51*e1fe3e4aSElliott Hughes 52*e1fe3e4aSElliott HughesARC_ARGUMENT_TYPES = ( 53*e1fe3e4aSElliott Hughes ("rx", FLOAT_RE), 54*e1fe3e4aSElliott Hughes ("ry", FLOAT_RE), 55*e1fe3e4aSElliott Hughes ("x-axis-rotation", FLOAT_RE), 56*e1fe3e4aSElliott Hughes ("large-arc-flag", BOOL_RE), 57*e1fe3e4aSElliott Hughes ("sweep-flag", BOOL_RE), 58*e1fe3e4aSElliott Hughes ("x", FLOAT_RE), 59*e1fe3e4aSElliott Hughes ("y", FLOAT_RE), 60*e1fe3e4aSElliott Hughes) 61*e1fe3e4aSElliott Hughes 62*e1fe3e4aSElliott Hughes 63*e1fe3e4aSElliott Hughesdef _tokenize_arc_arguments(arcdef): 64*e1fe3e4aSElliott Hughes raw_args = [s for s in SEPARATOR_RE.split(arcdef) if s] 65*e1fe3e4aSElliott Hughes if not raw_args: 66*e1fe3e4aSElliott Hughes raise ValueError(f"Not enough arguments: '{arcdef}'") 67*e1fe3e4aSElliott Hughes raw_args.reverse() 68*e1fe3e4aSElliott Hughes 69*e1fe3e4aSElliott Hughes i = 0 70*e1fe3e4aSElliott Hughes while raw_args: 71*e1fe3e4aSElliott Hughes arg = raw_args.pop() 72*e1fe3e4aSElliott Hughes 73*e1fe3e4aSElliott Hughes name, pattern = ARC_ARGUMENT_TYPES[i] 74*e1fe3e4aSElliott Hughes match = pattern.search(arg) 75*e1fe3e4aSElliott Hughes if not match: 76*e1fe3e4aSElliott Hughes raise ValueError(f"Invalid argument for '{name}' parameter: {arg!r}") 77*e1fe3e4aSElliott Hughes 78*e1fe3e4aSElliott Hughes j, k = match.span() 79*e1fe3e4aSElliott Hughes yield arg[j:k] 80*e1fe3e4aSElliott Hughes arg = arg[k:] 81*e1fe3e4aSElliott Hughes 82*e1fe3e4aSElliott Hughes if arg: 83*e1fe3e4aSElliott Hughes raw_args.append(arg) 84*e1fe3e4aSElliott Hughes 85*e1fe3e4aSElliott Hughes # wrap around every 7 consecutive arguments 86*e1fe3e4aSElliott Hughes if i == 6: 87*e1fe3e4aSElliott Hughes i = 0 88*e1fe3e4aSElliott Hughes else: 89*e1fe3e4aSElliott Hughes i += 1 90*e1fe3e4aSElliott Hughes 91*e1fe3e4aSElliott Hughes if i != 0: 92*e1fe3e4aSElliott Hughes raise ValueError(f"Not enough arguments: '{arcdef}'") 93*e1fe3e4aSElliott Hughes 94*e1fe3e4aSElliott Hughes 95*e1fe3e4aSElliott Hughesdef parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc): 96*e1fe3e4aSElliott Hughes """Parse SVG path definition (i.e. "d" attribute of <path> elements) 97*e1fe3e4aSElliott Hughes and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath 98*e1fe3e4aSElliott Hughes methods. 99*e1fe3e4aSElliott Hughes 100*e1fe3e4aSElliott Hughes If 'current_pos' (2-float tuple) is provided, the initial moveTo will 101*e1fe3e4aSElliott Hughes be relative to that instead being absolute. 102*e1fe3e4aSElliott Hughes 103*e1fe3e4aSElliott Hughes If the pen has an "arcTo" method, it is called with the original values 104*e1fe3e4aSElliott Hughes of the elliptical arc curve commands: 105*e1fe3e4aSElliott Hughes 106*e1fe3e4aSElliott Hughes pen.arcTo(rx, ry, rotation, arc_large, arc_sweep, (x, y)) 107*e1fe3e4aSElliott Hughes 108*e1fe3e4aSElliott Hughes Otherwise, the arcs are approximated by series of cubic Bezier segments 109*e1fe3e4aSElliott Hughes ("curveTo"), one every 90 degrees. 110*e1fe3e4aSElliott Hughes """ 111*e1fe3e4aSElliott Hughes # In the SVG specs, initial movetos are absolute, even if 112*e1fe3e4aSElliott Hughes # specified as 'm'. This is the default behavior here as well. 113*e1fe3e4aSElliott Hughes # But if you pass in a current_pos variable, the initial moveto 114*e1fe3e4aSElliott Hughes # will be relative to that current_pos. This is useful. 115*e1fe3e4aSElliott Hughes current_pos = complex(*current_pos) 116*e1fe3e4aSElliott Hughes 117*e1fe3e4aSElliott Hughes elements = list(_tokenize_path(pathdef)) 118*e1fe3e4aSElliott Hughes # Reverse for easy use of .pop() 119*e1fe3e4aSElliott Hughes elements.reverse() 120*e1fe3e4aSElliott Hughes 121*e1fe3e4aSElliott Hughes start_pos = None 122*e1fe3e4aSElliott Hughes command = None 123*e1fe3e4aSElliott Hughes last_control = None 124*e1fe3e4aSElliott Hughes 125*e1fe3e4aSElliott Hughes have_arcTo = hasattr(pen, "arcTo") 126*e1fe3e4aSElliott Hughes 127*e1fe3e4aSElliott Hughes while elements: 128*e1fe3e4aSElliott Hughes if elements[-1] in COMMANDS: 129*e1fe3e4aSElliott Hughes # New command. 130*e1fe3e4aSElliott Hughes last_command = command # Used by S and T 131*e1fe3e4aSElliott Hughes command = elements.pop() 132*e1fe3e4aSElliott Hughes absolute = command in UPPERCASE 133*e1fe3e4aSElliott Hughes command = command.upper() 134*e1fe3e4aSElliott Hughes else: 135*e1fe3e4aSElliott Hughes # If this element starts with numbers, it is an implicit command 136*e1fe3e4aSElliott Hughes # and we don't change the command. Check that it's allowed: 137*e1fe3e4aSElliott Hughes if command is None: 138*e1fe3e4aSElliott Hughes raise ValueError( 139*e1fe3e4aSElliott Hughes "Unallowed implicit command in %s, position %s" 140*e1fe3e4aSElliott Hughes % (pathdef, len(pathdef.split()) - len(elements)) 141*e1fe3e4aSElliott Hughes ) 142*e1fe3e4aSElliott Hughes last_command = command # Used by S and T 143*e1fe3e4aSElliott Hughes 144*e1fe3e4aSElliott Hughes if command == "M": 145*e1fe3e4aSElliott Hughes # Moveto command. 146*e1fe3e4aSElliott Hughes x = elements.pop() 147*e1fe3e4aSElliott Hughes y = elements.pop() 148*e1fe3e4aSElliott Hughes pos = float(x) + float(y) * 1j 149*e1fe3e4aSElliott Hughes if absolute: 150*e1fe3e4aSElliott Hughes current_pos = pos 151*e1fe3e4aSElliott Hughes else: 152*e1fe3e4aSElliott Hughes current_pos += pos 153*e1fe3e4aSElliott Hughes 154*e1fe3e4aSElliott Hughes # M is not preceded by Z; it's an open subpath 155*e1fe3e4aSElliott Hughes if start_pos is not None: 156*e1fe3e4aSElliott Hughes pen.endPath() 157*e1fe3e4aSElliott Hughes 158*e1fe3e4aSElliott Hughes pen.moveTo((current_pos.real, current_pos.imag)) 159*e1fe3e4aSElliott Hughes 160*e1fe3e4aSElliott Hughes # when M is called, reset start_pos 161*e1fe3e4aSElliott Hughes # This behavior of Z is defined in svg spec: 162*e1fe3e4aSElliott Hughes # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand 163*e1fe3e4aSElliott Hughes start_pos = current_pos 164*e1fe3e4aSElliott Hughes 165*e1fe3e4aSElliott Hughes # Implicit moveto commands are treated as lineto commands. 166*e1fe3e4aSElliott Hughes # So we set command to lineto here, in case there are 167*e1fe3e4aSElliott Hughes # further implicit commands after this moveto. 168*e1fe3e4aSElliott Hughes command = "L" 169*e1fe3e4aSElliott Hughes 170*e1fe3e4aSElliott Hughes elif command == "Z": 171*e1fe3e4aSElliott Hughes # Close path 172*e1fe3e4aSElliott Hughes if current_pos != start_pos: 173*e1fe3e4aSElliott Hughes pen.lineTo((start_pos.real, start_pos.imag)) 174*e1fe3e4aSElliott Hughes pen.closePath() 175*e1fe3e4aSElliott Hughes current_pos = start_pos 176*e1fe3e4aSElliott Hughes start_pos = None 177*e1fe3e4aSElliott Hughes command = None # You can't have implicit commands after closing. 178*e1fe3e4aSElliott Hughes 179*e1fe3e4aSElliott Hughes elif command == "L": 180*e1fe3e4aSElliott Hughes x = elements.pop() 181*e1fe3e4aSElliott Hughes y = elements.pop() 182*e1fe3e4aSElliott Hughes pos = float(x) + float(y) * 1j 183*e1fe3e4aSElliott Hughes if not absolute: 184*e1fe3e4aSElliott Hughes pos += current_pos 185*e1fe3e4aSElliott Hughes pen.lineTo((pos.real, pos.imag)) 186*e1fe3e4aSElliott Hughes current_pos = pos 187*e1fe3e4aSElliott Hughes 188*e1fe3e4aSElliott Hughes elif command == "H": 189*e1fe3e4aSElliott Hughes x = elements.pop() 190*e1fe3e4aSElliott Hughes pos = float(x) + current_pos.imag * 1j 191*e1fe3e4aSElliott Hughes if not absolute: 192*e1fe3e4aSElliott Hughes pos += current_pos.real 193*e1fe3e4aSElliott Hughes pen.lineTo((pos.real, pos.imag)) 194*e1fe3e4aSElliott Hughes current_pos = pos 195*e1fe3e4aSElliott Hughes 196*e1fe3e4aSElliott Hughes elif command == "V": 197*e1fe3e4aSElliott Hughes y = elements.pop() 198*e1fe3e4aSElliott Hughes pos = current_pos.real + float(y) * 1j 199*e1fe3e4aSElliott Hughes if not absolute: 200*e1fe3e4aSElliott Hughes pos += current_pos.imag * 1j 201*e1fe3e4aSElliott Hughes pen.lineTo((pos.real, pos.imag)) 202*e1fe3e4aSElliott Hughes current_pos = pos 203*e1fe3e4aSElliott Hughes 204*e1fe3e4aSElliott Hughes elif command == "C": 205*e1fe3e4aSElliott Hughes control1 = float(elements.pop()) + float(elements.pop()) * 1j 206*e1fe3e4aSElliott Hughes control2 = float(elements.pop()) + float(elements.pop()) * 1j 207*e1fe3e4aSElliott Hughes end = float(elements.pop()) + float(elements.pop()) * 1j 208*e1fe3e4aSElliott Hughes 209*e1fe3e4aSElliott Hughes if not absolute: 210*e1fe3e4aSElliott Hughes control1 += current_pos 211*e1fe3e4aSElliott Hughes control2 += current_pos 212*e1fe3e4aSElliott Hughes end += current_pos 213*e1fe3e4aSElliott Hughes 214*e1fe3e4aSElliott Hughes pen.curveTo( 215*e1fe3e4aSElliott Hughes (control1.real, control1.imag), 216*e1fe3e4aSElliott Hughes (control2.real, control2.imag), 217*e1fe3e4aSElliott Hughes (end.real, end.imag), 218*e1fe3e4aSElliott Hughes ) 219*e1fe3e4aSElliott Hughes current_pos = end 220*e1fe3e4aSElliott Hughes last_control = control2 221*e1fe3e4aSElliott Hughes 222*e1fe3e4aSElliott Hughes elif command == "S": 223*e1fe3e4aSElliott Hughes # Smooth curve. First control point is the "reflection" of 224*e1fe3e4aSElliott Hughes # the second control point in the previous path. 225*e1fe3e4aSElliott Hughes 226*e1fe3e4aSElliott Hughes if last_command not in "CS": 227*e1fe3e4aSElliott Hughes # If there is no previous command or if the previous command 228*e1fe3e4aSElliott Hughes # was not an C, c, S or s, assume the first control point is 229*e1fe3e4aSElliott Hughes # coincident with the current point. 230*e1fe3e4aSElliott Hughes control1 = current_pos 231*e1fe3e4aSElliott Hughes else: 232*e1fe3e4aSElliott Hughes # The first control point is assumed to be the reflection of 233*e1fe3e4aSElliott Hughes # the second control point on the previous command relative 234*e1fe3e4aSElliott Hughes # to the current point. 235*e1fe3e4aSElliott Hughes control1 = current_pos + current_pos - last_control 236*e1fe3e4aSElliott Hughes 237*e1fe3e4aSElliott Hughes control2 = float(elements.pop()) + float(elements.pop()) * 1j 238*e1fe3e4aSElliott Hughes end = float(elements.pop()) + float(elements.pop()) * 1j 239*e1fe3e4aSElliott Hughes 240*e1fe3e4aSElliott Hughes if not absolute: 241*e1fe3e4aSElliott Hughes control2 += current_pos 242*e1fe3e4aSElliott Hughes end += current_pos 243*e1fe3e4aSElliott Hughes 244*e1fe3e4aSElliott Hughes pen.curveTo( 245*e1fe3e4aSElliott Hughes (control1.real, control1.imag), 246*e1fe3e4aSElliott Hughes (control2.real, control2.imag), 247*e1fe3e4aSElliott Hughes (end.real, end.imag), 248*e1fe3e4aSElliott Hughes ) 249*e1fe3e4aSElliott Hughes current_pos = end 250*e1fe3e4aSElliott Hughes last_control = control2 251*e1fe3e4aSElliott Hughes 252*e1fe3e4aSElliott Hughes elif command == "Q": 253*e1fe3e4aSElliott Hughes control = float(elements.pop()) + float(elements.pop()) * 1j 254*e1fe3e4aSElliott Hughes end = float(elements.pop()) + float(elements.pop()) * 1j 255*e1fe3e4aSElliott Hughes 256*e1fe3e4aSElliott Hughes if not absolute: 257*e1fe3e4aSElliott Hughes control += current_pos 258*e1fe3e4aSElliott Hughes end += current_pos 259*e1fe3e4aSElliott Hughes 260*e1fe3e4aSElliott Hughes pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) 261*e1fe3e4aSElliott Hughes current_pos = end 262*e1fe3e4aSElliott Hughes last_control = control 263*e1fe3e4aSElliott Hughes 264*e1fe3e4aSElliott Hughes elif command == "T": 265*e1fe3e4aSElliott Hughes # Smooth curve. Control point is the "reflection" of 266*e1fe3e4aSElliott Hughes # the second control point in the previous path. 267*e1fe3e4aSElliott Hughes 268*e1fe3e4aSElliott Hughes if last_command not in "QT": 269*e1fe3e4aSElliott Hughes # If there is no previous command or if the previous command 270*e1fe3e4aSElliott Hughes # was not an Q, q, T or t, assume the first control point is 271*e1fe3e4aSElliott Hughes # coincident with the current point. 272*e1fe3e4aSElliott Hughes control = current_pos 273*e1fe3e4aSElliott Hughes else: 274*e1fe3e4aSElliott Hughes # The control point is assumed to be the reflection of 275*e1fe3e4aSElliott Hughes # the control point on the previous command relative 276*e1fe3e4aSElliott Hughes # to the current point. 277*e1fe3e4aSElliott Hughes control = current_pos + current_pos - last_control 278*e1fe3e4aSElliott Hughes 279*e1fe3e4aSElliott Hughes end = float(elements.pop()) + float(elements.pop()) * 1j 280*e1fe3e4aSElliott Hughes 281*e1fe3e4aSElliott Hughes if not absolute: 282*e1fe3e4aSElliott Hughes end += current_pos 283*e1fe3e4aSElliott Hughes 284*e1fe3e4aSElliott Hughes pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) 285*e1fe3e4aSElliott Hughes current_pos = end 286*e1fe3e4aSElliott Hughes last_control = control 287*e1fe3e4aSElliott Hughes 288*e1fe3e4aSElliott Hughes elif command == "A": 289*e1fe3e4aSElliott Hughes rx = abs(float(elements.pop())) 290*e1fe3e4aSElliott Hughes ry = abs(float(elements.pop())) 291*e1fe3e4aSElliott Hughes rotation = float(elements.pop()) 292*e1fe3e4aSElliott Hughes arc_large = bool(int(elements.pop())) 293*e1fe3e4aSElliott Hughes arc_sweep = bool(int(elements.pop())) 294*e1fe3e4aSElliott Hughes end = float(elements.pop()) + float(elements.pop()) * 1j 295*e1fe3e4aSElliott Hughes 296*e1fe3e4aSElliott Hughes if not absolute: 297*e1fe3e4aSElliott Hughes end += current_pos 298*e1fe3e4aSElliott Hughes 299*e1fe3e4aSElliott Hughes # if the pen supports arcs, pass the values unchanged, otherwise 300*e1fe3e4aSElliott Hughes # approximate the arc with a series of cubic bezier curves 301*e1fe3e4aSElliott Hughes if have_arcTo: 302*e1fe3e4aSElliott Hughes pen.arcTo( 303*e1fe3e4aSElliott Hughes rx, 304*e1fe3e4aSElliott Hughes ry, 305*e1fe3e4aSElliott Hughes rotation, 306*e1fe3e4aSElliott Hughes arc_large, 307*e1fe3e4aSElliott Hughes arc_sweep, 308*e1fe3e4aSElliott Hughes (end.real, end.imag), 309*e1fe3e4aSElliott Hughes ) 310*e1fe3e4aSElliott Hughes else: 311*e1fe3e4aSElliott Hughes arc = arc_class( 312*e1fe3e4aSElliott Hughes current_pos, rx, ry, rotation, arc_large, arc_sweep, end 313*e1fe3e4aSElliott Hughes ) 314*e1fe3e4aSElliott Hughes arc.draw(pen) 315*e1fe3e4aSElliott Hughes 316*e1fe3e4aSElliott Hughes current_pos = end 317*e1fe3e4aSElliott Hughes 318*e1fe3e4aSElliott Hughes # no final Z command, it's an open path 319*e1fe3e4aSElliott Hughes if start_pos is not None: 320*e1fe3e4aSElliott Hughes pen.endPath() 321