"""Ein Windows-Symbol, das Windows auch wirklich anzeigt. ## Der Befund (28.08.2026) Commander: *„Startmenü, Desktop und Taskleisten-Icon ohne icon file (Weißes Leeres Blatt)."* Nachgesehen, was in `deploy/worker-windows/rippy.ico` steckte: Bilder = 1 256x256 32 bit 5657 Bytes PNG **Genau ein Bild, und zwar das größte.** Windows holt sich für jede Stelle die passende Größe aus der Datei: Taskleiste, Verknüpfung klein 16x16 Desktop, Startmenü 32x32 / 48x48 Große Kacheln, Alt-Tab 256x256 Fehlt die passende Größe, skaliert die Shell nicht immer selbst — sie zeigt das leere Blatt. Bei einer Datei, die NUR ein PNG-komprimiertes 256er enthält, passiert das zuverlässig. ## Was diese Datei tut Sie baut aus dem vorhandenen Symbol eine `.ico` mit allen gebrauchten Größen. Die kleinen Größen werden mit `LANCZOS` verkleinert — bei 16x16 ist das der Unterschied zwischen einem erkennbaren Zeichen und Pixelbrei. Aufruf (macht der Bau selbst, siehe `build.py`): python packaging/windows/icon.py [--quelle DATEI] [--ziel DATEI] """ import argparse import os # Was Windows an den einzelnen Stellen holt. 24 und 64 sind fuer hohe # Bildschirmaufloesungen (150 %/200 % Skalierung) — ohne sie skaliert die # Shell aus 32 bzw. 48 hoch, und das sieht ausgefranst aus. GROESSEN = (16, 20, 24, 32, 40, 48, 64, 128, 256) HIER = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.dirname(os.path.dirname(HIER)) QUELLE = os.path.join(REPO, "deploy", "worker-windows", "rippy.ico") def groessen_in(pfad: str) -> list: """Welche Bildgroessen stecken in einer .ico? (ohne Pillow, reines Lesen) Fuer die Pruefung im Bau: Eine Datei mit nur EINER Groesse ist genau der Fehler, den es hier zu verhindern gilt. """ import struct with open(pfad, "rb") as f: roh = f.read() if len(roh) < 6: return [] reserviert, typ, anzahl = struct.unpack(" tuple: """Welche Pflichtgroessen fehlen? Leer heisst: alles da.""" vorhanden = set(groessen_in(pfad)) return tuple(k for k in PFLICHTGROESSEN if k not in vorhanden) def bauen(quelle: str = None, ziel: str = None) -> str: """Baut die vollstaendige .ico. Gibt den Zielpfad zurueck. Wirft, wenn es nicht geht — ein Symbol, das still fehlschlaegt, fuehrt genau zu dem leeren Blatt, das hier abgestellt werden soll. """ from PIL import Image quelle = quelle or QUELLE ziel = ziel or quelle bild = Image.open(quelle) bild = bild.convert("RGBA") # Aus dem groessten vorhandenen Bild verkleinern, nicht aus einem # zufaellig ausgewaehlten: Pillow oeffnet bei einer .ico sonst die erste # Groesse, und aus 16x16 laesst sich kein 256er machen. if hasattr(bild, "size") and bild.size[0] < 256: try: bild = Image.open(quelle) bild.size = (256, 256) # Pillow: gewuenschte Groesse waehlen bild = bild.convert("RGBA") except Exception: # noqa: BLE001 pass ebenen = [] for kante in GROESSEN: if kante >= bild.size[0]: ebenen.append(bild.copy().resize((kante, kante), Image.LANCZOS)) else: # LANCZOS auch beim Verkleinern: bei 16x16 ist das der Unterschied # zwischen einem erkennbaren Zeichen und Pixelbrei. ebenen.append(bild.resize((kante, kante), Image.LANCZOS)) ebenen[-1].save(ziel, format="ICO", sizes=[(k, k) for k in GROESSEN]) return ziel def main(argv=None) -> int: p = argparse.ArgumentParser(description="Baut eine vollstaendige rippy.ico") p.add_argument("--quelle", default=QUELLE) p.add_argument("--ziel", default=None) p.add_argument("--nur-pruefen", action="store_true") args = p.parse_args(argv) vorher = groessen_in(args.quelle) print("Vorher: %s" % (vorher or "keine lesbare .ico")) if args.nur_pruefen: return 0 if len(vorher) > 1 else 1 ziel = bauen(args.quelle, args.ziel) print("Nachher: %s" % groessen_in(ziel)) print("Geschrieben: %s (%d Bytes)" % (ziel, os.path.getsize(ziel))) return 0 if __name__ == "__main__": raise SystemExit(main())