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')
|
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
||||||
|
ARC_COMMANDS = set("Aa")
|
||||||
UPPERCASE = set('MZLHVCSQTA')
|
UPPERCASE = set('MZLHVCSQTA')
|
||||||
|
|
||||||
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
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):
|
def _tokenize_path(pathdef):
|
||||||
|
arc_cmd = None
|
||||||
for x in COMMAND_RE.split(pathdef):
|
for x in COMMAND_RE.split(pathdef):
|
||||||
if x in COMMANDS:
|
if x in COMMANDS:
|
||||||
|
arc_cmd = x if x in ARC_COMMANDS else None
|
||||||
yield x
|
yield x
|
||||||
for token in FLOAT_RE.findall(x):
|
continue
|
||||||
yield token
|
|
||||||
|
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):
|
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
|
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