svgLib: handle arc commands with bool flags not separated by space/comma

Some SVG authoring tool write arc commands without any space or comma around the boolean 'large-arc' and 'sweep' flags, leading our svg path parser to choke.
This makes the path parser smarter by special-casing arc command parsing so that it only consumes one character ('0' or '1') for these special boolean flags.
This commit is contained in:
Cosimo Lupo 2020-10-29 15:00:10 +00:00
parent 28a215173a
commit aad51ec4d9
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F
2 changed files with 149 additions and 3 deletions

View File

@ -13,18 +13,81 @@ import re
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
ARC_COMMANDS = set("Aa")
UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
FLOAT_RE = re.compile(
r"[-+]?" # optional sign
r"(?:"
r"(?:0|[1-9][0-9]*)(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)?" # int/float
r"|"
r"(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)" # float with leading dot (e.g. '.42')
r")"
)
BOOL_RE = re.compile("^[01]")
SEPARATOR_RE = re.compile(f"[, \t]")
def _tokenize_path(pathdef):
arc_cmd = None
for x in COMMAND_RE.split(pathdef):
if x in COMMANDS:
arc_cmd = x if x in ARC_COMMANDS else None
yield x
for token in FLOAT_RE.findall(x):
yield token
continue
if arc_cmd:
try:
yield from _tokenize_arc_arguments(x)
except ValueError as e:
raise ValueError(f"Invalid arc command: '{arc_cmd}{x}'") from e
else:
for token in FLOAT_RE.findall(x):
yield token
ARC_ARGUMENT_TYPES = (
("rx", FLOAT_RE),
("ry", FLOAT_RE),
("x-axis-rotation", FLOAT_RE),
("large-arc-flag", BOOL_RE),
("sweep-flag", BOOL_RE),
("x", FLOAT_RE),
("y", FLOAT_RE),
)
def _tokenize_arc_arguments(arcdef):
raw_args = [s for s in SEPARATOR_RE.split(arcdef) if s]
if not raw_args:
raise ValueError(f"Not enough arguments: '{arcdef}'")
raw_args.reverse()
i = 0
while raw_args:
arg = raw_args.pop()
name, pattern = ARC_ARGUMENT_TYPES[i]
match = pattern.search(arg)
if not match:
raise ValueError(f"Invalid argument for '{name}' parameter: {arg!r}")
j, k = match.span()
yield arg[j:k]
arg = arg[k:]
if arg:
raw_args.append(arg)
# wrap around every 7 consecutive arguments
if i == 6:
i = 0
else:
i += 1
if i != 0:
raise ValueError(f"Not enough arguments: '{arcdef}'")
def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc):

View File

@ -353,3 +353,86 @@ def test_arc_pen_with_arcTo():
]
assert pen.value == expected
@pytest.mark.parametrize(
"path, expected",
[
(
"M1-2A3-4-1.0 01.5.7",
[
("moveTo", ((1.0, -2.0),)),
("arcTo", (3.0, -4.0, -1.0, False, True, (0.5, 0.7))),
("endPath", ()),
],
),
(
"M21.58 7.19a2.51 2.51 0 10-1.77-1.77",
[
("moveTo", ((21.58, 7.19),)),
("arcTo", (2.51, 2.51, 0.0, True, False, (19.81, 5.42))),
("endPath", ()),
],
),
(
"M22 12a25.87 25.87 0 00-.42-4.81",
[
("moveTo", ((22.0, 12.0),)),
("arcTo", (25.87, 25.87, 0.0, False, False, (21.58, 7.19))),
("endPath", ()),
],
),
(
"M0,0 A1.2 1.2 0 012 15.8",
[
("moveTo", ((0.0, 0.0),)),
("arcTo", (1.2, 1.2, 0.0, False, True, (2.0, 15.8))),
("endPath", ()),
],
),
(
"M12 7a5 5 0 105 5 5 5 0 00-5-5",
[
("moveTo", ((12.0, 7.0),)),
("arcTo", (5.0, 5.0, 0.0, True, False, (17.0, 12.0))),
("arcTo", (5.0, 5.0, 0.0, False, False, (12.0, 7.0))),
("endPath", ()),
],
)
],
)
def test_arc_flags_without_spaces(path, expected):
pen = ArcRecordingPen()
parse_path(path, pen)
assert pen.value == expected
@pytest.mark.parametrize(
"path", ["A", "A0,0,0,0,0,0", "A 0 0 0 0 0 0 0 0 0 0 0 0 0"]
)
def test_invalid_arc_not_enough_args(path):
pen = ArcRecordingPen()
with pytest.raises(ValueError, match="Invalid arc command") as e:
parse_path(path, pen)
assert isinstance(e.value.__cause__, ValueError)
assert "Not enough arguments" in str(e.value.__cause__)
def test_invalid_arc_argument_value():
pen = ArcRecordingPen()
with pytest.raises(ValueError, match="Invalid arc command") as e:
parse_path("M0,0 A0,0,0,2,0,0,0", pen)
cause = e.value.__cause__
assert isinstance(cause, ValueError)
assert "Invalid argument for 'large-arc-flag' parameter: '2'" in str(cause)
pen = ArcRecordingPen()
with pytest.raises(ValueError, match="Invalid arc command") as e:
parse_path("M0,0 A0,0,0,0,-2.0,0,0", pen)
cause = e.value.__cause__
assert isinstance(cause, ValueError)
assert "Invalid argument for 'sweep-flag' parameter: '-2.0'" in str(cause)