Struktur: Docker/ (skrift-backend, signature-service), Webseite/ (skrift-configurator, skrift-job-manager), Produktion/ (skrift-produktion, AutoRecover). Aufgeräumt: signature-to-svg entfernt, Backend-PayPal-Service entfernt (PayPal läuft client-seitig im Configurator), Dev-/Test-Artefakte und Deployment-Dokus bereinigt, .dockerignore repariert. CLAUDE.md mit Architektur ergänzt. Secrets (.env, config.json) via .gitignore ausgeschlossen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
198 lines
8.5 KiB
Markdown
198 lines
8.5 KiB
Markdown
# Signatur-Plotter-Service
|
||
|
||
Zustandsloser FastAPI-Service, der für einen Schreibroboter **A4-SVGs** erzeugt,
|
||
auf denen handschriftliche Unterschriften an exakt der richtigen Position sitzen.
|
||
|
||
Im Druck-PDF stehen an jeder Signaturstelle unsichtbare Marker-Token in
|
||
transparenter Schrift (z.B. `§§SIG1§§`). Der Service sucht jeden Token im PDF,
|
||
liest dessen Koordinaten aus und platziert die zugeordnete SVG-Unterschrift
|
||
relativ dazu. Der Plotter druckt nur die erzeugte SVG – das bereits gedruckte
|
||
Dokument liefert den Rest.
|
||
|
||
> **Keine Plotter-Kalibrierung im Service.** Mechanischer Versatz wird komplett in
|
||
> der Druckersoftware behandelt. Es gibt keine Offset-Logik und keine ENV-Variablen
|
||
> für Versatz.
|
||
|
||
## Funktionsweise / Geometrie
|
||
|
||
- PDF-Punkte werden in mm umgerechnet (`1 pt = 25.4/72 mm`).
|
||
- PyMuPDF nutzt den Ursprung oben links, Y wächst nach unten.
|
||
- Referenzpunkt ist die **linke Oberkante** des Marker-Tokens (`rect.x0`, `rect.y0`).
|
||
- `position: "genau"` → `y_sig = y_token` (Unterschrift sitzt **direkt** am Marker, **Standard**)
|
||
- `position: "ueber"` → `y_sig = y_token - abstand_mm - signatur_hoehe`
|
||
- `position: "unter"` → `y_sig = y_token + abstand_mm`
|
||
- Skalierung gleichmäßig: `skala = breite_mm / svg_originalbreite`
|
||
(Originalbreite aus der **viewBox**, nicht aus `width`/`height`).
|
||
- Eingebettet wird per `<g transform="translate(x,y) scale(skala)">`.
|
||
|
||
Die Vorschau legt die fertige A4-Signatur-SVG vektoriell (via PyMuPDF
|
||
`convert_to_pdf` + `show_pdf_page`) über eine Kopie der betroffenen Originalseiten
|
||
und gibt ein zusammengeführtes PDF zurück. Begründung siehe Kommentar in
|
||
[`core.py`](core.py) (`build_preview_pdf`): voll vektoriell, gleiche Geometrie wie
|
||
`/render`, ohne zusätzliche Pillow-Abhängigkeit.
|
||
|
||
## Job-Definition
|
||
|
||
Pro Eintrag. Ein Token darf **mehrfach** im PDF vorkommen (z.B. dieselbe Unterschrift
|
||
auf jeder Seite) – jedes Vorkommen wird platziert:
|
||
|
||
| Feld | Typ | Default | Bedeutung |
|
||
|--------------|-------|---------|--------------------------------------------------|
|
||
| `token` | str | – | Unsichtbarer Token im PDF, z.B. `§§SIG1§§` |
|
||
| `svg_id` | str | – | Dateiname der hochgeladenen SVG |
|
||
| `position` | str | `genau` | `genau` (direkt am Marker), `ueber` oder `unter` |
|
||
| `breite_mm` | float | `45` | Zielbreite der Unterschrift |
|
||
| `abstand_mm` | float | `8` | Abstand zum Token (nur bei `ueber`/`unter` relevant) |
|
||
| `versatz_x_mm` | float | `0` | Feinjustierung links/rechts (+ = rechts, − = links) |
|
||
| `versatz_y_mm` | float | `0` | Feinjustierung oben/unten (+ = unten, − = oben) |
|
||
| `linienstaerke_mm` | float | `0.35` | Linienstärke der Kontur (Signaturen werden immer als Single-Stroke gezeichnet) |
|
||
|
||
Beispiel (`job.json`):
|
||
|
||
```json
|
||
[
|
||
{ "token": "§§SIG1§§", "svg_id": "unterschrift_a.svg", "position": "unter", "breite_mm": 45, "abstand_mm": 8 },
|
||
{ "token": "§§SIG2§§", "svg_id": "unterschrift_b.svg", "position": "ueber" }
|
||
]
|
||
```
|
||
|
||
## Endpunkte
|
||
|
||
### `POST /render`
|
||
Findet **jedes Vorkommen** jedes Tokens und platziert die zugeordnete SVG an jeder
|
||
Stelle. **Rückgabe:** ZIP-Download mit **einer A4-SVG pro betroffener Seite**
|
||
(`210×297mm`, `viewBox "0 0 210 297"`), Dateiname `seite_<n>.svg`. Jede Seiten-SVG
|
||
enthält alle Signaturen dieser Seite – nur die Unterschriften, Rest leer.
|
||
|
||
### `POST /preview`
|
||
Gleiche Platzierungslogik. **Rückgabe:** zusammengeführtes PDF mit allen
|
||
betroffenen Seiten und der Unterschrift als Overlay – nur zur Sichtkontrolle.
|
||
|
||
Übertragung jeweils als `multipart/form-data`: `pdf` als Datei, `svgs` als Dateien
|
||
(max. 3) und `job` als JSON-Form-Feld.
|
||
|
||
## Fehlerbehandlung
|
||
|
||
Es wird nie still übersprungen – Fehler kommen als klare Liste zurück:
|
||
|
||
- Token gar nicht gefunden (0 Treffer) → **422** (Mehrfachtreffer sind erlaubt)
|
||
- derselbe Token mehrfach in der Job-Definition → **422**
|
||
- `svg_id` ohne hochgeladene Datei → **422**
|
||
- SVG ohne `viewBox` → **422**
|
||
- Mehr als 3 SVGs → **400**
|
||
- Ungültiges Job-JSON / fehlendes/leeres PDF → **400**
|
||
- Ungültige Felder (`position`, `breite_mm` …) → **422**
|
||
|
||
Antwort-Format bei Fehlern: `{ "detail": [ "…", "…" ] }`.
|
||
|
||
## Beispiel-curl
|
||
|
||
`/render` (lädt das ZIP herunter):
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8000/render \
|
||
-F "pdf=@dokument.pdf;type=application/pdf" \
|
||
-F "svgs=@unterschrift_a.svg;type=image/svg+xml" \
|
||
-F "svgs=@unterschrift_b.svg;type=image/svg+xml" \
|
||
-F 'job=[{"token":"§§SIG1§§","svg_id":"unterschrift_a.svg","position":"unter","breite_mm":45,"abstand_mm":8},{"token":"§§SIG2§§","svg_id":"unterschrift_b.svg","position":"ueber"}]' \
|
||
-o signaturen.zip
|
||
```
|
||
|
||
`/preview` (lädt das Vorschau-PDF herunter):
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8000/preview \
|
||
-F "pdf=@dokument.pdf;type=application/pdf" \
|
||
-F "svgs=@unterschrift_a.svg;type=image/svg+xml" \
|
||
-F 'job=[{"token":"§§SIG1§§","svg_id":"unterschrift_a.svg","position":"unter"}]' \
|
||
-o vorschau.pdf
|
||
```
|
||
|
||
## Lokal starten
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
uvicorn app:app --host 0.0.0.0 --port 8000
|
||
```
|
||
|
||
## Docker (lokal)
|
||
|
||
```bash
|
||
docker build -t signatur-plotter .
|
||
docker run --rm -p 8000:8000 signatur-plotter
|
||
```
|
||
|
||
## Deployment hinter Nginx Proxy Manager
|
||
|
||
Der Service läuft im selben Docker-Netz wie der NPM (`nginx-proxy-manager_default`)
|
||
und wird per Domain **`api-sign.skrift.de`** erreichbar gemacht. Start via Compose:
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Die [`docker-compose.yml`](docker-compose.yml) hängt den Container ins externe Netz
|
||
`nginx-proxy-manager_default` (kein Host-Port – NPM proxyt intern). Anschließend im
|
||
NPM einen **Proxy Host** anlegen:
|
||
|
||
| Feld | Wert |
|
||
|------------------|-------------------------------|
|
||
| Domain Names | `api-sign.skrift.de` |
|
||
| Scheme | `http` |
|
||
| Forward Hostname | `skrift-signature-service` |
|
||
| Forward Port | `8000` |
|
||
| SSL | Let's Encrypt-Zertifikat aktivieren |
|
||
|
||
Im Frontend wird die Backend-URL dann auf `https://api-sign.skrift.de` gesetzt
|
||
(siehe unten).
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
pip install pytest
|
||
pytest
|
||
```
|
||
|
||
Die Tests decken Token-Suche, Geometrie (`ueber`/`unter`, viewBox-Offset) und die
|
||
Fehlerfälle (Token fehlt/mehrfach, SVG fehlt, keine viewBox) ab.
|
||
|
||
## Frontend-Tab in der Skrift-Anwendung
|
||
|
||
Der Upload-Bereich ist als eigener Tab **„Signaturen“** in die bestehende
|
||
Skrift-FTP-Anwendung integriert (`app/static/index.html`). Er ist über die
|
||
Hauptnavigation neben „Manuell drucken“ erreichbar und bietet:
|
||
|
||
- Upload für genau **ein PDF** und bis zu **3 SVG-Unterschriften**,
|
||
- pro SVG eine Zuordnungszeile (Marker-Token, Position **genau** (Standard) / über / unter,
|
||
Breite, Abstand, **X-/Y-Versatz** zur Feinjustierung sowie **Linienstärke**;
|
||
Signaturen werden immer als Single-Stroke gezeichnet),
|
||
- die zuletzt verwendeten Werte werden **pro Signatur** (Dateiname) im Browser
|
||
gespeichert und beim erneuten Hochladen automatisch wiederhergestellt,
|
||
- ein **Standard-Template** für den Direktdruck ist in den App-Einstellungen unter
|
||
*Zuordnung → Signaturen* festlegbar (wird im Tab vorausgewählt),
|
||
- Buttons **Vorschau** (zeigt das Vorschau-PDF inline) und **Rendern** (ZIP-Download),
|
||
- **Direkt drucken**: Template + Maschine auswählen und die gerenderten A4-Signaturen
|
||
sofort an den Schreibroboter senden,
|
||
- eine gut sichtbare Fehlerliste (Token nicht/mehrfach gefunden, fehlende Zuordnung …).
|
||
|
||
### Direktdruck-Pfad
|
||
|
||
`Direkt drucken` ruft den Skrift-Backend-Endpunkt `POST /api/signature/print` auf.
|
||
Dieser rendert die Signaturen serverseitig über den Signatur-Service (`/render`),
|
||
entpackt das ZIP und schickt die A4-SVGs mit den Parametern des gewählten Templates
|
||
über den vorhandenen Maschinen-Client an den Plotter (gleicher Ablauf wie
|
||
„Manuell drucken“, inkl. Formatwechsel-Prüfung und Bestätigung im Tab
|
||
„Druckvorgang“). Da dies über den gleichen Origin läuft, ist hierfür kein CORS nötig.
|
||
|
||
### Backend-URL setzen
|
||
|
||
Die URL des Signatur-Service ist konfigurierbar (Default `http://localhost:8000`,
|
||
hinter dem Nginx Proxy Manager z.B. `https://api-sign.skrift.de`):
|
||
|
||
- **Im Tab:** Feld *„Signatur-Service (Backend-URL)“* ausfüllen und *Speichern*.
|
||
- Persistiert wird in `config.json` unter `app.signature_backend_url`
|
||
(Endpunkte `GET`/`POST /api/signature/backend-url` der Skrift-App).
|
||
|
||
Damit das Frontend (Port `8765`) den Service (Port `8000`) erreichen darf, ist im
|
||
Service **CORS** offen konfiguriert.
|