Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions svgpathtools/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')

# Number of numeric parameters each command consumes per repetition.
COMMAND_NUM_ARGS = {'M': 2, 'L': 2, 'H': 1, 'V': 1, 'C': 6,
'S': 4, 'Q': 4, 'T': 2, 'A': 7, 'Z': 0}

COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")

Expand Down Expand Up @@ -3227,6 +3231,15 @@ def _parse_path(self, pathdef, current_pos=0j, tree_element=None):
pathdef, len(pathdef.split()) - len(elements)))
last_command = command # Used by S and T

# A command must be followed by its full set of coordinates; a
# truncated path (e.g. "M 0 0 L") would otherwise pop past the end
# of the token list and raise a bare IndexError.
if len(elements) < COMMAND_NUM_ARGS[command]:
raise ValueError("Invalid path string: command '%s' expects %d "
"values but only %d remain in %r" % (
command, COMMAND_NUM_ARGS[command],
len(elements), pathdef))

if command == 'M':
# Moveto command.
x = elements.pop()
Expand Down Expand Up @@ -3297,7 +3310,7 @@ def _parse_path(self, pathdef, current_pos=0j, tree_element=None):
# Smooth curve. First control point is the "reflection" of
# the second control point in the previous path.

if last_command not in 'CS':
if last_command not in ('C', 'S'):
# 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.
Expand Down Expand Up @@ -3333,7 +3346,7 @@ def _parse_path(self, pathdef, current_pos=0j, tree_element=None):
# Smooth curve. Control point is the "reflection" of
# the second control point in the previous path.

if last_command not in 'QT':
if last_command not in ('Q', 'T'):
# 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.
Expand Down
16 changes: 16 additions & 0 deletions test/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,22 @@ def test_errors(self):
self.assertRaises(ValueError, parse_path,
'M 100 100 L 200 200 Z 100 200')

def test_truncated_commands(self):
# A command missing some of its coordinates used to pop past the end of
# the token list and raise a bare IndexError instead of a ValueError.
for d in ('M', 'M 10', 'M 0 0 L', 'M 0 0 C 1 1 2 2',
'H', 'V', 'Q 1 1', 'A 1 1 0 0 1'):
self.assertRaises(ValueError, parse_path, d)

def test_leading_smooth_command(self):
# A path starting with a smooth command (S/T) has no previous control
# point to reflect; the first control point coincides with the current
# point. This used to raise a TypeError from ``None in 'CS'``.
self.assertEqual(parse_path('S 1 1 2 2'),
Path(CubicBezier(0j, 0j, 1 + 1j, 2 + 2j)))
self.assertEqual(parse_path('T 1 1'),
Path(QuadraticBezier(0j, 0j, 1 + 1j)))


def test_transform(self):

Expand Down