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
48 changes: 46 additions & 2 deletions svgpathtools/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,41 @@

COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
# In an elliptical arc argument the large-arc-flag and sweep-flag are each a
# single "0"/"1" character that, unlike the other fields, may be written with
# no separator before the next number (e.g. "0110 0" == "0 1 10 0"); see the
# SVG path grammar: https://www.w3.org/TR/SVG/paths.html#PathDataBNF
ARC_FLAG_RE = re.compile(r"[01]")
WSP_COMMA_RE = re.compile(r"[\s,]*")


def _tokenize_arc_args(arg_chunk):
"""Yield the tokens of one or more elliptical-arc argument groups.

Each group has seven fields ``rx ry rotation large-arc-flag sweep-flag x
y``. The two flags are single "0"/"1" characters, so -- unlike a plain
``FLOAT_RE.findall`` -- they must be read one character at a time rather
than greedily swallowed into the following number.
"""
pos = 0
field = 0
n = len(arg_chunk)
while True:
sep = WSP_COMMA_RE.match(arg_chunk, pos)
if sep:
pos = sep.end()
if pos >= n:
return
match = None
if field % 7 in (3, 4):
match = ARC_FLAG_RE.match(arg_chunk, pos)
if match is None:
match = FLOAT_RE.match(arg_chunk, pos)
if match is None:
return
yield match.group()
pos = match.end()
field += 1

# Default Parameters ##########################################################

Expand Down Expand Up @@ -3191,11 +3226,20 @@ def joints(self):
return zip(a, b)

def _tokenize_path(self, pathdef):
command = None
for x in COMMAND_RE.split(pathdef):
if x in COMMANDS:
command = x
yield x
for token in FLOAT_RE.findall(x):
yield token
continue
if command in ('A', 'a'):
# Arc arguments need flag-aware tokenizing; the other commands
# only carry plain numbers.
for token in _tokenize_arc_args(x):
yield token
else:
for token in FLOAT_RE.findall(x):
yield token

def _parse_path(self, pathdef, current_pos=0j, tree_element=None):
# In the SVG specs, initial movetos are absolute, even if
Expand Down
32 changes: 32 additions & 0 deletions test/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,38 @@ def test_negative(self):
path2 = parse_path('M 100 200 c 10 -5 20 -10 30 -20')
self.assertEqual(path1, path2)

def test_arc_flag_packing(self):
"""Arc large-arc/sweep flags are single 0/1 characters and may be
written with no separator before the next number (as SVGO emits).
Fixes issue #129."""
# Each packed form must parse identically to the spaced-out form.
equivalent = [
('M 0 0 A 5 5 0 0110 0', 'M 0 0 A 5 5 0 0 1 10 0'),
('M 0 0 A 5 5 0 1110 0', 'M 0 0 A 5 5 0 1 1 10 0'),
('M 0 0 A 5 5 0 0010 0', 'M 0 0 A 5 5 0 0 0 10 0'),
('M 0 0 a 5 5 0 0010 0', 'M 0 0 a 5 5 0 0 0 10 0'),
('M 0 0 A 5 5 0 01-10 0', 'M 0 0 A 5 5 0 0 1 -10 0'),
('M 0 0 A 5 5 0 0 1 10 0 5 5 0 1120 0',
'M 0 0 A 5 5 0 0 1 10 0 5 5 0 1 1 20 0'),
]
for packed, spaced in equivalent:
self.assertEqual(parse_path(packed), parse_path(spaced))

# The flags carry through to the resulting Arc.
arc = parse_path('M 0 0 A 5 5 0 0110 0')[0]
self.assertFalse(arc.large_arc)
self.assertTrue(arc.sweep)
self.assertEqual(arc.end, 10 + 0j)

# Real-world path from issue #129 (simple-icons "emlakjet") with packed
# "00" flags; this used to raise IndexError.
icon = 'M12 0a3.543 3.543 0 00-1.267 2.471A3.543 3.543 0 0012 0z'
path = parse_path(icon)
self.assertEqual(len(path), 2)
self.assertTrue(all(isinstance(seg, Arc) for seg in path))
self.assertFalse(path[0].large_arc)
self.assertFalse(path[0].sweep)

def test_numbers(self):
"""Exponents and other number format cases"""
# It can be e or E, the plus is optional, and a minimum of
Expand Down