diff --git a/Lib/fontTools/svgLib/path/parser.py b/Lib/fontTools/svgLib/path/parser.py index e23d8aa8d..3d8d539d5 100644 --- a/Lib/fontTools/svgLib/path/parser.py +++ b/Lib/fontTools/svgLib/path/parser.py @@ -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): diff --git a/Tests/svgLib/path/parser_test.py b/Tests/svgLib/path/parser_test.py index 3b130bea3..d76e99524 100644 --- a/Tests/svgLib/path/parser_test.py +++ b/Tests/svgLib/path/parser_test.py @@ -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)