140 lines
6.3 KiB
Markdown
140 lines
6.3 KiB
Markdown
# Plan: PDF-Metadaten — anzeigen und bereinigen
|
||
|
||
## Goal
|
||
|
||
Add a new tool **"Metadaten"** (`/tools/metadata`) with two jobs:
|
||
|
||
1. **Inspect** a PDF: show document metadata (title, author, subject, keywords, creator,
|
||
producer, creation/modification dates), page count, file size, and encryption state —
|
||
lawyers need to know what a document reveals before forwarding it.
|
||
2. **Clean** a PDF: strip the metadata fields and re-save, so the cleaned file can be shared
|
||
without leaking software, author names, or edit history.
|
||
|
||
No rasterization is involved — this tool works directly with pdf-lib and preserves the document
|
||
fully (vector text stays selectable). No new dependencies.
|
||
|
||
---
|
||
|
||
## Open decisions (defaults marked ✅)
|
||
|
||
### D1 — Cleaning strategy
|
||
|
||
- ✅ **A. Neutralize fields + re-save with pdf-lib.** Set title/author/subject/keywords to empty
|
||
strings, creator/producer to neutral values, reset creation/modification dates to the cleaning
|
||
moment. Saving through pdf-lib rebuilds the file without unreferenced objects. Simple and
|
||
dependency-free.
|
||
- B. Also attempt to remove embedded files/attachments. pdf-lib has no reliable public API to
|
||
enumerate or remove all embedded-file entries; defer to D2.
|
||
|
||
### D2 — Embedded-file detection
|
||
|
||
- ✅ **A. Best-effort heuristic warning.** Scan the raw bytes for `/EmbeddedFile` and
|
||
`/EmbeddedFiles` markers; if found, show "Die Datei enthält möglicherweise eingebettete
|
||
Anlagen. Diese werden beim Bereinigen nicht entfernt." No removal in v1.
|
||
- B. No detection at all. Rejected: silent ignorance is worse than an honest warning.
|
||
|
||
### D3 — Encrypted PDFs
|
||
|
||
- ✅ **A. Friendly rejection.** pdf-lib cannot load encrypted files; show a German hint pointing
|
||
to `tools/decrypt` first. No silent failure.
|
||
|
||
---
|
||
|
||
## UX
|
||
|
||
- Route `/tools/metadata`, new entry in `src/lib/tools.ts`:
|
||
`{ slug: 'metadata', title: 'Metadaten', description: 'Zeigen Sie Metadaten eines PDFs an und entfernen Sie sie.', icon: 'info' }`
|
||
- icon mapping on the start page.
|
||
- Flow: `PdfDropzone` (single PDF) → results card with:
|
||
- **Info table** (label/value rows, empty values shown as "–"): Titel, Autor, Betreff,
|
||
Schlüsselwörter, Erstellt mit (creator), PDF-Erzeuger (producer), Erstellt am, Geändert am,
|
||
Seiten, Größe, Verschlüsselung (Ja/Nein — from load success/failure is insufficient; use
|
||
pdf-lib's load error for encrypted detection and show it as a state, or report "Unbekannt"
|
||
when not determinable).
|
||
- **Embedded-file warning block** when the heuristic fires (amber styling consistent with
|
||
existing warning patterns).
|
||
- **Quality note**: "Beim Bereinigen bleibt der Inhalt vollständig erhalten (kein Qualitätsverlust)."
|
||
- Actions:
|
||
- **"Bereinigt herunterladen"** (primary, disabled for encrypted files) → cleaned PDF named
|
||
`{baseName}_bereinigt.pdf`.
|
||
- Secondary: "Original herunterladen" is _not_ offered (the user already has the original).
|
||
- German inline error area for corrupt/encrypted files, matching existing tool pages.
|
||
|
||
## Technical approach
|
||
|
||
New route `src/routes/tools/metadata/+page.svelte` plus a small lib module
|
||
`src/lib/pdf-metadata.ts`:
|
||
|
||
```ts
|
||
export type PdfMetadataInfo = {
|
||
title: string;
|
||
author: string;
|
||
subject: string;
|
||
keywords: string[];
|
||
creator: string;
|
||
producer: string;
|
||
creationDate: Date | null;
|
||
modificationDate: Date | null;
|
||
pageCount: number;
|
||
embeddedFileSuspected: boolean;
|
||
};
|
||
|
||
export const readPdfMetadata = async (bytes: Uint8Array): Promise<PdfMetadataInfo>;
|
||
export const stripPdfMetadata = async (bytes: Uint8Array): Promise<Uint8Array>;
|
||
```
|
||
|
||
- `readPdfMetadata`: `PDFDocument.load(bytes)` (with `{ ignoreEncryption: false }` default so
|
||
encrypted files throw), then the `getTitle()`/`getAuthor()`/`getSubject()`/`getKeywords()`/
|
||
`getCreator()`/`getProducer()`/`getCreationDate()`/`getModificationDate()`/`getPageCount()`
|
||
accessors. Encryption detection: catch the pdf-lib `EncryptedPDFError` and return a distinct
|
||
`{ encrypted: true }` result shape (discriminated union) so the UI can branch.
|
||
- `stripPdfMetadata`: load, apply setters (`setTitle('')`, `setAuthor('')`, `setSubject('')`,
|
||
`setKeywords([])`, `setCreator('')`, `setProducer('beA-Edit')`, `setCreationDate(new Date())`,
|
||
`setModificationDate(new Date())`), `save()`.
|
||
- Embedded-file heuristic: decode bytes with `TextDecoder` once and search for the two marker
|
||
strings; cheap, runs before pdf-lib load.
|
||
- Reading metadata does **not** need pdfjs or thumbnails — no rasterization limits apply; large
|
||
files are fine because pdf-lib parses lazily enough for this purpose.
|
||
|
||
## Files changed
|
||
|
||
- `src/lib/pdf-metadata.ts` — new.
|
||
- `src/routes/tools/metadata/+page.svelte` — new route.
|
||
- `src/lib/tools.ts`, `src/routes/+page.svelte` — registry entry + icon map.
|
||
- `tests/e2e/toolbox.spec.ts` — new spec block.
|
||
- `agents.md` — structure update.
|
||
|
||
## Implementation phases
|
||
|
||
1. `pdf-metadata.ts` with read + strip + heuristic.
|
||
2. Route UI: info table, warning block, clean action, encrypted-file branch.
|
||
3. Registry entry + icon.
|
||
4. Playwright tests: PDF created with `createPdf` + set metadata fields → fields displayed;
|
||
clean download loads with pdf-lib and has empty author/title; encrypted-PDF rejection hint
|
||
(reuse the encrypt tool to produce a fixture inline).
|
||
5. Polish, German copy review, `pnpm check`/`lint`/`build`/`test:e2e`.
|
||
|
||
## Verification
|
||
|
||
- Metadata of a known PDF renders completely; empty fields show "–".
|
||
- Cleaned PDF: open in viewer, metadata fields empty; content and page count identical; file
|
||
still selectable/searchable (no rasterization happened).
|
||
- Encrypted input shows the decrypt hint instead of crashing.
|
||
- `/EmbeddedFile`-containing fixture triggers the amber warning.
|
||
- Standard command gauntlet passes.
|
||
|
||
## Risks
|
||
|
||
- **XMP metadata**: pdf-lib's setters touch the Info dict; a separate XMP metadata stream can
|
||
survive. Mitigation: note in the UI copy that cleaning covers standard document properties.
|
||
(Full XMP removal could be a follow-up by deleting the metadata stream object.)
|
||
- **Producer fingerprint**: setting `beA-Edit` as producer is itself a small fingerprint; neutral
|
||
but acceptable — alternative is an empty string; pick empty string if reviewers prefer.
|
||
|
||
## Out of scope
|
||
|
||
- Removing embedded files/attachments (see D2).
|
||
- XMP stream removal (follow-up).
|
||
- Metadata editing (rename title, etc.) — inspection + removal only.
|
||
- Hooking metadata cleaning into the beA export pipeline.
|