"""Ein hochgeladenes Bild zu einem brauchbaren Logo machen. Was aus einem Formular kommt, ist selten das, was man anzeigen will: ein Screenshot vom Telefon, drei Megabyte, 3000 Pixel breit, für eine Kachel von dreissig. Hier wird daraus ein kleines PNG. Immer PNG, egal was hereinkam. Das erspart der Anzeige jede Fallunterscheidung und schliesst SVG aus – eine SVG-Datei kann Skript enthalten, und ein Bild, das der Nutzer hochlädt und der Browser ausführt, ist genau die Art Falle, die man sich nicht in ein Finanzprogramm legt. """ from __future__ import annotations import io from PIL import Image, UnidentifiedImageError # Was hochgeladen werden darf. Grosszügig – gerechnet wird ohnehin klein. MAX_UPLOAD = 4 * 1024 * 1024 # Kantenlänge des gespeicherten Bilds. 128 statt 64: Auf einem Bildschirm mit # doppelter Pixeldichte ist eine 64er-Kachel sonst sichtbar unscharf. MAX_KANTE = 128 MIME = "image/png" class LogoFehler(ValueError): """Das Bild lässt sich nicht verwenden – mit einem Grund für den Nutzer.""" def mach_logo(daten: bytes) -> bytes: """Aus hochgeladenen Bytes ein kleines PNG machen. Das Seitenverhältnis bleibt: Ein verzerrtes Logo sieht falscher aus als ein kleines. """ if not daten: raise LogoFehler("Die Datei ist leer.") if len(daten) > MAX_UPLOAD: raise LogoFehler( f"Das Bild ist zu gross ({len(daten) // 1024} KB, erlaubt sind " f"{MAX_UPLOAD // 1024 // 1024} MB)." ) try: bild = Image.open(io.BytesIO(daten)) bild.load() except (UnidentifiedImageError, OSError) as e: raise LogoFehler( "Das ist kein lesbares Bild. PNG, JPEG, WEBP oder GIF gehen." ) from e # Durchsichtigkeit erhalten – Logos sind meist freigestellt, und auf # dunklem Grund fiele ein weisser Kasten sofort auf. if bild.mode not in ("RGBA", "LA"): bild = bild.convert("RGBA") bild.thumbnail((MAX_KANTE, MAX_KANTE), Image.LANCZOS) ziel = io.BytesIO() bild.save(ziel, format="PNG", optimize=True) return ziel.getvalue()