This commit is contained in:
@@ -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
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-primary-300 bg-white px-6 py-10 text-center transition focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 {isDragging
|
||||
? 'border-primary-400 bg-primary-100/60'
|
||||
: ''} {className}"
|
||||
class="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed px-6 py-10 text-center transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 {isDragging
|
||||
? 'scale-[1.02] border-primary bg-primary-100/70 shadow-lg ring-4 ring-primary/25'
|
||||
: 'border-primary-300 bg-white hover:border-primary-400 hover:bg-primary-50/60'} {className}"
|
||||
aria-label={ariaLabel}
|
||||
data-dropzone-ready={fileInput ? 'true' : 'false'}
|
||||
onclick={openFileDialog}
|
||||
@@ -116,7 +116,11 @@
|
||||
>
|
||||
{#if icon}
|
||||
{@const IconComponent = icon}
|
||||
<IconComponent class="h-10 w-10 text-primary" />
|
||||
<IconComponent
|
||||
class="h-10 w-10 text-primary transition-transform duration-150 {isDragging
|
||||
? 'scale-125'
|
||||
: ''}"
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex flex-row items-baseline justify-center gap-2">
|
||||
<span class="text-[18px] font-bold text-primary-900">{label}</span>
|
||||
@@ -125,6 +129,9 @@
|
||||
{#if sublabel}
|
||||
<p class="text-[13px] text-primary-700">{sublabel}</p>
|
||||
{/if}
|
||||
{#if isDragging}
|
||||
<p class="text-[13px] font-bold text-primary">Jetzt loslassen zum Hinzufügen</p>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if rejectionMessage}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { PdfThumbnail } from '$lib/pdf-thumbnails';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
@@ -8,6 +9,8 @@
|
||||
onPageClick?: (pageNumber: number) => void;
|
||||
thumbnailWidth?: number;
|
||||
class?: string;
|
||||
pageActions?: Snippet<[pageNumber: number]>;
|
||||
cardClass?: (pageNumber: number) => string;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -15,7 +18,9 @@
|
||||
selectedPages = new Set<number>(),
|
||||
onPageClick,
|
||||
thumbnailWidth = 120,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
pageActions,
|
||||
cardClass
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
@@ -25,30 +30,80 @@
|
||||
>
|
||||
{#each thumbnails as thumb}
|
||||
{@const isSelected = selectedPages.has(thumb.pageNumber)}
|
||||
<button
|
||||
type="button"
|
||||
class={cn(
|
||||
'group relative flex flex-col items-center gap-2 rounded-xl border-2 p-2 transition focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-transparent hover:border-primary-200 hover:bg-primary-50/50'
|
||||
)}
|
||||
onclick={() => onPageClick?.(thumb.pageNumber)}
|
||||
>
|
||||
<img
|
||||
src={thumb.imageUrl}
|
||||
alt="Seite {thumb.pageNumber}"
|
||||
class="w-full rounded border border-primary-100 bg-white object-contain"
|
||||
style="aspect-ratio: {thumb.width} / {thumb.height};"
|
||||
/>
|
||||
<span class="text-[12px] font-medium text-primary-700">Seite {thumb.pageNumber}</span>
|
||||
{#if isSelected}
|
||||
<div
|
||||
class="absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-white"
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{#if onPageClick}
|
||||
<button
|
||||
type="button"
|
||||
class={cn(
|
||||
'group relative flex flex-col items-center gap-2 rounded-xl border-2 p-2 transition focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-transparent hover:border-primary-200 hover:bg-primary-50/50'
|
||||
)}
|
||||
onclick={() => onPageClick(thumb.pageNumber)}
|
||||
>
|
||||
{#if cardClass}
|
||||
<div
|
||||
class={cn(
|
||||
'flex aspect-square w-full items-center justify-center',
|
||||
cardClass(thumb.pageNumber)
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={thumb.imageUrl}
|
||||
alt="Seite {thumb.pageNumber}"
|
||||
class="max-h-full max-w-full rounded border border-primary-100 bg-white object-contain"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<img
|
||||
src={thumb.imageUrl}
|
||||
alt="Seite {thumb.pageNumber}"
|
||||
class="w-full rounded border border-primary-100 bg-white object-contain"
|
||||
style="aspect-ratio: {thumb.width} / {thumb.height};"
|
||||
/>
|
||||
{/if}
|
||||
<span class="text-[12px] font-medium text-primary-700">Seite {thumb.pageNumber}</span>
|
||||
{#if isSelected}
|
||||
<div
|
||||
class="absolute top-1 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-white"
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class={cn(
|
||||
'group relative flex flex-col items-center gap-2 rounded-xl border-2 border-transparent p-2',
|
||||
isSelected && 'border-primary bg-primary/5'
|
||||
)}
|
||||
>
|
||||
{#if cardClass}
|
||||
<div
|
||||
class={cn(
|
||||
'flex aspect-square w-full items-center justify-center',
|
||||
cardClass(thumb.pageNumber)
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={thumb.imageUrl}
|
||||
alt="Seite {thumb.pageNumber}"
|
||||
class="max-h-full max-w-full rounded border border-primary-100 bg-white object-contain"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<img
|
||||
src={thumb.imageUrl}
|
||||
alt="Seite {thumb.pageNumber}"
|
||||
class="w-full rounded border border-primary-100 bg-white object-contain"
|
||||
style="aspect-ratio: {thumb.width} / {thumb.height};"
|
||||
/>
|
||||
{/if}
|
||||
<span class="text-[12px] font-medium text-primary-700">Seite {thumb.pageNumber}</span>
|
||||
{#if pageActions}
|
||||
{@render pageActions(thumb.pageNumber)}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -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',
|
||||
|
||||
+4
-27
@@ -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));
|
||||
};
|
||||
</script>
|
||||
@@ -65,24 +59,7 @@
|
||||
<title>beA-Edit — Werkzeugkasten für das besondere elektronische Anwaltspostfach</title>
|
||||
</svelte:head>
|
||||
|
||||
<svelte:window
|
||||
ondragover={handleWindowDragOver}
|
||||
ondragleave={handleWindowDragLeave}
|
||||
ondrop={handleWindowDrop}
|
||||
/>
|
||||
|
||||
{#if isDraggingFiles}
|
||||
<div
|
||||
class="pointer-events-none fixed inset-3 z-50 grid place-items-center rounded-3xl border-4 border-dashed border-primary bg-background/95 text-center shadow-2xl"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="px-4">
|
||||
<Archive class="mx-auto h-12 w-12" />
|
||||
<p class="mt-4 text-xl font-bold">beA-ZIP hier ablegen</p>
|
||||
<p class="mt-1 text-sm">Archiv aus dem besonderen elektronischen Anwaltspostfach</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:window ondragover={handleWindowDragOver} ondrop={handleWindowDrop} />
|
||||
|
||||
<div class="flex flex-col gap-10 px-4 py-10">
|
||||
<!-- Hero -->
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { degrees, PDFDocument } from 'pdf-lib';
|
||||
import { RotateCcw, RotateCw } from '@lucide/svelte';
|
||||
import FileDropzone from '$lib/components/FileDropzone.svelte';
|
||||
import PdfPageGrid from '$lib/components/PdfPageGrid.svelte';
|
||||
import Button from '$lib/components/ui/button.svelte';
|
||||
import { downloadBlob } from '$lib/download';
|
||||
import { loadPdfDocument, type PdfDocumentHandle } from '$lib/pdf-thumbnails';
|
||||
|
||||
let pdfFile: File | null = $state(null);
|
||||
let pdfHandle: PdfDocumentHandle | null = $state(null);
|
||||
let rotations = $state(new Map<number, number>());
|
||||
let isProcessing = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let loadingError = $state<string | null>(null);
|
||||
|
||||
const rotatedCount = $derived(rotations.size);
|
||||
const hasRotations = $derived(rotatedCount > 0);
|
||||
|
||||
const isEncryptedPdfError = (loadError: unknown) => {
|
||||
const message =
|
||||
loadError instanceof Error ? `${loadError.name} ${loadError.message}` : String(loadError);
|
||||
return /password|encrypted|encryption|verschl/i.test(message);
|
||||
};
|
||||
|
||||
const loadErrorMessage = (loadError: unknown) =>
|
||||
isEncryptedPdfError(loadError)
|
||||
? 'Dieses PDF ist passwortgeschützt. Entfernen Sie zuerst unter „Passwort entfernen“ den Schutz.'
|
||||
: 'PDF konnte nicht geladen werden. Prüfen Sie, ob die Datei gültig ist.';
|
||||
|
||||
const handleFileSelected = async (files: File[]) => {
|
||||
const file = files[0];
|
||||
if (!file) return;
|
||||
|
||||
pdfHandle?.destroy();
|
||||
pdfHandle = null;
|
||||
pdfFile = file;
|
||||
rotations = new Map();
|
||||
loadingError = null;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
pdfHandle = await loadPdfDocument(bytes);
|
||||
} catch (err) {
|
||||
console.error('Failed to load PDF:', err);
|
||||
loadingError = loadErrorMessage(err);
|
||||
pdfHandle = null;
|
||||
}
|
||||
};
|
||||
|
||||
const rotatePage = (pageNumber: number, delta: number) => {
|
||||
const next = new Map(rotations);
|
||||
const current = next.get(pageNumber) ?? 0;
|
||||
const value = (((current + delta) % 360) + 360) % 360;
|
||||
if (value === 0) {
|
||||
next.delete(pageNumber);
|
||||
} else {
|
||||
next.set(pageNumber, value);
|
||||
}
|
||||
rotations = next;
|
||||
};
|
||||
|
||||
const rotateAll = (delta: number) => {
|
||||
if (!pdfHandle) return;
|
||||
for (let pageNumber = 1; pageNumber <= pdfHandle.pageCount; pageNumber += 1) {
|
||||
rotatePage(pageNumber, delta);
|
||||
}
|
||||
};
|
||||
|
||||
const resetRotations = () => {
|
||||
rotations = new Map();
|
||||
};
|
||||
|
||||
const previewClass = (pageNumber: number) => {
|
||||
const delta = rotations.get(pageNumber) ?? 0;
|
||||
switch (delta) {
|
||||
case 90:
|
||||
return 'rotate-90';
|
||||
case 180:
|
||||
return 'rotate-180';
|
||||
case 270:
|
||||
return 'rotate-[270deg]';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const rotateAndDownload = async () => {
|
||||
if (!pdfFile || !pdfHandle || !hasRotations) return;
|
||||
|
||||
isProcessing = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const bytes = new Uint8Array(await pdfFile.arrayBuffer());
|
||||
const pdfDoc = await PDFDocument.load(bytes);
|
||||
for (const [index, page] of pdfDoc.getPages().entries()) {
|
||||
const delta = rotations.get(index + 1);
|
||||
if (!delta) continue;
|
||||
const current = page.getRotation().angle;
|
||||
page.setRotation(degrees((((current + delta) % 360) + 360) % 360));
|
||||
}
|
||||
|
||||
const resultBytes = await pdfDoc.save();
|
||||
const baseName = pdfFile.name.replace(/\.pdf$/i, '');
|
||||
downloadBlob(resultBytes, `${baseName}_gedreht.pdf`);
|
||||
} catch (err) {
|
||||
console.error('Failed to rotate pages:', err);
|
||||
error = isEncryptedPdfError(err)
|
||||
? 'Dieses PDF ist passwortgeschützt. Entfernen Sie zuerst unter „Passwort entfernen“ den Schutz.'
|
||||
: 'Fehler beim Drehen der Seiten.';
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
pdfHandle?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-primary-900">Seiten drehen</h1>
|
||||
<p class="mt-2 text-sm text-primary-700">
|
||||
Drehen Sie einzelne oder alle Seiten eines PDFs — verlustfrei und ohne Qualitätsverlust.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if !pdfHandle}
|
||||
<FileDropzone onFilesSelected={handleFileSelected} />
|
||||
{#if loadingError}
|
||||
<div class="mt-4 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
{loadingError}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<FileDropzone
|
||||
onFilesSelected={handleFileSelected}
|
||||
label="Anderes PDF ablegen"
|
||||
sublabel={pdfFile?.name ?? ''}
|
||||
class="py-4"
|
||||
/>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 class="text-lg font-semibold text-primary-900">
|
||||
{pdfFile?.name} — {pdfHandle.pageCount} Seite{pdfHandle.pageCount === 1 ? '' : 'n'}
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="tertiary" class="min-w-0 px-3" onclick={() => rotateAll(90)}>
|
||||
Alle 90° rechts
|
||||
</Button>
|
||||
<Button variant="tertiary" class="min-w-0 px-3" onclick={() => rotateAll(270)}>
|
||||
Alle 90° links
|
||||
</Button>
|
||||
<Button variant="tertiary" class="min-w-0 px-3" onclick={() => rotateAll(180)}>
|
||||
Alle 180°
|
||||
</Button>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
class="min-w-0 px-3"
|
||||
onclick={resetRotations}
|
||||
disabled={!hasRotations}
|
||||
>
|
||||
Zurücksetzen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PdfPageGrid thumbnails={pdfHandle.thumbnails} thumbnailWidth={160} cardClass={previewClass}>
|
||||
{#snippet pageActions(pageNumber)}
|
||||
{#if (rotations.get(pageNumber) ?? 0) !== 0}
|
||||
<span
|
||||
class="absolute top-1 right-1 rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-bold text-white"
|
||||
>
|
||||
{rotations.get(pageNumber)}°
|
||||
</span>
|
||||
{/if}
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-7 w-7 items-center justify-center rounded-lg border border-primary-200 bg-white text-primary transition hover:bg-primary-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400"
|
||||
aria-label="Seite {pageNumber} um 90° nach links drehen"
|
||||
onclick={() => rotatePage(pageNumber, 270)}
|
||||
>
|
||||
<RotateCcw class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-7 w-7 items-center justify-center rounded-lg border border-primary-200 bg-white text-primary transition hover:bg-primary-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400"
|
||||
aria-label="Seite {pageNumber} um 90° nach rechts drehen"
|
||||
onclick={() => rotatePage(pageNumber, 90)}
|
||||
>
|
||||
<RotateCw class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</PdfPageGrid>
|
||||
|
||||
{#if error}
|
||||
<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-primary-700">
|
||||
{rotatedCount} von {pdfHandle.pageCount} Seite{pdfHandle.pageCount === 1 ? '' : 'n'}
|
||||
gedreht
|
||||
</p>
|
||||
<Button onclick={rotateAndDownload} disabled={isProcessing || !hasRotations}>
|
||||
{isProcessing ? 'Wird verarbeitet...' : 'Drehen & herunterladen'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user