Files
bea-edit/src/routes/tools/compress/+page.svelte
T
2026-10-28 16:09:53 +01:00

176 lines
5.6 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import BatchFileList from '$lib/components/BatchFileList.svelte';
import FileDropzone from '$lib/components/FileDropzone.svelte';
import Button from '$lib/components/ui/button.svelte';
import { createBatchItem, runBatch, type BatchItem } from '$lib/batch';
import { downloadBlob } from '$lib/download';
import { pdfProcessingErrorMessage } from '$lib/pdf-errors';
import {
compressWithPreset,
compressionPresets,
defaultCompressionPreset
} from '$lib/pdf-compression';
import { isRasterizationTooLarge, RASTERIZATION_MAX_BYTES } from '$lib/rasterization-limits';
import { AlertTriangle } from '@lucide/svelte';
const compressErrorMessage = (error: unknown) =>
pdfProcessingErrorMessage(error, 'Diese PDF konnte nicht komprimiert werden.');
let items = $state<BatchItem[]>([]);
let selectedPreset = $state(defaultCompressionPreset.id);
let isRunning = $state(false);
const pendingCount = $derived(items.filter((item) => item.status === 'pending').length);
const canStart = $derived(pendingCount > 0 && !isRunning);
const handleFilesSelected = (files: File[]) => {
const knownKeys = new Set(items.map((item) => `${item.file.name}:${item.file.size}`));
const additions = files
.filter((file) => !knownKeys.has(`${file.name}:${file.size}`))
.map((file) => {
const item = createBatchItem(file);
if (isRasterizationTooLarge(file)) {
item.status = 'error';
item.errorMessage = `Diese Datei ist größer als ${RASTERIZATION_MAX_BYTES / (1024 * 1024)} MB und kann aus Speichergründen nicht im Browser gerastert werden.`;
}
return item;
});
items = [...items, ...additions];
};
const updateItem = (id: string, patch: Partial<BatchItem>) => {
items = items.map((item) => (item.id === id ? { ...item, ...patch } : item));
};
const processFile = async (file: File) => {
// The preset is read per file so an option change during a run only
// affects the still pending files.
const preset =
compressionPresets.find((entry) => entry.id === selectedPreset) ?? defaultCompressionPreset;
const bytes = await compressWithPreset(file, preset);
return { bytes, name: `${file.name.replace(/\.pdf$/i, '')}_komprimiert.pdf` };
};
const startBatch = async () => {
if (isRunning) return;
const queue = items.filter((item) => item.status === 'pending');
if (queue.length === 0) return;
isRunning = true;
try {
await runBatch(queue, processFile, updateItem, {
errorMessage: compressErrorMessage,
// Skip rows removed while they were still waiting in the queue.
shouldProcess: (id) => items.some((item) => item.id === id)
});
} finally {
isRunning = false;
}
};
const retryItem = async (id: string) => {
if (isRunning) return;
const item = items.find((entry) => entry.id === id);
if (!item) return;
isRunning = true;
try {
await runBatch([item], processFile, updateItem, {
errorMessage: compressErrorMessage
});
} finally {
isRunning = false;
}
};
const removeItem = (id: string) => {
items = items.filter((item) => item.id !== id);
};
const downloadItem = (item: BatchItem) => {
if (item.resultBytes && item.resultName) downloadBlob(item.resultBytes, item.resultName);
};
</script>
<div class="mx-auto max-w-4xl space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Komprimieren</h1>
<p class="mt-2 text-sm text-primary">
Reduzieren Sie die Dateigröße Ihrer PDFs durch Komprimierung – einzeln oder mehrere in einem
Durchlauf.
</p>
</div>
<div class="flex gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4">
<AlertTriangle class="mt-0.5 h-5 w-5 shrink-0 text-amber-600" />
<div class="text-sm text-amber-800">
<p class="font-medium">Hinweis zur Qualität</p>
<p class="mt-1">
Das PDF wird neu gerendert, wodurch Text zu Bildern wird. Ausgewählter Text,
Durchsuchbarkeit und Vektorgrafiken gehen verloren.
</p>
</div>
</div>
<FileDropzone
onFilesSelected={handleFilesSelected}
multiple
label={items.length === 0 ? 'Dateien hinzufügen' : 'Weitere Dateien ablegen'}
sublabel={items.length === 0
? 'PDFs hier ablegen oder zum Auswählen klicken'
: 'Weitere PDFs hinzufügen – bestehende Dateien bleiben erhalten'}
/>
{#if items.length > 0}
<div class="space-y-4 rounded-2xl border border-border bg-card p-4">
<h2 class="text-lg font-semibold text-foreground">Komprimierungsstufe</h2>
<p class="text-xs text-primary">Die Einstellung gilt für alle Dateien der Liste.</p>
<div class="space-y-2">
{#each compressionPresets as preset (preset.id)}
<label
class="flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition {selectedPreset ===
preset.id
? 'border-primary bg-primary/5'
: 'hover:border-primary-300 border-border'}"
>
<input
type="radio"
name="compression-preset"
value={preset.id}
bind:group={selectedPreset}
class="mt-0.5 h-4 w-4"
/>
<div>
<p class="text-sm font-medium text-foreground">{preset.label}</p>
<p class="text-xs text-primary">{preset.description}</p>
</div>
</label>
{/each}
</div>
{#if isRunning}
<p class="text-xs text-primary" role="note">
Änderungen an der Komprimierungsstufe gelten für die noch ausstehenden Dateien.
</p>
{/if}
</div>
{#if items.length > 0}
<BatchFileList
{items}
zipBaseName="komprimiert"
onDownload={downloadItem}
onRemove={removeItem}
onRetry={retryItem}
/>
{/if}
<div class="flex justify-end">
<Button onclick={startBatch} disabled={!canStart}>
{isRunning ? 'Wird komprimiert...' : 'Komprimieren'}
</Button>
</div>
{/if}
</div>