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:
parent
28a215173a
commit
aad51ec4d9
@ -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):
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user