#1484·pdfcpu

watermark: source page /Rotate is ignored when sizing and positioning a PDF watermark

Author: saukifCreated Sep 14, 2026Updated Sep 16, 2026
Labelsbug

What happens

When the watermark source page has a /Rotate entry, pdfcpu computes the watermark's scale and position from the unrotated MediaBox, and applies the rotation afterwards to the placement matrix.

So a page stored landscape with /Rotate 270 is placed in the right orientation, but at the wrong size and in the wrong place.

This came up with a scanned PDF used as the watermark. Scanners commonly store a portrait A4 page landscape and set /Rotate 270, so it is stored 842x595 but displays as 595x842. Overlaying it on a 595x842 portrait document should be a 1:1 fit, but pdfcpu measures the unrotated 842 wide box, shrinks the watermark by 29% and centres it on the wrong box. Any scanned page saved this way is affected.

Reproduce

Build a rotated watermark page and a matching destination page from any single page PDF (in.pdf):

bash
pdfcpu resize -- "dim:842 595, enforce:on" in.pdf land.pdf
pdfcpu rotate -- land.pdf 270 wm.pdf     # MediaBox 842x595, /Rotate 270, displays 595x842
pdfcpu resize -- "dim:595 842, enforce:on" in.pdf dest.pdf

wm.pdf displays at exactly the size of dest.pdf, so applying it at scale:1 rel should cover the page 1:1:

bash
pdfcpu watermark add -m pdf -- "wm.pdf:1" "scale:1 rel, position:c, rotation:0" dest.pdf out.pdf

Instead the watermark is scaled by 0.70665 (= 595/842, the unrotated aspect ratio) and shifted about 508pt / 123pt off position. Expected is scale 1.0 at offset 0,0.

scale:1 abs fixes the size but not the position, because the anchor is still computed on the unrotated box.

Also affects multi page watermarks

The source rotation is carried by subtracting it from the watermark rotation, and that happens once per page, so it accumulates across a multi page watermark: -270 - 270 = 180. Every page then comes out upside down.

Cause

In pkg/pdfcpu/stamp.go, createPDFRes takes the watermark's visible region from viewPort(inhPAttrs) without accounting for /Rotate. That region is what the bounding box is scaled against and what the anchor is positioned against, so both are computed in the wrong orientation.

normalizePageWatermark in the same file already does the matching box swap for the destination page — it is just never applied to the watermark source page.

Environment

Reproduced on v0.12.1, v0.13.0 and master (105c0c28), so not a regression. go1.25.0, linux/amd64.