From 09cc5c06fb39b71b64f9648f30830ef5d7625352 Mon Sep 17 00:00:00 2001 From: rehlert Date: Sat, 29 Aug 2026 12:04:29 +0200 Subject: [PATCH] rotate + hover on start-page --- agents.md | 2 +- src/lib/components/FileDropzone.svelte | 15 +- src/lib/components/PdfPageGrid.svelte | 107 +++++++++--- src/lib/tools.ts | 6 + src/routes/+page.svelte | 31 +--- src/routes/tools/rotate/+page.svelte | 219 +++++++++++++++++++++++++ tests/e2e/toolbox.spec.ts | 80 +++++++++ 7 files changed, 402 insertions(+), 58 deletions(-) create mode 100644 src/routes/tools/rotate/+page.svelte diff --git a/agents.md b/agents.md index f308666..2885bfa 100644 --- a/agents.md +++ b/agents.md @@ -96,7 +96,7 @@ src/ ├── tools/ │ ├── +layout.svelte # Shared back-navigation shell │ ├── bea/+page.svelte # Existing ZIP archive workflow - │ └── {merge,separate,stamp,watermark,encrypt,decrypt,compress,convert}/ + │ └── {merge,separate,stamp,watermark,encrypt,decrypt,compress,convert,rotate}/ ├── datenschutz/+page.svelte # Privacy policy └── impressum/+page.svelte # Imprint static/ # Public logo, footer art, and robots.txt diff --git a/src/lib/components/FileDropzone.svelte b/src/lib/components/FileDropzone.svelte index 78e01be..e7f1f45 100644 --- a/src/lib/components/FileDropzone.svelte +++ b/src/lib/components/FileDropzone.svelte @@ -103,9 +103,9 @@ {#if rejectionMessage} diff --git a/src/lib/components/PdfPageGrid.svelte b/src/lib/components/PdfPageGrid.svelte index 6ab3804..6b8ee3d 100644 --- a/src/lib/components/PdfPageGrid.svelte +++ b/src/lib/components/PdfPageGrid.svelte @@ -1,4 +1,5 @@ @@ -25,30 +30,80 @@ > {#each thumbnails as thumb} {@const isSelected = selectedPages.has(thumb.pageNumber)} - + {#if onPageClick} + + {:else} +
+ {#if cardClass} +
+ Seite {thumb.pageNumber} +
+ {:else} + Seite {thumb.pageNumber} + {/if} + Seite {thumb.pageNumber} + {#if pageActions} + {@render pageActions(thumb.pageNumber)} + {/if} +
+ {/if} {/each} diff --git a/src/lib/tools.ts b/src/lib/tools.ts index a0e9aeb..65d587e 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -48,6 +48,12 @@ export const tools: Tool[] = [ description: 'Entfernen Sie Seiten aus einem PDF.', icon: 'scissors' }, + { + slug: 'rotate', + title: 'Seiten drehen', + description: 'Drehen Sie einzelne oder alle Seiten eines PDFs.', + icon: 'rotate-cw' + }, { slug: 'merge', title: 'PDFs zusammenfügen', diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 18f5ee4..843a2fa 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -9,6 +9,7 @@ Scissors, Merge, Stamp, + RotateCw, FileText } from '@lucide/svelte'; import { goto } from '$app/navigation'; @@ -25,11 +26,10 @@ minimize: Minimize2, scissors: Scissors, merge: Merge, - stamp: Stamp + stamp: Stamp, + 'rotate-cw': RotateCw }; - let isDraggingFiles = $state(false); - const isZipFile = (file: File) => file.name.toLowerCase().endsWith('.zip') || file.type === 'application/zip' || @@ -46,17 +46,11 @@ const handleWindowDragOver = (event: DragEvent) => { if (!event.dataTransfer?.types.includes('Files')) return; event.preventDefault(); - isDraggingFiles = true; - }; - - const handleWindowDragLeave = (event: DragEvent) => { - if (event.relatedTarget === null) isDraggingFiles = false; }; const handleWindowDrop = (event: DragEvent) => { if (!event.dataTransfer?.files.length) return; event.preventDefault(); - isDraggingFiles = false; openBeaFiles(Array.from(event.dataTransfer.files)); }; @@ -65,24 +59,7 @@ beA-Edit — Werkzeugkasten für das besondere elektronische Anwaltspostfach - - -{#if isDraggingFiles} - -{/if} +
diff --git a/src/routes/tools/rotate/+page.svelte b/src/routes/tools/rotate/+page.svelte new file mode 100644 index 0000000..55de1d0 --- /dev/null +++ b/src/routes/tools/rotate/+page.svelte @@ -0,0 +1,219 @@ + + +
+
+

Seiten drehen

+

+ Drehen Sie einzelne oder alle Seiten eines PDFs — verlustfrei und ohne Qualitätsverlust. +

+
+ + {#if !pdfHandle} + + {#if loadingError} +
+ {loadingError} +
+ {/if} + {:else} +
+ +
+

+ {pdfFile?.name} — {pdfHandle.pageCount} Seite{pdfHandle.pageCount === 1 ? '' : 'n'} +

+
+ + + + +
+
+ + + {#snippet pageActions(pageNumber)} + {#if (rotations.get(pageNumber) ?? 0) !== 0} + + {rotations.get(pageNumber)}° + + {/if} +
+ + +
+ {/snippet} +
+ + {#if error} +
+ {error} +
+ {/if} + +
+

+ {rotatedCount} von {pdfHandle.pageCount} Seite{pdfHandle.pageCount === 1 ? '' : 'n'} + gedreht +

+ +
+
+ {/if} +
diff --git a/tests/e2e/toolbox.spec.ts b/tests/e2e/toolbox.spec.ts index c0e4586..9f21669 100644 --- a/tests/e2e/toolbox.spec.ts +++ b/tests/e2e/toolbox.spec.ts @@ -1,6 +1,7 @@ import { expect, test, type Download, type Locator, type Page } from '@playwright/test'; import { decodePDFRawStream, + degrees, PDFArray, PDFDocument, PDFRawStream, @@ -132,6 +133,23 @@ const createEncryptedPdf = async (pageCount: number, label: string, password: st return Buffer.from(await document.save()); }; +const createPreRotatedPdf = async (pageCount: number, label: string) => { + const document = await PDFDocument.create(); + const font = await document.embedFont(StandardFonts.Helvetica); + for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) { + const page = document.addPage([420, 594]); + page.drawText(`${label} – Seite ${pageNumber}`, { + x: 40, + y: 520, + size: 20, + font, + color: rgb(0.1, 0.2, 0.4) + }); + if (pageNumber === 1) page.setRotation(degrees(90)); + } + return Buffer.from(await document.save()); +}; + const trackPageErrors = (page: Page) => { const errors: Error[] = []; page.on('pageerror', (error) => errors.push(error)); @@ -264,6 +282,68 @@ test('separate renders dropped PDF pages and removes a selected page', async ({ expect(pageErrors).toEqual([]); }); +test('rotate turns pages losslessly via /Rotate and reset clears deltas', async ({ page }) => { + const pageErrors = trackPageErrors(page); + await page.goto('/tools/rotate'); + await dropFiles(page, pdfDropzone(page), [ + { name: 'drehung.pdf', mimeType: 'application/pdf', buffer: await createPdf(3, 'Drehen') } + ]); + + const downloadButton = page.getByRole('button', { name: 'Drehen & herunterladen' }); + await expect(page.getByRole('button', { name: 'Seite 2 um 90° nach rechts drehen' })).toBeVisible( + { + timeout: 15_000 + } + ); + await expect(downloadButton).toBeDisabled(); + + const rotateRightPage2 = page.getByRole('button', { name: 'Seite 2 um 90° nach rechts drehen' }); + await rotateRightPage2.click(); + await rotateRightPage2.click(); + await expect(page.getByText('1 von 3 Seiten gedreht')).toBeVisible(); + await expect(page.getByText('180°', { exact: true })).toBeVisible(); + + const downloadPromise = page.waitForEvent('download'); + await downloadButton.click(); + const bytes = await expectPdfDownload(await downloadPromise, /^drehung_gedreht\.pdf$/); + const output = await PDFDocument.load(bytes); + expect(output.getPage(1).getRotation().angle).toBe(180); + expect(output.getPage(0).getRotation().angle).toBe(0); + + await page.getByRole('button', { name: 'Zurücksetzen' }).click(); + await expect(page.getByText('0 von 3 Seiten gedreht')).toBeVisible(); + await expect(downloadButton).toBeDisabled(); + expect(pageErrors).toEqual([]); +}); + +test('rotate adds the delta on top of an existing /Rotate value', async ({ page }) => { + const pageErrors = trackPageErrors(page); + await page.goto('/tools/rotate'); + await dropFiles(page, pdfDropzone(page), [ + { + name: 'vorgedreht.pdf', + mimeType: 'application/pdf', + buffer: await createPreRotatedPdf(2, 'Vorgedreht') + } + ]); + + await expect(page.getByRole('button', { name: 'Seite 1 um 90° nach rechts drehen' })).toBeVisible( + { + timeout: 15_000 + } + ); + await page.getByRole('button', { name: 'Seite 1 um 90° nach rechts drehen' }).click(); + await expect(page.getByText('1 von 2 Seiten gedreht')).toBeVisible(); + + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Drehen & herunterladen' }).click(); + const bytes = await expectPdfDownload(await downloadPromise, /^vorgedreht_gedreht\.pdf$/); + const output = await PDFDocument.load(bytes); + expect(output.getPage(0).getRotation().angle).toBe(180); + expect(output.getPage(1).getRotation().angle).toBe(0); + expect(pageErrors).toEqual([]); +}); + test('watermark accepts numeric controls and downloads text and image watermarks', async ({ page }) => {