6.0 KiB
6.0 KiB
Plan: Seiten drehen
Goal
Add a new tool "Seiten drehen" (/tools/rotate) that fixes page orientation — the classic
case being scanned court mail with individual pages rotated 90°/180°. Users rotate pages
individually or all at once; the result is saved via the standard PDF /Rotate mechanism, so the
operation is lossless and instant (no rasterization, no re-encoding).
No new dependencies.
Open decisions (defaults marked ✅)
D1 — How rotation is applied
- ✅ A. pdf-lib
setRotation(document-level/Rotate).page.setRotation(degrees(angle))writes the page's rotation entry; every viewer honors it. Lossless, fast, file size unchanged. Existing rotation of a page (from scanners that already set/Rotate) is read viapage.getRotation().angleand added to, so the user's delta is always applied on top of the current state. - B. Rasterize-and-rotate pixels. Unnecessary quality loss and memory cost; rejected.
D2 — Grid capabilities
- ✅ Extend
PdfPageGridwith an optional action snippet. The current grid renders fixed cards. Add an optional snippet prop (e.g.pageActions) rendered below each thumbnail; existing call sites (separate, watermark, extract) pass nothing and look unchanged. The rotate page uses it for per-page rotate buttons and a rotation badge. - B. Fork the grid inside the rotate page. Rejected: duplicates the accessible grid work.
UX
- Route
/tools/rotate, newsrc/lib/tools.tsentry:{ slug: 'rotate', title: 'Seiten drehen', description: 'Drehen Sie einzelne oder alle Seiten eines PDFs.', icon: 'rotate-cw' }- icon map entry.
- After load (
loadPdfDocument+ thumbnails like separate):- Quick-action bar: "Alle 90° rechts", "Alle 90° links", "Alle 180°", "Zurücksetzen" (clears all deltas).
- Page grid: each card shows the thumbnail with a CSS rotation preview
(
transform: rotate({total}deg)on the<img>, contained in a square-ish frame so rotated pages still fit), a small badge when the total rotation is non-zero (e.g. "90°"), and two icon buttons (rotate left / rotate right) provided through the newpageActionssnippet. Buttons are real<button>elements witharia-label="Seite 3 um 90° nach rechts drehen". - Count label: "4 von 12 Seiten gedreht".
- Primary button "Drehen & herunterladen" →
{baseName}_gedreht.pdf. Disabled when no page has a non-zero total rotation change.
- Keyboard operability is inherent (all buttons); no drag interactions required at all.
Technical approach
State
let rotations = $state(new Map<number, number>()); // pageNumber → delta degrees (0/90/180/270)
rotatePage(pageNumber, delta):next = ((current + delta) % 360 + 360) % 360; store; delete entry when back to 0 (keeps "gedreht" count honest).- Preview angle:
originalRotation(page) + delta— the thumbnail renderer cannot know the original/Rotatewithout pdf-lib; simplest correct-enough approach: apply only the delta to the preview (pdfjs already renders pages honoring/Rotate, so the preview matches what the user sees plus their delta). No need to read pdf-lib during preview.
Export
const pdfDoc = await PDFDocument.load(bytes);
for (const page of pdfDoc.getPages()) {
const delta = rotations.get(pageIndexPlusOne);
if (!delta) continue;
const current = page.getRotation().angle; // respects existing /Rotate
page.setRotation(degrees((((current + delta) % 360) + 360) % 360));
}
downloadBlob(await pdfDoc.save(), `${baseName}_gedreht.pdf`);
- Instant for any file size — no rasterization limits needed, but keep the shared load error handling (encrypted files: pdf-lib load fails → German error; optionally hint to decrypt like the metadata plan does).
PdfPageGrid extension (backward compatible)
type Props = {
thumbnails: PdfThumbnail[];
selectedPages?: Set<number>;
onPageClick?: (pageNumber: number) => void;
thumbnailWidth?: number;
class?: string;
pageActions?: Snippet<[pageNumber: number]>; // NEW, optional
cardClass?: (pageNumber: number) => string; // NEW, optional — rotate preview transform hook
};
- When
pageActionsis provided, render{@render pageActions(thumb.pageNumber)}inside the card. WhencardClassis provided, merge it into the image wrapper viacn(). Existing pages need no changes.
Files changed
src/lib/components/PdfPageGrid.svelte— optional snippet/class props.src/routes/tools/rotate/+page.svelte— new route.src/lib/tools.ts,src/routes/+page.svelte— registry + icon map.tests/e2e/toolbox.spec.ts— spec block.agents.md— structure update.
Implementation phases
PdfPageGridextension; run existing e2e to prove separate/watermark visuals unchanged.- Rotate route: state, quick actions, grid with badges/buttons, CSS preview rotation.
- Export via
setRotation. - Registry + icon.
- Playwright: rotate page 2 by 90° twice via its buttons, download, load output with pdf-lib,
assert
getPage(1).getRotation().angle === 180; "Zurücksetzen" disables the download button. - Polish +
pnpm check/lint/build/test:e2e.
Verification
- Single page rotate, all-page rotate, reset flows.
- A PDF whose pages already carry
/Rotate=90gains a correct cumulative rotation (assert in e2e by creating such a fixture with pdf-lib). - Output opens with correct orientation in a viewer; file size ~unchanged.
- Grid cards stay within layout bounds when rotated (object-contain + fixed aspect frame).
Risks
- CSS preview vs. real rotation mismatch for pages that already carry
/Rotate: pdfjs thumbnails render the page as displayed; the preview adds the delta visually — acceptable. - Rotated landscape images inside the grid may look small in portrait cards; use
object-containand a square min-height frame.
Out of scope
- Auto-detection of upside-down scans (needs heuristics/OCR).
- Rotating arbitrary selections only via a separate tool (quick actions + per-page buttons cover it).