# 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 ``. 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_.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.