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>
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_hoeheposition: "unter"→y_sig = y_token + abstand_mm- Skalierung gleichmäßig:
skala = breite_mm / svg_originalbreite(Originalbreite aus der viewBox, nicht auswidth/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 (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):
[
{ "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_idohne 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):
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):
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
pip install -r requirements.txt
uvicorn app:app --host 0.0.0.0 --port 8000
Docker (lokal)
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:
docker compose up -d --build
Die 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
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.jsonunterapp.signature_backend_url(EndpunkteGET/POST /api/signature/backend-urlder Skrift-App).
Damit das Frontend (Port 8765) den Service (Port 8000) erreichen darf, ist im
Service CORS offen konfiguriert.