#5110·PyMuPDF

Story: a background block that ENDS on one page repaints its padding at the top of the next page

Author: elightCreated Sep 5, 2026Updated Sep 11, 2026
Labelsupstream bug

Description of the bug

A block-level element with a background-color and padding that ends on one page has its padding repainted as a filled bar at the top of the next page.

The bar is padding-top + padding-bottom tall, starts padding-top above the frame that was passed to Story.place(), and is emitted before the page's real content — so the first line of text on that page is printed on top of it. With a dark background and dark body text, that line is unreadable.

The element is finished. It contributes nothing else to the following page, and nothing in the markup asks for a background there.

This is not the same thing as a background that genuinely continues across a page break. A real continuation starts at the same y but is as tall as the content that continues; this bar has zero content height.

How to reproduce the bug

python
import pymupdf

MEDIABOX = pymupdf.paper_rect("letter")
FRAME = MEDIABOX + (54, 54, -54, -54)

HTML = (
    '<html><body>'
    '<div style="background-color:#1f3a5f;color:#ffffff;padding:12pt">BANNER</div>'
    "<p>" + ("filler words " * 400) + "</p>"
    "</body></html>"
)

story = pymupdf.Story(html=HTML)
writer = pymupdf.DocumentWriter("out.pdf")
story.write(writer, lambda rect_num, filled: (MEDIABOX, FRAME, None))
writer.close()

doc = pymupdf.open("out.pdf")
for n in range(1, doc.page_count):
    for d in doc[n].get_drawings():
        r = d["rect"]
        if r.y0 < FRAME.y0:
            print(f"page {n + 1}: fill at y {r.y0}..{r.y1} "
                  f"(frame starts at {FRAME.y0}), height {r.y1 - r.y0}")

Output on 1.28.2 and on 1.27.2.3:

page 2: fill at y 42.0..66.0 (frame starts at 54.0), height 24.0

42 = 54 - 12 (one padding-top above the frame) and 24 = 12 + 12 (padding-top + padding-bottom). The first line of body text on page 2 has its baseline at y 64.8, inside that bar.

The page content stream shows the ordering — the bar is painted immediately after the clip and before the first BT:

1 0 0 -1 -0 792 cm
q
54 54 504 684 re
W n
.12156863 .22745098 .37254904 rg
66 42 480 24 re
f
0 0 0 rg
BT
/F0 12 Tf
1 0 0 -1 66 64.8 Tm
...

MuPDF's own clip removes the part above y 54, so what survives is a 12pt bar lying under the first line of text.

Expected: no background is painted on page 2 for an element that ended on page 1.

Notes

  • Reproduced with Story.write() above so that no user-written place/draw loop is involved. A hand-written place() / draw() loop produces byte-identical stray rects.
  • padding is required — with padding: 0 there is no bar. The block must also be followed by enough content to paginate; the block alone fits on one page and there is no second page to repaint onto.
  • A background block taller than one page behaves correctly: on the continuation page it starts at the same y but is as tall as the remaining content (measured: 700.8pt against this bar's 24pt), so the two cases are distinguishable by height.
  • Found in the wild in a 14-page generated report whose page 2 opened with a dark bar painted across its first table row. The reduction above is the smallest markup we could get it down to; we did not isolate which of that document's several padded, coloured blocks produced the bar.

PyMuPDF version

1.28.2 (also reproduced on 1.27.2.3)

Python version / OS

Python 3.12, Linux x86-64