The bug: it works until the page is rotated
Signature placement should be simple — the user drags a box on a preview, you draw the signature image inside that box on the real PDF. It worked for straight portrait pages. Then someone signed a scanned contract that a photocopier had helpfully saved with a /Rotate 90 flag, and the signature landed in the margin, sideways.
The root cause: pdf.js (what renders the preview in the browser) applies the page's /Rotate value automatically when displaying it, so the user is looking at and clicking on the visual page. The PDF's own internal coordinate space, which any stamping library writes into, is the unrotated page. If your code treats a click position as if it were already in PDF space, it's off by a 90-degree rotation on any page that isn't upright — and by extension, wrong on any page whose crop box doesn't match its media box.
The fix: own the translation, explicitly
WKFGo's signing package (Go, using pdfcpu instead of the old Python pipeline that broke whenever PyPDF2 or reportlab weren't installed) makes the coordinate contract explicit in the code, not implicit in convention:
The caller always supplies the signature box in normalized visual coordinates — 0 to 1, origin at the top-left, exactly what the user saw and clicked in the browser. The package reads the page's actual visible box and effective rotation, normalized to a right angle:
visible := inhAttrs.MediaBox
if inhAttrs.CropBox != nil {
visible = inhAttrs.CropBox // the region pdf.js actually displays
}
rot := ((inhAttrs.Rotate % 360) + 360) % 360
rot = (rot / 90) * 90
If the page is rotated 90 or 270 degrees, width and height swap for the visual dimensions — because what looks like the page's width on screen is its height in the unrotated PDF space:
if rot == 90 || rot == 270 {
g.VisualW, g.VisualH = visible.Height(), visible.Width()
}
Everything downstream — the box position, the image scaling — is computed in this visual space, then handed to pdfcpu's watermark placement, which independently compensates for /Rotate on its own end. The two compensations meet in the middle and cancel out, instead of one of them silently not happening.
The second bug hiding in the first: aspect ratio
Even with the coordinate space right, stretching a signature image to exactly fill a wide, short box looks wrong — signatures get squashed. The fix mirrors CSS object-fit: contain: scale the image to the largest size that fits inside the box while keeping its aspect ratio, then center it:
scale := boxW / float64(imgW)
if s := boxH / float64(imgH); s < scale {
scale = s
}
fitW, fitH := float64(imgW)*scale, float64(imgH)*scale
fitX := boxX + (boxW-fitW)/2
fitY := boxY + (boxH-fitH)/2
This is the exact behavior the browser preview uses to show the box before signing, so what the signer approved and what lands on the PDF are computed the same way.
The third detail: white background isn't transparent by default
A signature drawn on a canvas is a white rectangle with dark strokes, not a transparent PNG. Stamped as-is, it covers the text underneath with a white box. ProcessSignatureImage converts any near-white pixel (R, G, and B all above 210) to fully transparent before stamping, so the signature overlays cleanly without occluding the document.
Common mistakes
Trusting the "size" fields on a rotated page. MediaBox width/height are always in the PDF's native orientation — read them raw on a 90-degree-rotated page and every calculation is transposed.
Testing only with straight portrait PDFs. The bug is invisible until someone signs a scanned, rotated, or landscape document — which is exactly the kind of document that tends to need a signature in the first place (contracts scanned from a fax, forms photographed sideways).
Hard-coding a white threshold of exactly 255. Compressed or scanned signature images rarely have pure white backgrounds — anti-aliasing and JPEG artifacts leave near-white pixels that a strict == 255 check would leave opaque.
FAQ
Does this handle landscape-oriented PDFs that aren't rotated (just wide pages)?
Yes — orientation from /Rotate and page shape (wide vs. tall) are independent; the visual-space math handles both without special-casing either.
What image formats can a signature be uploaded as?
PNG, JPEG, and GIF decode natively; the output is always a transparency-processed PNG regardless of input format.
Is this tested against real rotation cases, not just the happy path?
Yes — the test suite stamps across all four rotation values combined with both portrait and landscape page geometry, checking the stamp lands in the same visual position a human would expect regardless of how the underlying PDF encodes orientation.
Summary
"The signature is in the wrong place" is rarely a drawing bug — it's almost always a coordinate-space bug, where code silently assumes the space the user sees and the space the file stores are the same one. They're not, the moment rotation enters the picture, and the fix is to make the translation between them an explicit, tested step rather than an assumption.