Files
pl_mail/assets/pdf_geometry.d.ts
T
Paul LütznerandClaude Opus 5 e732178f4d feat: sign a PDF by drawing on it, and reply with the signed copy
Draw a signature in the reader, place it on the page, drag it where it
belongs, and either reply with the signed file attached or download it.

It is a VISUAL signature — a picture of a name on a page, worth what a
printed-signed-scanned copy is worth. No certificate, no proof the
document is unaltered, and nothing in the interface says otherwise. A
password-protected PDF is refused with a reason rather than signed by
stripping the protection it was given.

The stamping happens in the browser. The server sees the result only if
you reply with it, and there is no signing service in the path.

GEOMETRY

assets/pdf_geometry.js holds the arithmetic, because it is the part that
goes wrong. Placement is stored in fractions of the page from the moment
it is committed, so it survives scrolling, zooming and a resized window;
the box on screen is always derived from that.

Two corrections to what was planned, each found by a test:

The MediaBox origin is subtracted NOWHERE. pdf.js builds its viewport
transform around the viewBox centre, so convertToPdfPoint() already
answers in absolute user space, and pdf-lib draws into that same space.
Subtracting "to be safe" introduces exactly the inch of error it looks
like it prevents.

The stamp cannot be position: fixed. It belongs to the page, so it has
to scroll with the document — and fixed does not escape a clipping
ancestor with a backdrop-filter, which is what the modal panel is.

TESTS

pdf-geometry.spec.ts covers all four rotation anchors as pure functions.
pdf-sign.spec.ts signs the awkward fixture — page 2 is rotated a quarter
turn with an offset MediaBox — and measures the centroid of the ink
signing ADDED, comparing the signed page against the original.

That assertion started as a count per quadrant and was worthless: a
stamp misplaced by a sixth of a page stays inside its quadrant, so it
passed against a deliberately broken rotation-90 anchor. The centroid
version moves from 0.25 to 0.08 under the same mutation.

SERVER

POST /compose/reply-signed/{id} creates the reply and attaches in one
request. CSRF checked, type established by finfo over the bytes rather
than from the client's filename, size held to the same 25 MB ceiling,
and the stored name derived from the part being signed.

DraftAttachmentService gains storeBytes()/attachBytes(), the "bytes to a
part on a draft" primitive that IntegrationFilePicker already had in the
wrong class and now delegates to.

DEMO

The demo invoice says outright that nothing needs signing, so trying the
feature there was a bad first impression of it. There is a document that
asks to be signed now, and each demo attachment carries its own contents
rather than every one of them being the same invoice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:54:36 +02:00

57 lines
1.6 KiB
TypeScript

/**
* Types for assets/pdf_geometry.js, so the specs that import it are checked.
*
* A declaration file rather than `allowJs`, which would pull every controller
* in assets/ into the TypeScript run — those are plain JavaScript by choice and
* are exercised through the browser, not the compiler.
*
* This is the contract, and it is worth reading as one: everything here is
* expressed in PDF user-space points except `Box`, which is in client pixels.
* The module's own docblock says why that direction is the only one allowed.
*/
/** A rectangle on screen, in client coordinates. */
export interface Box {
left: number;
top: number;
width: number;
height: number;
}
/** As much of pdf.js's PageViewport as any of this needs. */
export interface Viewport {
width: number;
height: number;
rotation?: number;
convertToPdfPoint(x: number, y: number): number[];
}
/** A rectangle in PDF user space; width and height are the stamp's own. */
export interface PdfRect {
x: number;
y: number;
width: number;
height: number;
}
/** A PdfRect moved to the corner pdf-lib rotates about, and by how much. */
export interface Anchor extends PdfRect {
rotate: number;
}
export function viewportToPdfPoint(
viewport: Viewport,
rect: Box,
clientX: number,
clientY: number,
): [number, number];
export function pdfRect(viewport: Viewport, rect: Box, box: Box): PdfRect;
export function stampAnchor(rect: PdfRect, rotation: number): Anchor;
export function trimAlpha(
image: { data: Uint8ClampedArray; width: number; height: number },
threshold?: number,
): Box | null;