# SVG Path specification parser. # This is an adaptation from 'svg.path' by Lennart Regebro (@regebro), # modified so that the parser takes a FontTools Pen object instead of # returning a list of svg.path Path objects. # The original code can be found at: # https://github.com/regebro/svg.path/blob/4f9b6e3/src/svg/path/parser.py # Copyright (c) 2013-2014 Lennart Regebro # License: MIT from __future__ import ( print_function, division, absolute_import, unicode_literals) from fontTools.misc.py23 import * import re COMMANDS = set('MmZzLlHhVvCcSsQqTtAa') UPPERCASE = set('MZLHVCSQTA') COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])") FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?") def _tokenize_path(pathdef): for x in COMMAND_RE.split(pathdef): if x in COMMANDS: yield x for token in FLOAT_RE.findall(x): yield token def parse_path(pathdef, pen, current_pos=(0, 0)): """ Parse SVG path definition (i.e. "d" attribute of elements) and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath methods. If 'current_pos' (2-float tuple) is provided, the initial moveTo will be relative to that instead being absolute. Arc segments (commands "A" or "a") are not currently supported, and raise NotImplementedError. """ # In the SVG specs, initial movetos are absolute, even if # specified as 'm'. This is the default behavior here as well. # But if you pass in a current_pos variable, the initial moveto # will be relative to that current_pos. This is useful. current_pos = complex(*current_pos) elements = list(_tokenize_path(pathdef)) # Reverse for easy use of .pop() elements.reverse() start_pos = None command = None last_control = None while elements: if elements[-1] in COMMANDS: # New command. last_command = command # Used by S and T command = elements.pop() absolute = command in UPPERCASE command = command.upper() else: # If this element starts with numbers, it is an implicit command # and we don't change the command. Check that it's allowed: if command is None: raise ValueError("Unallowed implicit command in %s, position %s" % ( pathdef, len(pathdef.split()) - len(elements))) last_command = command # Used by S and T if command == 'M': # Moveto command. x = elements.pop() y = elements.pop() pos = float(x) + float(y) * 1j if absolute: current_pos = pos else: current_pos += pos # M is not preceded by Z; it's an open subpath if start_pos is not None: pen.endPath() pen.moveTo((current_pos.real, current_pos.imag)) # when M is called, reset start_pos # This behavior of Z is defined in svg spec: # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand start_pos = current_pos # Implicit moveto commands are treated as lineto commands. # So we set command to lineto here, in case there are # further implicit commands after this moveto. command = 'L' elif command == 'Z': # Close path if current_pos != start_pos: pen.lineTo((start_pos.real, start_pos.imag)) pen.closePath() current_pos = start_pos start_pos = None command = None # You can't have implicit commands after closing. elif command == 'L': x = elements.pop() y = elements.pop() pos = float(x) + float(y) * 1j if not absolute: pos += current_pos pen.lineTo((pos.real, pos.imag)) current_pos = pos elif command == 'H': x = elements.pop() pos = float(x) + current_pos.imag * 1j if not absolute: pos += current_pos.real pen.lineTo((pos.real, pos.imag)) current_pos = pos elif command == 'V': y = elements.pop() pos = current_pos.real + float(y) * 1j if not absolute: pos += current_pos.imag * 1j pen.lineTo((pos.real, pos.imag)) current_pos = pos elif command == 'C': control1 = float(elements.pop()) + float(elements.pop()) * 1j control2 = float(elements.pop()) + float(elements.pop()) * 1j end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: control1 += current_pos control2 += current_pos end += current_pos pen.curveTo((control1.real, control1.imag), (control2.real, control2.imag), (end.real, end.imag)) current_pos = end last_control = control2 elif command == 'S': # Smooth curve. First control point is the "reflection" of # the second control point in the previous path. if last_command not in 'CS': # If there is no previous command or if the previous command # was not an C, c, S or s, assume the first control point is # coincident with the current point. control1 = current_pos else: # The first control point is assumed to be the reflection of # the second control point on the previous command relative # to the current point. control1 = current_pos + current_pos - last_control control2 = float(elements.pop()) + float(elements.pop()) * 1j end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: control2 += current_pos end += current_pos pen.curveTo((control1.real, control1.imag), (control2.real, control2.imag), (end.real, end.imag)) current_pos = end last_control = control2 elif command == 'Q': control = float(elements.pop()) + float(elements.pop()) * 1j end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: control += current_pos end += current_pos pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) current_pos = end last_control = control elif command == 'T': # Smooth curve. Control point is the "reflection" of # the second control point in the previous path. if last_command not in 'QT': # If there is no previous command or if the previous command # was not an Q, q, T or t, assume the first control point is # coincident with the current point. control = current_pos else: # The control point is assumed to be the reflection of # the control point on the previous command relative # to the current point. control = current_pos + current_pos - last_control end = float(elements.pop()) + float(elements.pop()) * 1j if not absolute: end += current_pos pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) current_pos = end last_control = control elif command == 'A': raise NotImplementedError('arcs are not supported') # no final Z command, it's an open path if start_pos is not None: pen.endPath()