Files
skrift-signature-service/README.md
Lucas Orth 1fe6160b21
All checks were successful
Build & Deploy / deploy (push) Successful in 1m15s
Import aus Monorepo (frischer Start)
2026-08-24 12:13:51 +02:00

8.5 KiB
Raw Permalink Blame History

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 (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_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):

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.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.