Skip to content

0033: The seal honours the page's rotation ​

Status: implemented.

Context ​

/Rotate turns a page clockwise for display and leaves its coordinate system alone (ISO 32000-1 §7.7.3.3). SealPlacement is documented in terms of where the seal appears, and the caller giving those coordinates is looking at the document in a reader, where it is already turned.

The two were never reconciled. The placement went straight into /Rect, and grep -rn "Rotate" src/ returned nothing at all: the key was read nowhere.

Measured on a page carrying /Rotate 90, with a placement of (60, 400):

Rect[60 400 180 460]

The caller's numbers, untouched. On screen that is somewhere else entirely, the seal reads sideways, and on a page rotated 90 or 270 it can fall outside the visible area, since the displayed width and height have swapped.

/Rotate 90 is how most scanners and many generators express landscape. This was not an exotic input.

Decision ​

The placement is where the seal appears, and the file records where that is.

Two things follow, and only doing the first would be worse than doing neither: the seal would land correctly and read sideways.

  • /Rect is mapped from displayed coordinates into user space. For a quarter turn clockwise the user-space origin, the bottom left, is displayed at the top left, so a displayed point (x, y) sits at user (width − y, x).
  • The appearance carries a /Matrix turning it the other way, because a form XObject is drawn in user space and the display rotation applies to it too. No translation is needed: the reader maps the transformed bounding box onto /Rect (§12.5.5).

/Rotate and /MediaBox are read with inheritance, through /Parent. Both are inheritable (§7.7.3.4, Table 30), and one declaration on /Pages is the ordinary way to say "this document is landscape". Reading the page object alone would have missed the common case.

Geometry is per page, not per document. A file can carry a landscape scan beside a portrait cover, and SealPlacement can put a stamp on both.

Verification ​

The rectangle is asserted at every rotation, including 360 and −90, which normalise to no turn and to 270.

The claim that matters is about what a reader shows, so it was checked with one. Both documents rendered with poppler's pdftoppm at 40 dpi, and the ink located:

Asked for, as displayedRendered
No rotationx 10-30%, y 45-52%x 10-30%, y 45-51%
/Rotate 90x 7-21%, y 23-33%x 7-21%, y 24-33%

The rotated page's ink also spans 120 pt across and 60 pt down, so the seal is neither distorted nor turned on screen.

Consequences ​

  • A document with no rotation produces the bytes it did before. The mapping returns its input and no /Matrix is written, so every committed sample and every conformance verdict measured against them is unaffected.
  • A page with no /MediaBox anywhere above it behaves as unrotated. The mapping needs the page's size and inventing one would put the seal somewhere arbitrary; landing where it used to is at least predictable.
  • Signing\Incremental\PageGeometry is where the mapping lives, so the arithmetic is testable without signing anything.

Alternatives rejected ​

Why not
Treat the placement as user space and let the caller compensateThe caller is looking at a reader. The package is the only party that knows the page is turned
/MK <</R 90>> on the widgetReaders vary in whether they honour it. A matrix on the appearance is what the rendering algorithm is specified to apply
Read /Rotate from the page onlyMisses the document declared landscape once on /Pages, which is the common shape
Fix the rectangle and leave the appearanceThe seal lands correctly and reads sideways, which looks like a different bug

Outcome, 2026-08-18 ​

Signing\Incremental\PageGeometry was built here to answer "where does a displayed point live in user space", and it read /Rotate and /MediaBox. Two entries that decide the same question were not read at all, which grep -rn 'CropBox\|UserUnit' src/ said plainly: it returned /MediaBox and nothing else.

  • /CropBox is the region a reader displays (§7.7.3.3), and a CAD or plotter export routinely crops smaller than the sheet. A placement resolved against the sheet then lands somewhere other than the corner that was asked for, and at worst outside the visible area while the code reports a placed seal. The effective box is the intersection of crop and media, which the same clause requires, and x and y are measured from its corner.
  • /UserUnit multiplies every coordinate on the page (§14.11.1). A seal sized in points on an A0 plot at /UserUnit 2 came out half the intended physical size. Sizes and offsets are divided by it.

The order is the load-bearing part, and it is the combination this record's own subject makes delicate: scale, then rotate inside the visible box, then move by the box's origin. A rotated page with an inset crop box is where an off-by-one origin hides, and it has a test of its own.

A seal that would fall outside the visible area now raises rather than being written off the page, which is 0017's rule one level down from the page it settled.

A page declaring neither entry produces exactly the bytes it did before, which is asserted rather than assumed.

Released under the MIT Licence.