Guides ·
The PDF /Rotate flag, and why your page number lands in the wrong corner
A PDF page has two coordinate spaces — the one it is stored in and the one you see. On a rotated scan they differ by 90 degrees, and anything you draw goes in sideways.
Because the page carries a /Rotate flag, so the coordinates it is stored in are not the coordinates you see. On a page rotated 90 degrees the stored width is the visible height, and a mark placed at the visible top-right has to be written to the stored top-left.
Two coordinate spaces
Every PDF page has a /MediaBox giving its size, and may also carry /Rotate — a multiple of 90 telling the viewer to turn the page before showing it. Scanners set it constantly, because turning a flag is far cheaper than re-encoding the image.
The consequence is that a page has two coordinate spaces:
- Stored space, which is what the drawing operators use.
- Visible space, which is what the reader sees and what you were thinking in when you said “bottom right”.
With /Rotate 0 they are the same and everything works. With /Rotate 90 they differ by a quarter turn, and code written against stored coordinates puts the mark somewhere the reader did not ask for.
/Rotate | Stored size | Visible size | Visible bottom-right is stored… |
|---|---|---|---|
| 0 | 595 × 842 | 595 × 842 | bottom-right (595, 0) |
| 90 | 595 × 842 | 842 × 595 | top-right (595, 842) |
| 180 | 595 × 842 | 595 × 842 | top-left (0, 842) |
| 270 | 595 × 842 | 842 × 595 | bottom-left (0, 0) |
Note the middle column. On a 90 or 270 page the width and height swap. Code that computes “ten points in from the right edge” using page.getSize().width on a rotated A4 page is measuring against 595 when the reader sees 842 — so the mark is not merely rotated, it is 247 points from where it should be, frequently off the page entirely.
What it looks like when it goes wrong
- Page numbers on the long edge of a landscape scan instead of the short one.
- A watermark reading bottom-to-top up the side of the page.
- A signature that vanishes: placed outside the visible area, still in the file, invisible on screen.
- A document where some pages are stamped correctly and others are not — which is the common case, because a scanned bundle often has a mix of rotations.
That last one is the giveaway. A tool that gets every page wrong has a different bug; a tool that gets the upright pages right and the rotated ones wrong is ignoring /Rotate.
Getting it right
Three things have to be handled, and they are separate.
1. Normalise the angle. It can be negative or a multiple of 360, and a malformed file can carry a value that is not a multiple of 90 at all. Most libraries will not let you set such a value — pdf-lib throws — but they will happily hand you one they parsed out of an existing document.
const raw = page.getRotation().angle;
const rotation = (((Math.round(raw / 90) * 90) % 360) + 360) % 360;
2. Swap the dimensions when the page is on its side, so anchoring maths uses the size the reader sees:
const { width, height } = page.getSize();
const swapped = rotation === 90 || rotation === 270;
const visible = { width: swapped ? height : width, height: swapped ? width : height };
3. Convert the point back into stored space before drawing. Both spaces put the origin at the bottom left; the whole difference is the display rotation.
function toStored(page, vx, vy) {
const { width, height } = page.getSize();
switch (rotation) {
case 90: return { x: width - vy, y: vx };
case 180: return { x: width - vx, y: height - vy };
case 270: return { x: vy, y: height - vx };
default: return { x: vx, y: vy };
}
}
Derive those cases from where the four corners actually land rather than copying a remembered formula. A sign error here puts the mark off the page, and the failure only appears on rotated documents — so it passes every test written with an upright PDF.
4. Turn the text itself. Content drawn at angle 0 in stored space appears turned clockwise by the display rotation, so it has to be turned the same amount anticlockwise to read upright. On a /Rotate 90 page, text drawn at 90 degrees comes out level.
Placing the mark correctly but leaving it at angle 0 gives you a page number in the right corner, printed sideways.
Why this is hard to notice
Almost every PDF anyone tests with has /Rotate 0. Take a document from a word processor, an invoice generator or a browser’s print-to-PDF and the flag is zero, both coordinate spaces coincide, and code that ignores rotation entirely is indistinguishable from code that handles it.
The flag turns up on the documents people actually stamp: scans, faxes, phone photographs of paper, and anything that has been through a document management system. So the bug is absent from the test corpus and present in production, which is the worst distribution a bug can have.
A useful test asset is one A4 page saved four times with /Rotate set to 0, 90, 180 and 270. Stamp all four and look at them. If the mark is in the same visible corner and the right way up on each, the code is correct.
Rotating a page properly
Worth separating from the above: to turn a page rather than draw on one, change the flag. Do not re-render.
page.setRotation(degrees((rotation + 90) % 360));
That is a structural edit. The text stays selectable, the fonts stay embedded, image quality is untouched and the file barely changes size. Rasterising the page to turn it converts a searchable document into a picture of one — and it is a one-way trip.
The PDF tools here all take the structural route, and the ones that draw — page numbers, watermarks, signatures — honour /Rotate, so a sideways scan is marked where you see it rather than where the file happens to store it. The tests place a mark on pages of equal visible size at different rotations and assert it lands in the same visible place, which is the only assertion that actually catches this.