Files
Webklar/scripts/relaunch-bilder.py
T
rootandClaude Opus 5.5 8f5d522682 Relaunch: Portal-Bild für Öffnungszeiten live
Echte Aufnahme des Portal-Editors (Handy 402x874, Hinweis-Leiste mit
Betriebsurlaub, Knopf "Hinweis-Leiste anlegen") ersetzt den Platzhalter
(Kopie von phone-9-termine). Tippunkt aus der gemessenen Knopfmitte
(135 / 488) in HINWEIS_PUNKT und HINWEIS_ANLEGEN_PUNKT; Test pinnt, dass
beide gleich sind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:17:48 +00:00

134 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""
Relaunch: verkleinert die Rohexporte der Kundenportal-Screenshots für die Website.
docs/relaunch/assets/portal/*.webp (Rohdateien, doppelte Auflösung)
-> public/relaunch/portal/<name>-<breite>.webp
Aufruf (aus dem Projektverzeichnis): python3 scripts/relaunch-bilder.py [NAME ...]
Ohne Argument werden alle Bilder neu geschrieben (alte Ausgaben vorher entfernt). Mit einem oder mehreren NAME-Filtern
(Dateiname ohne .webp, auch als Anfang, z. B. "phone-9" oder "phone-10-analyse") entstehen nur die passenden Bilder;
alle anderen Dateien in public/relaunch/portal bleiben unangetastet.
Nur die Bilder, die der Abschnitt "Kundenportal" (Spezifikation 7.2) wirklich zeigt, landen in public/.
Format und Budgets laut Spezifikation 9.5:
- WebP, Qualität etwa 80, Desktop-Bilder in 800 / 1200 / 1600 px Breite, Handy-Bilder in 390 / 780 px
- ein einzelnes Desktop-Bild höchstens 120 KB, ein einzelnes Handy-Bild höchstens 60 KB
(der lange Screenshot hat ein eigenes, größeres Budget, weil er mehr als dreimal so hoch ist)
- wird ein Budget überschritten, sinkt die Qualität pro Datei schrittweise (nicht unter MIN_QUALITAET)
Braucht Python 3 und Pillow mit WebP-Unterstützung.
"""
from pathlib import Path
import sys
from PIL import Image
WURZEL = Path(__file__).resolve().parent.parent
QUELLE = WURZEL / "docs" / "relaunch" / "assets" / "portal"
ZIEL = WURZEL / "public" / "relaunch" / "portal"
QUALITAET = 80
MIN_QUALITAET = 46
SCHRITT = 4
KB = 1024
BUDGET_DESKTOP = 120 * KB
BUDGET_HANDY = 60 * KB
# Der lange Screenshot ist gut dreimal so hoch wie ein Handy-Bild (5131 statt 1748 px). Sein Budget
# wächst mit der Höhe (je Bildschirmhöhe weiterhin unter 60 KB), sonst wäre der Text nur noch Matsch.
BUDGET_LANG = {390: 80 * KB, 780: 150 * KB}
# Unter der letzten Inhaltszeile (Zeile 5091) ist der Rohexport nur noch schwarz. Abgeschnitten wird
# 40 px darunter, sonst endet die Verschiebung im Handy-Rahmen auf einem leeren Streifen.
LANG_HOEHE = 5131
# Dateiname (ohne .webp) -> (Art, Zielbreiten)
BILDER = {
# Desktop-Fenster (Roh 3024 x 1786 = MacBook-Safari-Fläche 1512 x 893 @2x); die Live-Vorschau zeigt im MacBook die
# Beispiel-Website selbst (BaeckereiSeite), kein Bild
"desktop-2-fortschritt": ("desktop", (800, 1200, 1600)),
# Chat-Demo (Station Chat): leerer Chat als Kulisse, die Nachrichten sind live; 2400 px, weil die Kamera hineinzoomt
"desktop-3c-chat-leer": ("desktop", (1600, 2400)),
"desktop-4-rechnungen": ("desktop", (800, 1200, 1600)),
# Handy (Roh 804 x 1748 = iPhone 16 Pro @2x, Safe Area 59/34; das lange Bild 804 x 5340, beschnitten auf 5131)
"phone-1-projekte": ("handy", (390, 780)),
"phone-3-projektdetails": ("handy", (390, 780)),
"phone-4c-chat-leer": ("handy", (390, 780)),
"phone-5-rechnungen": ("handy", (390, 780)),
# Funktions-Stationen (Spezifikation 2026-10-04-funktionen-demos.md): Handy-Ansicht der Funktion im Kundenportal
"phone-9-termine": ("handy", (390, 780)),
"phone-10-analyse": ("handy", (390, 780)),
"phone-11-news-musterbach": ("handy", (390, 780)),
"phone-12-zeiten": ("handy", (390, 780)),
# langer Screenshot: behält sein hohes Seitenverhältnis (eigenes Budget, siehe BUDGET_LANG)
"phone-2-projekte-lang": ("handy", (390, 780)),
}
def speichern(bild: Image.Image, ziel: Path, budget: int) -> tuple[int, int]:
"""Speichert als WebP; senkt die Qualität, bis das Budget passt. Gibt (Bytes, Qualität) zurück."""
qualitaet = QUALITAET
while True:
bild.save(ziel, "WEBP", quality=qualitaet, method=6)
groesse = ziel.stat().st_size
if groesse <= budget or qualitaet - SCHRITT < MIN_QUALITAET:
return groesse, qualitaet
qualitaet -= SCHRITT
def waehle(filter_: list[str]) -> dict:
"""Alle Bilder ohne Filter, sonst die, deren Name genau so heißt oder mit einem Filter beginnt (z. B. "phone-9")."""
if not filter_:
return BILDER
# "phone-1" soll nicht auch "phone-10-analyse" treffen: Anfang nur bis zum nächsten Bindestrich-Segment
treffer = {
name: wert for name, wert in BILDER.items()
if any(name == f or name.startswith(f + "-") for f in filter_)
}
unbekannt = [f for f in filter_ if not any(n == f or n.startswith(f + "-") for n in BILDER)]
if unbekannt:
raise SystemExit(f"Unbekannter Bildname: {', '.join(unbekannt)}")
return treffer
def main(argv: list[str]) -> int:
ZIEL.mkdir(parents=True, exist_ok=True)
bilder = waehle(argv)
if not argv:
# alte Ausgaben entfernen, damit nichts Verwaistes liegen bleibt (nur bei einem Lauf über alle Bilder)
for alt in ZIEL.glob("*.webp"):
alt.unlink()
fehler = 0
summe = 0
print(f"{'Datei':44} {'Pixel':>11} {'KB':>7} {'Q':>3} Budget")
for name, (art, breiten) in bilder.items():
quelle = QUELLE / f"{name}.webp"
if not quelle.exists():
print(f"FEHLT: {quelle}", file=sys.stderr)
fehler += 1
continue
roh = Image.open(quelle).convert("RGB")
if name.endswith("-lang"):
roh = roh.crop((0, 0, roh.width, LANG_HOEHE))
for breite in breiten:
if name.endswith("-lang"):
budget = BUDGET_LANG[breite]
else:
budget = BUDGET_DESKTOP if art == "desktop" else BUDGET_HANDY
if breite > roh.width:
print(f"übersprungen: {name} {breite} (Rohbild nur {roh.width} px breit)")
continue
hoehe = round(roh.height * breite / roh.width)
klein = roh if breite == roh.width else roh.resize((breite, hoehe), Image.LANCZOS)
datei = ZIEL / f"{name}-{breite}.webp"
groesse, qualitaet = speichern(klein, datei, budget)
summe += groesse
ok = "ok" if groesse <= budget else f"ÜBER {budget // KB} KB"
print(f"{datei.name:44} {breite:>5}x{hoehe:<5} {groesse / KB:7.1f} {qualitaet:>3} {ok}")
print(f"Summe aller Dateien: {summe / KB:.0f} KB")
return 1 if fehler else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))