1 parent
d71b25f834
commit
b7969b4c9e
7 files changed
+426
-10
No files matched your search
@@ -10,6 +10,7 @@
|
||||
Merge,
|
||||
Stamp,
|
||||
RotateCw,
|
||||
Info,
|
||||
FileText
|
||||
} from '@lucide/svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -27,7 +28,8 @@
|
||||
scissors: Scissors,
|
||||
merge: Merge,
|
||||
stamp: Stamp,
|
||||
'rotate-cw': RotateCw
|
||||
'rotate-cw': RotateCw,
|
||||
info: Info
|
||||
};
|
||||
|
||||
const isZipFile = (file: File) =>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
import FileDropzone from '$lib/components/FileDropzone.svelte';
|
||||
import Button from '$lib/components/ui/button.svelte';
|
||||
import { formatBytes } from '$lib/batch';
|
||||
import { downloadBlob } from '$lib/download';
|
||||
import { readPdfMetadata, stripPdfMetadata, type PdfMetadataResult } from '$lib/pdf-metadata';
|
||||
import { AlertTriangle, Info, Lock } from '@lucide/svelte';
|
||||
|
||||
let file = $state<File | null>(null);
|
||||
let result = $state<PdfMetadataResult | null>(null);
|
||||
let isReading = $state(false);
|
||||
let isCleaning = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
const cleanedFileName = $derived(file ? `${file.name.replace(/\.pdf$/i, '')}_bereinigt.pdf` : '');
|
||||
|
||||
const handleFilesSelected = async (files: File[]) => {
|
||||
const selected = files[0];
|
||||
if (!selected) return;
|
||||
|
||||
file = selected;
|
||||
result = null;
|
||||
error = null;
|
||||
isReading = true;
|
||||
try {
|
||||
const bytes = new Uint8Array(await selected.arrayBuffer());
|
||||
result = await readPdfMetadata(bytes);
|
||||
} catch {
|
||||
file = null;
|
||||
error = 'Diese Datei konnte nicht als PDF gelesen werden. Bitte prüfen Sie die Datei.';
|
||||
} finally {
|
||||
isReading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const downloadCleaned = async () => {
|
||||
if (!file || isCleaning) return;
|
||||
isCleaning = true;
|
||||
error = null;
|
||||
try {
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
const cleaned = await stripPdfMetadata(bytes);
|
||||
downloadBlob(cleaned, cleanedFileName);
|
||||
} catch {
|
||||
error = 'Die Metadaten konnten nicht entfernt werden. Bitte versuchen Sie es erneut.';
|
||||
} finally {
|
||||
isCleaning = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (value: Date | null) =>
|
||||
value ? value.toLocaleString('de-DE', { dateStyle: 'long', timeStyle: 'short' }) : '';
|
||||
|
||||
const infoRows = $derived.by(() => {
|
||||
if (!result || result.status !== 'ok') return [];
|
||||
const metadata = result.metadata;
|
||||
return [
|
||||
{ label: 'Titel', value: metadata.title },
|
||||
{ label: 'Autor', value: metadata.author },
|
||||
{ label: 'Betreff', value: metadata.subject },
|
||||
{ label: 'Schlüsselwörter', value: metadata.keywords.join(', ') },
|
||||
{ label: 'Erstellt mit', value: metadata.creator },
|
||||
{ label: 'PDF-Erzeuger', value: metadata.producer },
|
||||
{ label: 'Erstellt am', value: formatDate(metadata.creationDate) },
|
||||
{ label: 'Geändert am', value: formatDate(metadata.modificationDate) },
|
||||
{ label: 'Seiten', value: String(metadata.pageCount) },
|
||||
{ label: 'Größe', value: file ? formatBytes(file.size) : '' },
|
||||
{ label: 'Verschlüsselung', value: 'Nein' }
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-foreground">Metadaten</h1>
|
||||
<p class="mt-2 text-sm text-primary">
|
||||
Prüfen Sie, welche Informationen Ihr PDF enthält, und entfernen Sie sie vor dem Weitergeben.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FileDropzone
|
||||
onFilesSelected={handleFilesSelected}
|
||||
label={file ? 'Anderes PDF auswählen' : 'PDF auswählen'}
|
||||
sublabel="PDF hier ablegen oder zum Auswählen klicken"
|
||||
ariaLabel="PDF-Dateien hinzufügen"
|
||||
/>
|
||||
|
||||
{#if isReading}
|
||||
<p class="text-sm text-primary" role="status">Metadaten werden gelesen …</p>
|
||||
{:else if error}
|
||||
<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
{:else if result}
|
||||
{#if result.status === 'encrypted'}
|
||||
<div class="rounded-lg border border-red-200 bg-red-50 p-4 flex gap-3" role="alert">
|
||||
<Lock class="h-5 w-5 shrink-0 text-red-600 mt-0.5" />
|
||||
<div class="text-sm text-red-800">
|
||||
<p class="font-medium">Dieses PDF ist verschlüsselt</p>
|
||||
<p class="mt-1">
|
||||
Verschlüsselte Dokumente können nicht gelesen werden. Bitte entfernen Sie zuerst das
|
||||
Passwort mit dem Werkzeug
|
||||
<a href="/tools/decrypt" class="font-medium underline">Passwort entfernen</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-2xl border border-border bg-card p-6 space-y-5" data-metadata-card>
|
||||
<div class="flex items-center gap-2 text-foreground">
|
||||
<Info class="h-5 w-5" />
|
||||
<h2 class="text-lg font-semibold">Dokument-Informationen</h2>
|
||||
</div>
|
||||
<p class="text-xs text-primary truncate" title={file?.name}>{file?.name}</p>
|
||||
|
||||
<dl class="grid grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-[12rem_1fr]">
|
||||
{#each infoRows as row (row.label)}
|
||||
<dt class="text-sm font-medium text-foreground">{row.label}</dt>
|
||||
<dd class="text-sm text-primary wrap-break-word" data-metadata-value={row.label}>
|
||||
{row.value || '–'}
|
||||
</dd>
|
||||
{/each}
|
||||
</dl>
|
||||
|
||||
{#if result.metadata.embeddedFileSuspected}
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 flex gap-3">
|
||||
<AlertTriangle class="h-5 w-5 shrink-0 text-amber-600 mt-0.5" />
|
||||
<p class="text-sm text-amber-800">
|
||||
Die Datei enthält möglicherweise eingebettete Anlagen. Diese werden beim Bereinigen
|
||||
nicht entfernt.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-lg border border-border bg-muted/50 p-4 text-sm text-primary">
|
||||
<p>
|
||||
Beim Bereinigen bleibt der Inhalt vollständig erhalten (kein Qualitätsverlust). Entfernt
|
||||
werden die Standard-Dokumenteigenschaften (Titel, Autor, Betreff usw.). Nicht entfernt
|
||||
werden sogenannte XMP-Metadaten (Extensible Metadata Platform): Diese stehen nicht im
|
||||
Bereich „Dokumenteigenschaften“, sondern können direkt ins PDF-Dokument oder in
|
||||
eingebettete Bilder eingebettet sein.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
onclick={downloadCleaned}
|
||||
disabled={result.status !== 'ok' || isCleaning}
|
||||
data-clean-download
|
||||
>
|
||||
{isCleaning ? 'Wird bereinigt …' : 'Bereinigt herunterladen'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in new issue
Block a user