Skip to content

SVG: draw a path up to an error in its data, after a correct start - #2177

Merged
andersonhc merged 10 commits into
py-pdf:masterfrom
RavSinghChandan:svg-path-error-after-start
Nov 13, 2026
Merged

andersonhc merged 10 commits into
py-pdf:masterfrom
RavSinghChandan:svg-path-error-after-start

Conversation

@RavSinghChandan

Copy link
Copy Markdown

A <path> whose data has an error after a correct start makes the whole document fail:

  • numbers after a closepath, M0 0L10 0Z 20 20: ValueError: Unallowed implicit command
  • a command without its numbers, M0 0L10 0 L: IndexError: pop from empty list

The SVG implementation notes say a path is rendered up to and including the last correct command. _complete_points() already does that for <polyline> and <polygon>, and its docstring says "like an incorrect <path>". svg_path_converter() now keeps what parse_path() drew before such an error. A path that does not start with a moveto still raises, so test_bad_path_start is unchanged.

Two new svg_path_edge_cases cover it; both fail without the change.

Numbers after a closepath failed with ValueError: Unallowed implicit
command, and a command without its numbers with IndexError, so the
whole document failed. As the SVG implementation notes say, and as
_complete_points() already does for points, the path is now drawn up to
the last correct command; a path that does not start with a moveto
still fails.

@andersonhc andersonhc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed one related case that still fails: M0 0L10 0 A or M0 0L10 0 A5 5 0 0 raises ValueError: Invalid arc command instead of drawing the valid prefix. FontTools tokenizes the whole path before making any pen calls, so pen.first_is_move is still unset when these errors reach the handler.

Could you extend the recovery to cover incomplete arc commands and add a regression test? That would also make the behavior consistent with the changelog’s description.

@andersonhc andersonhc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one remaining edge case: recovery at command-letter boundaries can discard valid segments within an implicit command.

For example, M0 0 L10 0 A5 5 0 0 0 20 0 5 loses the completed arc before the trailing incomplete arguments. Similarly, M0 0 L10 0 20 A loses the valid line to (10, 0).

Could we preserve complete argument groups before the error? That would make recovery consistent with the “last correct command” behavior described in the changelog.

@RavSinghChandan

Copy link
Copy Markdown
Author

Thanks, fixed. When the error comes before any pen call, as with an arc, the converter now finds the longest run of whole commands at the start of the data that fontTools can read (by bisection, so a broken 20,000-command path takes 0.4 s) and draws that. A path that does not start with a moveto still raises. Added "arc without numbers" (M 0 1 L 2 3 A) and "arc without all its numbers" (M 0 1 L 2 3 A 5 5 0 0) to svg_path_directives; both fail without the change, and the changelog now mentions incomplete arcs.

@RavSinghChandan

Copy link
Copy Markdown
Author

Thanks, fixed. After finding the last whole command that can be read, recovery now goes into the failing command and keeps its complete sets of numbers: it tries the prefixes ending after each number of that command, longest first. Your two cases give M0 0 L10 0 A5 5 0 0 0 20 0 (the completed arc is kept) and M0 0 L10 0 (the line to (10, 0) is kept); both are in svg_path_directives as "repeated arc without all its numbers" and "repeated line without all its numbers" and fail without the change. A broken 20,000-command path still takes 0.4 s, and the changelog now says "up to the last complete set of numbers".

@andersonhc andersonhc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed one performance edge case in the new loop. With an invalid arc flag followed by many numbers, such as "M0 0 L10 0 A5 5 0 2 " + "1 " * n, every candidate prefix fails, so we repeatedly parse progressively shorter strings. Locally, 1,000 / 2,000 / 4,000 trailing numbers took about 0.08 / 0.28 / 1.07 seconds.

Could we find the first invalid argument while scanning forward, keeping track of the last complete argument group? That would let recovery stop at the error without reparsing every prefix.

Trying shorter and shorter prefixes parsed the path again for every
number, so an invalid arc flag followed by many numbers took quadratic
time. Read each command's numbers forward once, in complete sets, and
parse only the prefix before the first incomplete one.
@RavSinghChandan

Copy link
Copy Markdown
Author

Thanks. It now reads the path forward once: _path_argument_groups() splits each command's numbers into complete sets (7 for an arc, with single-digit flags as fontTools reads them). Recovery stops at the last complete set before the first command with an error, and fontTools parses only that prefix. Your input with 50,000 trailing numbers takes 0.002 s, and 5,000 arcs followed by an error are parsed twice instead of once per command. test_path_error_found_in_one_pass covers both and a lineto missing its last number.

@RavSinghChandan

Copy link
Copy Markdown
Author

Thanks. It now reads the path forward once: _path_argument_groups() splits each command's numbers into complete sets (7 for an arc, with single-digit flags as fontTools reads them). Recovery stops at the last complete set before the first command with an error, and fontTools parses only that prefix. Your input with 50,000 trailing numbers takes 0.002 s, and 5,000 arcs followed by an error are parsed twice instead of once per command. test_path_error_found_in_one_pass covers both and a lineto missing its last number.

@andersonhc
andersonhc merged commit 3f8d7c2 into py-pdf:master Nov 13, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants