implements page-numbers-plan
This commit is contained in:
1 parent
39da8dac4d
commit
b5279434b2
19 files changed
+1823
-339
No files matched your search
@@ -0,0 +1,250 @@
|
||||
# Plan: Paginierung — fortlaufende Blattnummern mit Aktenzeichen
|
||||
|
||||
## Goal
|
||||
|
||||
Add a new tool **„Paginieren“** (`/tools/pagination`) that stamps every page of one PDF with an
|
||||
ascending sheet number — the way authorities paginate a case file („Bl. 26“) — optionally combined
|
||||
with a static free text (e.g. an Aktenzeichen `123 Ja 60/26`) that repeats on every page (default) or
|
||||
only on the first page. The same operation is offered as a follow-up action after **„PDFs
|
||||
zusammenfügen“**.
|
||||
|
||||
Context: mail thread with Nikolai Klute (26.09.2026 / 06.10.2026). Two clarifications shape this
|
||||
plan: pagination needs a _simple_ free text next to the tool-generated number (no `{0}/{1}` template
|
||||
language), and that text should repeat „auf der ersten oder auf jeder Seite (Default)“.
|
||||
|
||||
Related: `plan/done/stamp-plan.md` already references this plan as `page-numbers-plan`.
|
||||
|
||||
---
|
||||
|
||||
## Open decisions (defaults marked ✅)
|
||||
|
||||
### D1 — Free-text model
|
||||
|
||||
- ✅ **A. Static free text + tool-generated number.** No placeholder syntax: the number is always
|
||||
produced by the tool, the free text is an optional label rendered with it. Directly matches
|
||||
Nikolai's answer („Einfach nur ein Text im Zusammenhang mit der Seitenzahl“).
|
||||
- B. Template field (`Seite {n} von {total}`). Rejected for v1: more validation and support surface
|
||||
than the use case needs; `{total}` is not requested.
|
||||
|
||||
### D2 — What repeats, what increments
|
||||
|
||||
- ✅ **A. Two independent scopes.** Page numbers always on every page (that is the point of
|
||||
pagination). The free text has its own scope: „Auf jeder Seite“ (default) or „Nur auf der ersten
|
||||
Seite“ — on page 2+ only the number remains.
|
||||
- B. One shared scope (text scope also gates numbering). Rejected: a first-page-only pagination is
|
||||
not a thing.
|
||||
|
||||
### D3 — Number format
|
||||
|
||||
- ✅ **A. Select with four fixed formats** — `Bl. {n}` (default), `Seite {n}`, `S. {n}`, `{n}`.
|
||||
Internally a prefix/suffix pair, so no user-visible template language, but „Bl.“ as the
|
||||
German-authority default. Free text is prepended as `„{Text}, “`.
|
||||
- B. Free prefix/suffix inputs. More knobs, more invalid combinations.
|
||||
- C. One free format string. Reopened D1.
|
||||
|
||||
### D4 — Look and placement
|
||||
|
||||
- ✅ **A. Restrained authority style:** Helvetica, 11 pt (slider 8–18), black `#1a1a1a`, no frame,
|
||||
no rotation, default position **unten mittig** with 28 pt margin; the six other positions are
|
||||
selectable via the existing `getPositionCoords` presets.
|
||||
- B. Reuse the stamp tool's framed/bold look. Wrong semantics: a page number is a neutral mark, not
|
||||
an authority stamp.
|
||||
|
||||
### D5 — Files per run
|
||||
|
||||
- ✅ **A. Exactly one PDF.** A Gesamtdokument is paginated in one pass; multi-document cases are
|
||||
handled by „PDFs zusammenfügen“ + handoff. Continuous numbering across several dropped files is a
|
||||
different (and silently wrong) semantic.
|
||||
- B. Batch with the same start number per file. Ambiguous for users; deferred until requested.
|
||||
|
||||
### D6 — Integration into „PDF Zusammenfügen“
|
||||
|
||||
- ✅ **A. Handoff to the pagination tool.** Merge writes the merged bytes into a `File`, queues it via
|
||||
the existing in-memory file handoff, and navigates to `/tools/pagination`, where options and live
|
||||
preview live. One implementation of pagination, preview included.
|
||||
- B. Inline pagination options on the merge page. Duplicated UI and no preview.
|
||||
|
||||
### D7 — Preview renderer
|
||||
|
||||
- ✅ **A. On-demand single-page renderer.** A paginated case file can have hundreds of pages;
|
||||
`loadPdfDocument` rasterises _every_ page up front. A new
|
||||
`loadPdfPageRenderer(bytes)` renders only the visible page and disposes the previous one.
|
||||
- B. Reuse `loadPdfDocument` (all thumbnails). Unacceptable for long documents.
|
||||
|
||||
---
|
||||
|
||||
## UX
|
||||
|
||||
Route `/tools/pagination`, title „Paginieren“, registry entry after `stamp`:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'pagination',
|
||||
title: 'Paginieren',
|
||||
description: 'Versehen Sie alle Seiten mit fortlaufenden Blattnummern und optional einem Aktenzeichen.',
|
||||
icon: 'list-ordered'
|
||||
}
|
||||
```
|
||||
|
||||
Page layout (stamp tool as the pattern: options left, preview right):
|
||||
|
||||
- **FileDropzone** (single PDF) — „PDF zum Paginieren hinzufügen“; shows the file name and page count
|
||||
after loading.
|
||||
- **Options card**
|
||||
- **Startnummer** (number input, default 1, min 1, max 99999) with hint „Seite 1 erhält diese
|
||||
Nummer.“
|
||||
- **Zusatztext (optional)** — e.g. Aktenzeichen, `maxlength="60"`, placeholder
|
||||
„z. B. 123 Ja 60/26“, plus radio/checkbox „Auf jeder Seite“ (default) / „Nur auf der ersten
|
||||
Seite“.
|
||||
- **Zahlenformat** select: `Bl. {n}`, `Seite {n}`, `S. {n}`, `{n}`.
|
||||
- **Position** select (default „Unten mittig“), **Schriftgröße** slider 8–18 pt (default 11).
|
||||
- Live example line: `123 Ja 60/26, Bl. 26`.
|
||||
- **Preview card** — the current page rendered at reading size with an absolutely positioned overlay
|
||||
(same technique as the stamp preview), page counter „Seite 3 von 42“, prev/next buttons and a
|
||||
„Springe zu Seite“ number input (long documents). The overlay shows the number that page will
|
||||
actually receive (`start + pageIndex`). Label it „Annäherung an die Ausgabe im PDF“.
|
||||
- **Primary action** „Paginieren & herunterladen“ → `{base}_paginiert.pdf`. Options are read at click
|
||||
time, so the button can be pressed again after changing settings.
|
||||
- Errors: encrypted PDF → the shared German hint pointing to „Passwort entfernen“; text too long for
|
||||
a page → German error instead of silent clipping.
|
||||
|
||||
## Technical approach
|
||||
|
||||
### Shared text overlay (`src/lib/pdf-text-overlay.ts`)
|
||||
|
||||
Extract the reusable parts of `src/routes/tools/stamp/+page.svelte` into a module used by stamp,
|
||||
pagination and exhibit numbering:
|
||||
|
||||
```ts
|
||||
export type TextOverlayOptions = {
|
||||
font: PDFFont;
|
||||
text: string;
|
||||
size: number;
|
||||
color: RGB;
|
||||
position: OverlayPosition;
|
||||
margin?: number;
|
||||
padding?: number;
|
||||
framed?: boolean;
|
||||
borderWidth?: number;
|
||||
rotation?: number; // degrees
|
||||
minSize?: number; // shrink floor before throwing
|
||||
compensatePageRotation?: boolean; // default false; new tools set true
|
||||
};
|
||||
|
||||
export const drawTextOverlay = (page: PDFPage, options: TextOverlayOptions) => void;
|
||||
```
|
||||
|
||||
- Owns fit-to-page shrinking (loop down to `minSize`), the rotated placement maths already present in
|
||||
the stamp route (`getRotatedStampPlacement`), optional border, and the text draw.
|
||||
- The stamp route is rewired to call it with `framed: true`, `rotation: value` and its bold font;
|
||||
behaviour must stay identical (guard: `tests/e2e/stamp.spec.ts`).
|
||||
- `compensatePageRotation` maps bottom-center on a `/Rotate 90` scan to the correct user-space anchor
|
||||
and counter-rotates the text. Stamp keeps `false` so its output does not change.
|
||||
- Move `isEncryptedPdfError` / the encrypted-PDF message into `src/lib/pdf-errors.ts` so all three
|
||||
tools share one wording.
|
||||
|
||||
### Pagination
|
||||
|
||||
```ts
|
||||
const numberText = `${formatPrefix}${startNumber + pageIndex}${formatSuffix}`;
|
||||
const textVisible = textScope === 'all' || pageIndex === 0;
|
||||
const line = textVisible && freeText.trim() ? `${freeText.trim()}, ${numberText}` : numberText;
|
||||
|
||||
for (const [pageIndex, page] of pdfDoc.getPages().entries()) {
|
||||
drawTextOverlay(page, {
|
||||
font,
|
||||
text: line,
|
||||
size,
|
||||
color: parseHexColor('#1a1a1a'),
|
||||
position,
|
||||
margin: 28,
|
||||
compensatePageRotation: true
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- Embed one `StandardFonts.Helvetica` per document, save, name `${base}_paginiert.pdf`.
|
||||
- Page count comes from pdfjs (`numPages`) for the preview and from pdf-lib for processing; both are
|
||||
cheap, no full rasterisation.
|
||||
- **Renderer:** extend `src/lib/pdf-thumbnails.ts` with
|
||||
`loadPdfPageRenderer(bytes): Promise<{ pageCount, render(pageNumber), destroy() }>`; `render`
|
||||
builds the canvas from the requested page's own viewport (mixed page sizes stay correct), revokes
|
||||
the previous object URL, and `destroy` tears down the pdfjs loading task. Reused by the exhibit
|
||||
tool's page-1 preview.
|
||||
|
||||
### Merge integration
|
||||
|
||||
- Generalise `src/lib/pending-files.ts`: `queueZipFiles` / `takeQueuedZipFiles` become
|
||||
`queuePendingFiles` / `takePendingFiles` (same behaviour; update `src/routes/+page.svelte` and
|
||||
`src/routes/tools/bea/+page.svelte`).
|
||||
- `src/routes/tools/merge/+page.svelte`: keep „PDFs zusammenfügen“ (direct download) and add
|
||||
„Zusammenfügen & paginieren“, which merges, wraps the bytes in
|
||||
`new File([bytes], 'zusammengefuegt.pdf', { type: 'application/pdf' })`, queues it and calls
|
||||
`goto('/tools/pagination')`. Keep the change minimal — the page still uses older styling tokens;
|
||||
do not restyle it in this task.
|
||||
- The pagination route consumes `takePendingFiles()` in `onMount`, accepts the first PDF and ignores
|
||||
anything else defensively.
|
||||
|
||||
### Registry and docs
|
||||
|
||||
- `src/lib/tools.ts` + `iconMap` in `src/routes/+page.svelte` (`ListOrdered`).
|
||||
- `agents.md`: new route, shared overlay/error helpers, renderer, pending-file naming, merge handoff.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `src/routes/tools/pagination/+page.svelte` — new route.
|
||||
- `src/lib/pdf-text-overlay.ts`, `src/lib/pdf-errors.ts` — extracted shared helpers.
|
||||
- `src/routes/tools/stamp/+page.svelte` — use the shared helpers (no behaviour change).
|
||||
- `src/lib/pdf-thumbnails.ts` — add `loadPdfPageRenderer`.
|
||||
- `src/lib/pending-files.ts`, `src/routes/+page.svelte`, `src/routes/tools/bea/+page.svelte` —
|
||||
generic pending-file naming.
|
||||
- `src/routes/tools/merge/+page.svelte` — handoff button.
|
||||
- `src/lib/tools.ts`, `src/routes/+page.svelte` — registry + icon map.
|
||||
- `tests/e2e/pagination.spec.ts`, `tests/e2e/merge.spec.ts` — new/updated specs.
|
||||
- `agents.md` — structure and flow documentation.
|
||||
|
||||
## Implementation phases
|
||||
|
||||
1. Extract `pdf-text-overlay.ts` + `pdf-errors.ts`, rewire stamp, keep `stamp.spec.ts` green.
|
||||
2. Add `loadPdfPageRenderer` and use it for the preview.
|
||||
3. Build the pagination route (options, live preview, export, errors).
|
||||
4. Merge handoff + generic pending files.
|
||||
5. Registry/icon/docs.
|
||||
6. Playwright coverage + command gauntlet.
|
||||
|
||||
## Verification
|
||||
|
||||
- 3-page PDF, start 26, text `123 Ja 60/26`, all pages → each page's content stream contains
|
||||
`123 Ja 60/26, Bl. 26/27/28`; page count and page order preserved; download named
|
||||
`akte_paginiert.pdf`.
|
||||
- First-page-only text → page 1 contains the Aktenzeichen, page 2 contains `Bl. 2` but not the
|
||||
Aktenzeichen; the original page content is preserved on every page.
|
||||
- Formats `Seite {n}` / `{n}` and empty free text → expected strings.
|
||||
- Preview: navigating to page 2 shows `Bl. 27` (start 26); changing the start number updates the
|
||||
preview before export.
|
||||
- Rotated input (`createPreRotatedPdf`) → number appears at the visual bottom-center (test asserts
|
||||
the draw coordinates in the content stream or compares stream text plus position).
|
||||
- Merge: two PDFs (1 + 2 pages) → „Zusammenfügen & paginieren“ lands on `/tools/pagination` with a
|
||||
3-page document; running produces continuous `Bl. 1…3`.
|
||||
- Encrypted PDF → German error, no download.
|
||||
- `pnpm check`, targeted `pnpm test:e2e`, `pnpm build`, Prettier on touched files.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Long documents:** never call `loadPdfDocument` here; one page in memory at a time. The preview
|
||||
navigation must not leak object URLs or pdfjs tasks.
|
||||
- **Text length on narrow pages:** fit-to-page shrinks to `minSize`; below that, fail with a German
|
||||
message rather than clipping.
|
||||
- **Unsupported characters:** StandardFonts use WinAnsi; validate the free text (e.g. no emoji) and
|
||||
show a German hint instead of a pdf-lib save error.
|
||||
- **Rotated pages:** compensation is new logic; if the mapping turns out unreliable, ship v1 with a
|
||||
visible warning for rotated pages instead of silently misplaced numbers.
|
||||
- **Preview fidelity:** browser font metrics differ slightly from pdf-lib; the panel is labelled as
|
||||
an approximation.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Placeholder templates (`{total}`, Roman numerals, ranges) and numbering only selected pages.
|
||||
- Multiple files per run with continuous numbering (use merge first).
|
||||
- Persisted pagination settings.
|
||||
- Automatic pagination inside the beA archive export.
|
||||
Reference in new issue
Block a user