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.)
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
S
re 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 from get_drawings() output
via Shape (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
Description of the bug
After the
reoperator, the PDF specification (ISO 32000-1, 8.5.2.1) leaves thecurrent point at the operand origin
(x, y). If a path continues withlorcwithout an interveningm, that segment must start at(x, y).When
worhis negative,(x, y)is not a corner of the normalisedrectangle. In that case
get_drawings()reports the following segment asstarting at the opposite corner of the normalised rectangle instead of at the
reorigin. 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 documentedget_drawings()→Shaperound-trip, table/line detection — silently inheritsthe 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.)reproducer.pdf
Content stream
reorigin is(100, 720)withh = -20, so the rectangle spans PDFy 700..720. The current point after
reis(100, 720), i.e.(100, 80)inpage coordinates. There is no
m, so the line must start there.Actual output
Expected output
The reported start
(100, 100)is the rectangle corner opposite to thereorigin — off by
|h| = 20.Confirmation from rendering
MuPDF renders the line from
(100, 80), not from(100, 100):Output is
y ≈ 140. A line(100, 80) → (300, 200)passes throughy = 140atx = 200; the reported(100, 100) → (300, 200)would givey = 150.Screenshots
pymupdf_bug_1_correct.png— the actual MuPDF rendering.pymupdf_bug_2_wrong.png— the same page rebuilt fromget_drawings()outputvia
Shape(the round-trip described in the docs).pymupdf_bug_3_overlay.png— both overlaid; the start points differ by therectangle height while the end point matches.
Variants tested
reorientation100 700 40 20 re+100 600 m+300 600 l+1(100, 200)100 720 40 -20 re+100 600 m+300 600 l-1(100, 200)100 720 40 -20 re+300 600 l(nom)-1(100, 100)140 720 -40 -20 re+100 600 m+300 600 l-1(100, 200)So the trigger is a negative
w/htogether with a following segment thathas no explicit
m. With an explicitmthe start point is correct.Additional note
get_drawings()does not expose thereoperands, only the normalisedRectand an orientation flag. Since-1can result from a negative width, anegative 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