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.

Paint order on a slide-export page The page content is painted bottom to top. A watermark merged under the content is painted first. Then the exported slide paints a full-page white background rectangle, which covers the watermark completely. Text, charts and shaded table cells follow on top. A watermark merged over the content with low opacity is painted last and remains visible above everything. Watermark (over=True, 15% opacity) painted last visible above everything Text and charts slide content covers anything below Shaded table cells opaque fills hides patches of an underlay Full-page white rectangle slide background hides an underlay entirely Watermark (over=False) painted first invisible on this page

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.

Underlay versus translucent overlay The left panel shows merge page with over False and an opaque stamp, visible on text-only pages but hidden on slide exports, scans and shaded tables. The right panel shows merge page with over True and an 18 percent opacity fill, visible on every page type while text and images underneath stay readable. over=False, opaque text-only pages: visible slide exports: hidden scanned pages: hidden shaded tables: patches content readable: yes over=True, alpha 0.18 text-only pages: visible slide exports: visible scanned pages: visible shaded tables: visible content readable: yes

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.

Watermark methods and where they show A content underlay is hidden by opaque backgrounds on screen and in print and is hard to remove. A content overlay with transparency is visible on screen and in print and is hard to remove. An annotation stamp depends on its flags for screen and print visibility and is easy to delete. An optional content layer depends on its view and print settings and can be switched off by readers. Method On screen In print Removal Content underlay hidden by backgrounds hidden by backgrounds hard Translucent overlay visible visible hard Annotation stamp depends on flags depends on flags easy Optional content layer layer setting layer setting switchable

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.

Part of Watermarking and Securing PDFs.

/html>