Skip to content

get_drawings() reports a wrong start point for a subpath following a re with negative width/height #5122

Description

@tolot27

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.
Image Image Image

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions