Skip to content

fix(svg): keep the stripped width/height when detecting a percentage - #1936

Open
Anai-Guo wants to merge 1 commit into
py-pdf:masterfrom
Anai-Guo:fix-svg-percent-whitespace
Open

fix(svg): keep the stripped width/height when detecting a percentage#1936
Anai-Guo wants to merge 1 commit into
py-pdf:masterfrom
Anai-Guo:fix-svg-percent-whitespace

Conversation

@Anai-Guo

Copy link
Copy Markdown

Problem

SVGObject.extract_shape_info() calls .strip() on the width/height attributes as bare statements:

self.width = None
if width_str is not None:
    width_str.strip()                     # <-- result discarded
    if width_str.endswith("%"):
        self.width = Percent(width_str[:-1])
    else:
        self.width = resolve_length(width_str)

Python strings are immutable, so .strip() returns a new string and the original is unchanged — the whitespace is still there on the next line.

A trailing space then defeats the percentage check: "100% ".endswith("%") is False, so the value falls through to resolve_length(), which parses the unit as % and rejects it as a relative length:

>>> SVGObject('<svg xmlns="http://www.w3.org/2000/svg" width="100% " height="100% " viewBox="0 0 10 10"></svg>')
ValueError: 100%  uses unsupported relative length %

Thirteen lines below, in the same method, viewbox already does it correctly:

viewbox = viewbox.strip()

Scope

Only percentages are affected. unit_splitter (\s*(?P<value>[-+]?[\d\.]+)\s*(?P<unit>%|[a-zA-Z]*)) tolerates leading whitespace and .match() ignores anything trailing, so absolute lengths already survive the missing strip. Measured on the current code:

width before after
"100%" Percent(100.0) Percent(100.0)
"100% " ValueError Percent(100.0)
" 100% " ValueError Percent(100.0)
"100" / "100 " / "100px " fine fine (unchanged)

Leading-only whitespace (" 100%") happens to work already, because endswith("%") still holds and float(" 100") accepts the space — so 3 of the 4 whitespace cases in the new test fail without the fix.

Fix

Keep the stripped value, which is what the code was already trying to do:

-            width_str.strip()
+            width_str = width_str.strip()
...
-            height_str.strip()
+            height_str = height_str.strip()

Test

Added test_document_shape_info_percent_surrounded_by_whitespace to TestSVGObject, parametrized over "100% ", " 100%", " 100% ", "100%\n" (a newline is easy to hit when the attribute is written across lines in hand-authored SVG).

Verified both directions rather than just the happy one:

# with the patch reverted, new test only
3 failed, 1 passed        (" 100%" passes -- see the scope table above)

# with the patch applied
test/svg/  ->  315 passed

black (26.3.1, the revision pinned in .pre-commit-config.yaml) reports both files unchanged.

Note on the base commit and the CHANGELOG

My fork cannot be synced to master — the GitHub API refuses merge-upstream for this token because the sync would touch .github/workflows/codeql.yml, which needs the workflow OAuth scope — so this branch is based on my fork's own tip. I checked the drift explicitly: fpdf/svg.py and test/svg/test_svg.py are byte-identical between that commit and current master, so the diff is exactly what it looks like.

CHANGELOG.md is the one file that has moved since (the [2.8.9] section gained its ### Fixed heading and two entries). Writing my entry against the older copy either conflicts or auto-merges into a duplicated ### Fixed heading, so I have deliberately left CHANGELOG.md out rather than commit something that merges wrong. The entry I would add is:

* SVG `width`/`height` attributes holding a percentage surrounded by whitespace (_e.g._ `width="100% "`) no longer raise `ValueError: uses unsupported relative length %`; the value is now stripped before the percentage is detected, as the code already intended - _cf._ [PR #1936](https://github.com/py-pdf/fpdf2/pull/1936)

Happy to add it (or rebase the whole branch) on request.


🤖 Generated with Claude Code

extract_shape_info() calls width_str.strip() and height_str.strip() as bare
statements. Strings are immutable, so the results are discarded and the
whitespace survives. A trailing space then defeats the percentage check:

    <svg width="100% "> -> ValueError: 100%  uses unsupported relative length %

endswith("%") is False, so the value falls through to resolve_length(), which
parses the unit as % and rejects it as a relative length.

Absolute lengths are unaffected -- unit_splitter tolerates surrounding
whitespace -- so only percentages are affected. viewbox, thirteen lines below
in the same method, already assigns its strip() result.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants