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.