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
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
Description of the bug
A block-level element with a
background-colorandpaddingthat ends onone page has its padding repainted as a filled bar at the top of the next
page.
The bar is
padding-top + padding-bottomtall, startspadding-topabove theframe that was passed to
Story.place(), and is emitted before the page'sreal 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
Output on 1.28.2 and on 1.27.2.3:
42 = 54 - 12(onepadding-topabove the frame) and24 = 12 + 12(
padding-top + padding-bottom). The first line of body text on page 2 has itsbaseline 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: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
Story.write()above so that no user-written place/draw loopis involved. A hand-written
place()/draw()loop produces byte-identicalstray rects.
paddingis required — withpadding: 0there is no bar. The block must alsobe followed by enough content to paginate; the block alone fits on one page
and there is no second page to repaint onto.
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.
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