176 lines
5.6 KiB
Svelte
176 lines
5.6 KiB
Svelte
<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>
|