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>
134 lines
6.1 KiB
Python
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:]))
|