Klarbild

Verwandelt Screenshots und Fundstücke in saubere, druckfertige Bilder — Rahmen und Shop-Oberflächen entfernen, exakte Fotoformate (30×40 cm etc.), The-Frame-Querformat, für den Cricut freistellen, und automatisch nach Picdrop ausliefern.

Claim: „Screenshot rein, sauberes Bild raus."

Stack

Astro 5 (SSR, Node standalone) + React-Inseln · Postgres · S3 (MinIO) · pg-boss · sharp · pdf-lib (Druckbogen) · grammY (Telegram) · OKLCH-Tokens, kein Tailwind. Deploy: Gitea → Coolify → Hetzner.

Entwicklung

npm install
cp .env.example .env   # Werte eintragen
npm run dev

Migrationen und Seeds laufen beim Serverstart automatisch. Erststart-Passwörter über SEED_TILL_PASSWORD / SEED_LEA_PASSWORD.

Funktionen (Kurzüberblick)

  • Modi: Bereinigen (Screenshot säubern/freistellen/formatieren), Umwandeln (ein Bild + Text, z. B. Foto → Ölgemälde), Kombinieren (mehrere Bilder + Text), Neu erzeugen (nur Text).
  • Formate: feste cm-/DIN-/Screen-Formate und frei definierbare — Schlüssel WxH (z. B. 25x35) oder sticker<N> (sticker5 = 5×5 cm). Parsing in src/lib/format.ts, keine Migration nötig.
  • Dateiformat: PNG (verlustfrei, Transparenz) oder JPG (klein, Picdrop-freundlich). Global über settings.output_ext, pro Rezept via recipes.output_ext, pro Umwandlung im Studio. JPG nur ohne Alpha.
  • Auslieferung: Picdrop (SFTP/FTPS) + beliebige Zusatzziele; NAS/Backup-Ziele; optionaler .md-Metadaten-Beileger pro Ziel schaltbar. Nicht-transparente Ergebnisse gehen als JPG an Picdrop.
  • Bibliothek: Ordner, Farbmarkierungen, Prompt-Anzeige, Vorher/Nachher, Vollbild + „In Fotos sichern".
  • Rechte/Datenschutz: Sichtbarkeit (eigene/alle), anonyme Generierungen, private Sessions, NSFW-Gate.
  • Telegram-Bot (grammY-Webhook) als vollwertiger Website-Ersatz. API-Token (Bearer) für /api/*.
  • Drucken (/drucken, ohne KI): Bilder exakt auf physische Maße bringen (interaktiver Zuschnitt mit Zoom/Verschieben) und mehrere davon in 100-%-Größe mit Schnittmarken auf einen Bogen setzen — Passbildsatz, Kita-Satz, Sticker. Papier DIN A6–A2 inkl. A3+, Fotopapier 9×13–20×30, Letter/Legal oder freies Maß. Marken: Eckmarken (0,25 pt, außerhalb des Endformats) oder durchgehende Linien. Beschnittzugabe 0–10 mm. Ausgabe: PDF in 1:1 (pdf-lib) bzw. Einzelbild mit dpi-Metadaten. Layout: gleiche Größen → exaktes Raster, gemischte Größen → MaxRects-Packer. Alle Formate der KI-Generierung sind auch hier wählbar; passt das Bild nicht zum Format, entscheidet man je Bild zwischen Zuschneiden und Rand lassen (mit Randfarbe) — nie verzerren. 86 Bildmaße in 10 Gruppen (Passbild bis 100 × 140 cm), 19 Papierformate. Größer als der Bogen (20 × 30 cm auf A4): je Zelle oversize = fit (melden) · overflow (mittig aufs ganze Blatt, Verlust wird vorab beziffert — hier genau 3 mm) · tile (Posterdruck auf mehrere Blätter mit gestricheltem Klebefalz und Blattnummer). Beschriftung frei: aus, automatisch oder eigener Text an einer von sechs Stellen; sie entfällt automatisch, wenn an der Kante kein Platz frei ist. Dasselbe Bild darf mehrfach als Zelle vorkommen („Duplizieren"), um es in zwei Maßen zu drucken. Kern: src/lib/paper.ts (Formate), src/lib/printlayout.ts (reine Mathematik, getestet), src/lib/printoversize.ts (Überstand & Kachelung, getestet), src/lib/printrender.ts (sharp + pdf-lib). Browser-Vorschau und Server-PDF benutzen denselben Rechenkern. Auch per Telegram („🖨 Drucken") und MCP (exact_size, print_sheet). Öffentliche Vorstellungsseite ohne Login: /vorstellung.
  • Dateinamen (src/lib/naming.ts, abhängigkeitsfrei): JJJJ-MM-TT_HHMMSS_<motiv>_<format>_<kürzel>.<ext>, z. B. 2026-08-20_143207_lea-badge_the-frame_a3f9.jpg. Zeitstempel vorn → jede Dateiliste sortiert chronologisch; Kürzel aus der Item-ID → zwei Bilder können sich beim Ausliefern nicht mehr überschreiben. Zeitzone KLARBILD_TZ (Standard Europe/Berlin). Nachbenennung der Bibliothek und erneute Auslieferung im Admin unter „Dateinamen" (src/lib/renames.ts), idempotent.
  • Teilen (/s/<slug>, öffentlich): ein Bild oder eine Sammlung ohne Konto zugänglich machen — ansehen und in voller Auflösung herunterladen. Voreinstellungen bewusst vorsichtig: Ablauf (Standard 30 Tage), widerrufbar, optionales Zugangswort (argon2), Download abschaltbar, noindex. Zugriff hängt am Link, nicht am Bild (fremde item-Kennung ⇒ 404); private Sitzungen lassen sich nie teilen. Verwaltung unter /teilen mit Aufrufen und Downloads. Gezählt wird wann und womit, nicht wer: IP nur gehasht, gesalzen mit Server-Geheimnis und Link-Kennung, eine Zeile je Besucher und Stunde, Löschung nach 90 Tagen. Alle Bilder als ZIP über GET /api/s/<slug>/zip — strömend, eine Datei nach der anderen (440 MB Bilder → 196 MB Spitzenspeicher gemessen), gedeckelt bei 200 Dateien und 500 MB. Kern: src/lib/shares.ts.
  • Weitergeben: per E-Mail aus Klarbild heraus (mehrere Empfänger, kein Anhang — ein Link lässt sich zurückziehen), über das eigene Mailprogramm oder per wa.me-Link, der WhatsApp mit fertigem Text öffnet. (Server-seitiger WhatsApp-Versand bräuchte die Business-API: Meta-Konto, genehmigte Vorlagen, Gebühren je Konversation.)
  • E-Mail (src/lib/mail.ts): SMTP über das eigene Postfach — SPF/DKIM stimmen dadurch von selbst und kein Versanddienst sieht mit. Zugang AES-256-GCM in settings. Trägt Passwort-Reset (Einmal-Token, nur Hash gespeichert, 60 min, gleiche Antwort für unbekannte Adressen), Auftragshinweise (je Nutzer, /konto) und den Versand von Teilen-Links.
  • Sitzungen: signiertes Cookie plus serverseitige Prüfung (session_epoch). Passwortwechsel und Rollenänderung entwerten alle offenen Anmeldungen; die Rolle kommt aus der Datenbank, nicht aus dem Cookie.
  • KI-Kennzeichnung (src/lib/provenance.ts): IPTC DigitalSourceType als XMP direkt in der Datei (trainedAlgorithmicMedia bzw. compositeWithTrainedAlgorithmicMedia) samt Modell — von Hand in PNG-iTXt und JPEG-APP1 geschrieben, ohne neue native Abhängigkeit. Mit exiftool prüfbar.
  • Sicherheits-Header: CSP mit echten Hashes für jedes inline-Skript (experimental.csp), dazu nosniff, referrer-policy, X-Frame-Options, permissions-policy. style-src bleibt offen, weil React Stile als Attribute setzt und Attribute sich nicht hashen lassen.
  • Admin: Key/Picdrop/Modelle/Nutzer/Kostendeckel/Presets/Speicherverwaltung, E-Mail, Teilen-Links, Dateinamen, Picdrop-Diagnose.

Datenmodell / Migrationen

Nummerierte SQL in migrations/, beim Start automatisch (kein ORM-Push). Wichtige Spalten: items.has_alpha, jobs.status (inkl. failed), jobs.finished_at, settings.output_ext / recipes.output_ext, settings.*_metadata_sidecar, delivery_targets.*, print_presets (Bogen-Vorlagen, 013), shares/share_items/share_views + settings.smtp_* + users.email/notify_jobs (014), users.session_epoch (015).

Tests

npm test — Node-Testrunner mit nativem Type-Stripping (node --experimental-strip-types). Kein tsx, kein playwright in package.json — der Docker-Build installiert devDependencies, und deren native postinstall-Schritte haben den Deploy schon zweimal zerlegt. Testwerkzeug gehört außerhalb des Projekts installiert. Deckt die Druck-Layoutmathematik ab (Maß-Parsing, mm → px exakt, Überlappungsfreiheit, Ränder, Beschnittzugabe, Markengeometrie, Mehrseitigkeit).

Stolpersteine & Lösungen (Learnings)

  • „Bild bei Picdrop ✓, aber nicht in der Galerie": Fehldiagnose vermeiden — die Dateien lagen die ganze Zeit korrekt per SFTP in /POSTER LEA (per Admin → Picdrop → „Ordner anzeigen" verifizierbar). Der eigentliche Grund war die Sortierung in der Picdrop-Weboberfläche, nicht das Format. Unabhängig davon liefern wir nicht-transparente Ergebnisse jetzt als JPG aus (src/lib/delivery.ts, deliverableBuffer): ~25 MB PNG → ~2 MB JPG, schneller und handlicher — plus wählbares Ausgabeformat (output_ext).
  • „Unbekanntes Format: sticker5": Seed legte ein Rezept mit Formatschlüssel sticker5 an, den der Resolver nicht kannte → jeder Lauf schlug fehl. Lösung: generisches Parsen von sticker<N>/WxH.
  • Auftrag trotz Fehler grün: jobs.status kannte kein failed; alle Positionen fehlgeschlagen wurde trotzdem done. Lösung: Status failed, wenn done=0 && failed>0 (src/worker.ts maybeComplete).
  • Doppelte Datums-/UUID-Namen: \b-Wortgrenzen greifen nicht an Unterstrichen. Lösung: \b entfernt, Datum/UUID/Hex global ersetzt (src/lib/pipeline.ts cleanMotif).
  • Content-Type bei gemischten PNG/JPG-Ergebnissen: file.ts snifft jetzt die Magic Bytes.
  • .tmp--Reste auf Picdrop: früher Temp-Rename-Bug; cleanupTmp + Admin-Knopf „Reste aufräumen".

Status

Reihenfolge und Nachweise siehe CLAUDE.md. Anforderungen im Handover-Paket (Imagetool-Lea/00..07). Führend: 01-anforderungen.md.

S
Description
Klarbild — Screenshots & Fundstuecke zu druckfertigen Bildern (Astro+Postgres+S3)
Readme
2.4 MiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 86.6%
Astro 11.3%
JavaScript 1.7%
CSS 0.3%
Dockerfile 0.1%