Fix Watermark Hidden Behind Page Content
The script stamps DRAFT on every page of a report pack. On the text-only pages it looks right. On pages exported from PowerPoint, on scanned appendix pages and on pages with full-width shaded tables, the watermark is missing — or shows only as a few letters peeking out between table rows. Printing makes it worse: some printers show it, others do not, and a colleague's viewer shows it only when zoomed in.
page.merge_page(stamp_page, over=False) # pypdf: stamp under the content
Root Cause
PDF pages are painted in order: whatever is drawn later covers what was drawn earlier. A watermark merged under the page content (over=False in pypdf, or overlay=False in PyMuPDF) is painted first, then every opaque object on the page paints over it. Text-only pages have transparent backgrounds, so an underlay shows through. Pages from presentation software typically start with a full-page white or coloured rectangle; scanned pages are a full-page opaque image; shaded tables paint filled cells. Each of those hides an underlay completely or in patches. Moving the watermark on top fixes visibility but, without transparency, the stamp now hides content. Two further mechanisms cause "visible on screen, missing in print" and vice versa: stamps added as annotations carry flags such as NoView or lack the Print flag, and optional content groups (layers) can be configured to hide on print or screen.
Minimal Diagnostic
For each page, check whether an opaque full-page object is painted before the watermark position, and whether stamps are content or annotations with restrictive flags.
# pip install pymupdf
from pathlib import Path
import pymupdf
STAMPED = Path("out/report-draft.pdf")
def diagnose(pdf_path: Path, word: str = "DRAFT") -> None:
try:
doc = pymupdf.open(pdf_path)
except (pymupdf.FileDataError, RuntimeError) as exc:
raise SystemExit(f"cannot open {pdf_path}: {exc}")
with doc:
for page in doc:
area = page.rect.width * page.rect.height
big_fills = [d for d in page.get_drawings()
if d.get("fill") is not None and d["rect"].width * d["rect"].height > 0.6 * area]
big_images = [r for img in page.get_images(full=True) for r in page.get_image_rects(img[0])
if r.width * r.height > 0.6 * area]
in_content = bool(page.search_for(word))
annots = [(a.type[1], a.flags) for a in page.annots()]
print(f"page {page.number + 1:>2}: full-page fill={len(big_fills)} full-page image={len(big_images)} "
f"watermark in content={in_content} annots={annots}")
if __name__ == "__main__":
diagnose(STAMPED)
page 1: full-page fill=0 full-page image=0 watermark in content=True annots=[]
page 7: full-page fill=1 full-page image=0 watermark in content=True annots=[]
page 12: full-page fill=0 full-page image=1 watermark in content=True annots=[]
page 19: full-page fill=0 full-page image=0 watermark in content=False annots=[('Stamp', 6)]
The watermark text exists on pages 7 and 12, but a full-page fill and a full-page scan cover it — it was merged underneath. Page 19 uses a stamp annotation with flags 6, which is Hidden (2) plus Print (4) — invisible on screen, printed on paper.
Fix: Stamp Over the Content with Transparency
Draw the watermark on top, and make it translucent so it never hides what it covers. With pypdf, build the stamp with a transparent fill and merge it over the page. Changed lines carry comments.
# pip install "pypdf>=4.0" reportlab
import io
import math
from pathlib import Path
from pypdf import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
SOURCE = Path("in/report.pdf")
DEST = Path("out/report-draft.pdf")
def make_stamp(width: float, height: float, text: str = "DRAFT", opacity: float = 0.18):
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=(width, height))
c.setFillColorRGB(0.75, 0.1, 0.1, alpha=opacity) # changed: translucent fill
size = 0.55 * math.hypot(width, height) / c.stringWidth(text, "Helvetica-Bold", 1)
c.setFont("Helvetica-Bold", size)
c.translate(width / 2, height / 2)
c.rotate(math.degrees(math.atan2(height, width)))
c.drawCentredString(0, -size * 0.35, text)
c.save()
buf.seek(0)
return PdfReader(buf).pages[0]
def stamp_over(src: Path, dest: Path) -> None:
writer = PdfWriter(clone_from=PdfReader(src))
cache = {}
for page in writer.pages:
key = (round(float(page.mediabox.width)), round(float(page.mediabox.height)))
if key not in cache:
cache[key] = make_stamp(float(page.mediabox.width), float(page.mediabox.height))
page.merge_page(cache[key], over=True) # changed: paint after the content
dest.parent.mkdir(parents=True, exist_ok=True)
with dest.open("wb") as fh:
writer.write(fh)
if __name__ == "__main__":
try:
stamp_over(SOURCE, DEST)
except Exception as exc:
raise SystemExit(f"stamping failed: {exc}")
An opacity between 0.12 and 0.2 stays clearly visible on white pages while leaving text underneath readable; photographs and dark slides may need the upper end. ReportLab writes the alpha as an extended graphics state, which all mainstream viewers and printers honour. Page size and rotation handling for mixed documents follows add a text watermark to every PDF page.
Variant Fix 1: Keep an Underlay Look with a Blend Mode
Some organisations dislike a watermark that tints text on top. A Multiply blend mode keeps the overlay from lightening dark content: dark text stays fully dark, and the watermark only colours light areas — visually similar to an underlay, but painted last so backgrounds cannot hide it. PyMuPDF can insert the stamp over the content and then set the blend mode through the graphics state:
# pip install pymupdf
import math
import pymupdf
def multiply_watermark(page: pymupdf.Page, text: str = "DRAFT") -> None:
rect = page.rect
font = pymupdf.Font("hebo")
size = 0.55 * math.hypot(rect.width, rect.height) / font.text_length(text, fontsize=1)
centre = pymupdf.Point(rect.width / 2, rect.height / 2) * page.derotation_matrix
shape = page.new_shape()
angle = -math.degrees(math.atan2(rect.height, rect.width)) - page.rotation
start = pymupdf.Point(centre.x - font.text_length(text, size) / 2, centre.y + size * 0.35)
shape.insert_text(start, text, fontname="hebo", fontsize=size, color=(0.85, 0.35, 0.35),
fill_opacity=0.5, morph=(centre, pymupdf.Matrix(angle)))
shape.commit(overlay=True)
xref = page.get_contents()[-1] # the stream just added
stream = page.parent.xref_stream(xref)
page.parent.update_stream(xref, b"q /MultiplyGS gs\n" + stream + b"\nQ")
resources = page.parent.xref_get_key(page.xref, "Resources")
gs = page.parent.get_new_xref()
page.parent.update_object(gs, "<< /Type /ExtGState /BM /Multiply >>")
page.parent.xref_set_key(page.xref, "Resources/ExtGState/MultiplyGS", f"{gs} 0 R")
This manipulates page resources directly, so test it on your documents' variety before adopting it; the plain translucent overlay above is simpler and works in more tools. Blend modes are also unsupported by a few low-end printer drivers, which render the stamp at full strength — check a physical print.
Variant Fix 2: Annotation Stamps That Hide on Screen or Paper
Watermarks added as annotations — by some PDF editors, or by page.add_freetext_annot — obey annotation flags. Set them explicitly: visible, printable, and not editable by readers:
# pip install pymupdf
import pymupdf
def fix_annotation_flags(doc: pymupdf.Document, subtypes=("Stamp", "FreeText", "Watermark")) -> int:
changed = 0
for page in doc:
for annot in page.annots():
if annot.type[1] not in subtypes:
continue
flags = annot.flags
flags &= ~pymupdf.PDF_ANNOT_IS_HIDDEN # visible on screen
flags &= ~pymupdf.PDF_ANNOT_IS_NO_VIEW
flags |= pymupdf.PDF_ANNOT_IS_PRINT # printed
flags |= pymupdf.PDF_ANNOT_IS_LOCKED # cannot be moved or deleted in viewers
if flags != annot.flags:
annot.set_flags(flags)
annot.update()
changed += 1
return changed
Annotations are easy for recipients to delete even when locked, and some tools ignore them when flattening or printing. For watermarks that must survive forwarding, convert the annotation to page content with doc.bake(annots=True) (PyMuPDF), or stamp content directly as in the fix.
Verification
Render each page with and without the watermark and confirm the watermark changes pixels on every page — a hidden watermark changes nothing. Also confirm content pixels were not replaced by an opaque stamp.
# pip install pymupdf pillow
from pathlib import Path
import pymupdf
from PIL import Image, ImageChops, ImageStat
def render(page: pymupdf.Page, dpi: int = 40) -> Image.Image:
pix = page.get_pixmap(dpi=dpi, alpha=False)
return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
def verify_visible(original: Path, stamped: Path, min_change: float = 0.8, max_change: float = 40.0) -> None:
with pymupdf.open(original) as a, pymupdf.open(stamped) as b:
for pa, pb in zip(a, b):
diff = ImageChops.difference(render(pa), render(pb))
mean = sum(ImageStat.Stat(diff).mean) / 3
assert mean >= min_change, f"page {pa.number + 1}: watermark not visible (mean diff {mean:.2f})"
assert mean <= max_change, f"page {pa.number + 1}: stamp too opaque (mean diff {mean:.2f})"
for annot in pb.annots():
assert annot.flags & pymupdf.PDF_ANNOT_IS_PRINT, f"page {pb.number + 1}: annotation not printable"
print(f"{stamped.name}: watermark visible and translucent on every page")
The lower bound catches hidden watermarks; the upper bound catches an overlay so opaque that it obliterates content. Render at a low DPI to keep the check fast across large packs — visibility does not need detail.
FAQ
How do I remove an old underlay watermark before adding a new one? If it was merged as content, it is part of the page stream and cannot be removed cleanly without editing operators. Regenerate from the unstamped source instead; keeping unstamped originals is the practical rule for any watermarking workflow.
Why is the watermark visible in one viewer and not another? Viewers differ in how they handle annotation flags, optional content defaults and blend modes. Plain translucent content renders the same everywhere, which is the main reason to prefer it.
Why did the watermark disappear only after flattening? It was an annotation, and the flattening tool ignored annotations. Bake annotations into content before flattening, or stamp content directly.
Can I put the watermark behind images but above backgrounds? Only by inserting it at a specific position in the content stream, which requires parsing the stream operators. Translucent overlays are far simpler and look similar.
Does transparency increase file size? Negligibly — one graphics state object per page size.
Why is the watermark grey instead of red when printed? Printers in grayscale mode convert colour. Choose a colour that remains visible in grey, such as a mid-tone red or dark grey at low opacity.
Related
- Add a Text Watermark to Every PDF Page — sizing and rotation for mixed pages
- Watermarking and Securing PDFs — images, stamps and passwords
- Fix PDF Form Fields Not Visible After Filling — another appearance problem that differs by viewer
- Redacting Sensitive Data in PDFs — covering content that must actually be removed
Part of Watermarking and Securing PDFs.