`get_drawings()` reports a wrong start point for a subpath following a `re` with negative width/height
Description of the bug
After the re operator, the PDF specification (ISO 32000-1, 8.5.2.1) leaves the
current point at the operand origin (x, y). If a path continues with l or
c without an intervening m, that segment must start at (x, y).
When w or h is negative, (x, y) is not a corner of the normalised
rectangle. In that case get_drawings() reports the following segment as
starting at the opposite corner of the normalised rectangle instead of at the
re origin. The renderer is correct; only the extracted geometry is wrong.
The error equals the rectangle's height (or width), so an extracted line can be
skewed, mispositioned, or both, while the rendered page looks fine. Anything
built on get_drawings() — geometry extraction, the documented
get_drawings() → Shape round-trip, table/line detection — silently inherits
the wrong coordinates.
I hit this on a real CAD floor plan: 12 paths matched the pattern, and lines that are vertical in the PDF were extracted with a 0.97 mm skew. Pixel measurements on the rendered page confirmed the rendered geometry, not the extracted one.
How to reproduce the bug
Reproducer
This script writes a minimal PDF and prints the result — no external file needed.
(The same PDF is attached as reproducer.pdf.)
import pymupdf
content = b"1 w\n100 720 40 -20 re\n300 600 l\nS\n"
objs = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 800] "
b"/Contents 4 0 R /Resources << >> >>",
b"<< /Length %d >>\nstream\n" % len(content) + content + b"endstream",
]
pdf, offsets = b"%PDF-1.4\n", []
for i, o in enumerate(objs, 1):
offsets.append(len(pdf))
pdf += b"%d 0 obj\n" % i + o + b"\nendobj\n"
xref = len(pdf)
pdf += b"xref\n0 %d\n" % (len(objs) + 1) + b"0000000000 65535 f \n"
for off in offsets:
pdf += b"%010d 00000 n \n" % off
pdf += b"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n" % (
len(objs) + 1, xref)
open("reproducer.pdf", "wb").write(pdf)
page = pymupdf.open("reproducer.pdf")[0]
for d in page.get_drawings():
for it in d["items"]:
print(it)Content stream
1 w
100 720 40 -20 re
300 600 l
Sre origin is (100, 720) with h = -20, so the rectangle spans PDF
y 700..720. The current point after re is (100, 720), i.e. (100, 80) in
page coordinates. There is no m, so the line must start there.
Actual output
('re', Rect(100.0, 80.0, 140.0, 100.0), -1)
('l', Point(100.0, 100.0), Point(300.0, 200.0))Expected output
('re', Rect(100.0, 80.0, 140.0, 100.0), -1)
('l', Point(100.0, 80.0), Point(300.0, 200.0))The reported start (100, 100) is the rectangle corner opposite to the re
origin — off by |h| = 20.
Confirmation from rendering
MuPDF renders the line from (100, 80), not from (100, 100):
import pymupdf, numpy as np
page = pymupdf.open("reproducer.pdf")[0]
S = 4
pix = page.get_pixmap(matrix=pymupdf.Matrix(S, S), colorspace=pymupdf.csGRAY)
arr = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width)
col = arr[:, 200 * S] # sample the line at x = 200
print([d / S for d in np.where(col < 128)[0]])Output is y ≈ 140. A line (100, 80) → (300, 200) passes through y = 140 at
x = 200; the reported (100, 100) → (300, 200) would give y = 150.
Screenshots
pymupdf_bug_1_correct.png— the actual MuPDF rendering.pymupdf_bug_2_wrong.png— the same page rebuilt fromget_drawings()output viaShape(the round-trip described in the docs).pymupdf_bug_3_overlay.png— both overlaid; the start points differ by the rectangle height while the end point matches.
Variants tested
| Content stream | re orientation |
Reported start | Correct? |
|---|---|---|---|
100 700 40 20 re + 100 600 m + 300 600 l |
+1 |
(100, 200) |
yes |
100 720 40 -20 re + 100 600 m + 300 600 l |
-1 |
(100, 200) |
yes |
100 720 40 -20 re + 300 600 l (no m) |
-1 |
(100, 100) |
no |
140 720 -40 -20 re + 100 600 m + 300 600 l |
-1 |
(100, 200) |
yes |
So the trigger is a negative w/h together with a following segment that
has no explicit m. With an explicit m the start point is correct.
Additional note
get_drawings() does not expose the re operands, only the normalised
Rect and an orientation flag. Since -1 can result from a negative width, a
negative height, or both, callers cannot reconstruct the origin corner
themselves and therefore cannot work around this reliably. It would help if the
orientation flag were replaced (or complemented) by something that identifies
the origin corner.
Environment
Reproduced in a clean container, PyMuPDF installed from PyPI via
pip install pymupdf.
PyMuPDF version
1.28.2
Operating system
Linux
Python version
3.12
Source: pymupdf/PyMuPDF