# -*- coding: utf-8 -*- """Das Programmlogo - als Fenstersymbol und als blasses Wasserzeichen. WOHER DAS BILD KOMMT -------------------- Die Quelle ist die SVG aus dem Entwurf (`assets/programm_logo.svg`). Sie wird BEIM BAUEN einmal in 512 Pixel gerendert (`assets/programm_logo.png`) und in eine `programm.ico` fuer Windows umgesetzt. Hier zur Laufzeit wird nur noch skaliert. Warum nicht direkt aus der SVG zeichnen? Weil dafuer `svglib` und `lxml` mit in die EXE muessten - und bis 512 Pixel ist das Ergebnis pixelgenau dasselbe. Wird das Wasserzeichen je groesser gebraucht, gehoert das umgestellt. Warum ueberhaupt eine ICO? Weil Windows das Symbol einer EXE als Ressource in der Datei erwartet, und dieses Format ist ICO. Das gilt fuer jede Sprache, nicht nur fuer Python - eine SVG kann Windows dort nicht verwenden. Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie hinueber. """ import os import sys import tkinter as tk _lager = {} # (fenster, groesse, deckkraft) -> PhotoImage def _ordner(): """Alle Orte, an denen die Bilder liegen koennen - Quelle wie EXE.""" hier = os.path.dirname(os.path.abspath(__file__)) aus_exe = getattr(sys, "_MEIPASS", "") neben_exe = os.path.dirname(os.path.abspath(sys.executable)) \ if getattr(sys, "frozen", False) else "" return [p for p in (aus_exe, neben_exe, hier) if p] def pfad(name): """Voller Pfad zu einer Datei in assets/ - oder ''.""" for basis in _ordner(): p = os.path.join(basis, "assets", name) if os.path.exists(p): return p return "" def kachel(bild, kante, rand=0.16): """Das Logo auf eine helle Kachel mit runden Ecken setzen. Seine Ansage am 07.09.2026: *"bei den windows icons kannst du einen weissen bg setzen hinter die icons, mit abgerundeten ecken, und da das svg drauf - das geht auf dem desktop unter"*. Ein freigestelltes Zeichen ohne Flaeche verschwindet auf einem dunklen Hintergrundbild - und welchen Hintergrund jemand eingestellt hat, weiss das Programm nicht. Eine helle Kachel ist auf jedem sichtbar; so machen es die meisten Programme, und deshalb sieht es auch nicht fremd aus. `rand` ist der Abstand zum Kachelrand, als Anteil der Kantenlaenge - so sieht es in jeder Groesse gleich aus. Die Rundung liegt fest bei 22 %, etwa dem, was Windows fuer Kacheln verwendet. """ from PIL import Image, ImageDraw kante = int(kante) # In vierfacher Groesse zeichnen und herunterrechnen: sonst werden die # runden Ecken bei 32 oder 48 Pixeln treppig. fein = kante * 4 maske = Image.new("L", (fein, fein), 0) ImageDraw.Draw(maske).rounded_rectangle( (0, 0, fein - 1, fein - 1), radius=int(fein * 0.22), fill=255) flaeche = Image.new("RGBA", (fein, fein), (0xFF, 0xFF, 0xFF, 255)) flaeche.putalpha(maske) innen = max(1, int(fein * (1 - 2 * rand))) zeichen = bild.convert("RGBA").resize((innen, innen), Image.LANCZOS) versatz = (fein - innen) // 2 flaeche.paste(zeichen, (versatz, versatz), zeichen) return flaeche.resize((kante, kante), Image.LANCZOS) # Groessen, die eine Windows-ICO enthalten soll. 16 und 24 fuer Titelleiste # und Listen, 32/48 fuer den Desktop, 256 fuer die grosse Ansicht im Explorer. ICO_GROESSEN = (16, 24, 32, 48, 64, 128, 256) def symbol_bauen(ziel): """Die Windows-ICO aus dem Logo erzeugen. Gibt den Pfad zurueck oder ''. JEDE Groesse wird als Kachel gebaut: helle Flaeche, runde Ecken, Zeichen darauf - *"so wie das rustdesk, nur halt alle ecken in der rundung"*. Ein freigestelltes Logo verschwindet sonst auf einem dunklen Desktop-Hintergrund. Auch die 16 Pixel der Titelleiste: dort ist die Kachel zwar winzig, aber sichtbar. Ein freigestelltes Zeichen war dort je nach Darstellung mal da und mal nicht - und ein Symbol, das manchmal fehlt, ist schlechter als eines, das immer klein ist. Bei den kleinsten Groessen wird der Rand knapper gefasst: bliebe der Abstand anteilig gleich, waere vom Zeichen bei 16 Pixeln fast nichts mehr uebrig. """ quelle = pfad("programm_logo.png") if not quelle: return "" try: from PIL import Image roh = Image.open(quelle).convert("RGBA") # Dieselben Farben wie IM Programm: Tuerkis fuer die Wolke, Dunkelgrau # fuer das Zeichen daneben. Vorher stand hier das rohe Logo mit seinem # Petrol #004860 - sein Befund am 07.09.2026: "das schaut blau aus". # Die Kachel ist weiss, also gilt die HELLE Fassung (dunkel=False). zeichen = _umfaerben(roh, dunkel=False) bilder = [] for g in ICO_GROESSEN: bilder.append(kachel(zeichen, g, rand=0.10 if g <= 24 else 0.16)) # Pillow schreibt die uebrigen Groessen selbst aus dem groessten Bild - # deshalb wird das groesste uebergeben und der Rest angehaengt. bilder[-1].save(ziel, format="ICO", sizes=[(g, g) for g in ICO_GROESSEN], append_images=bilder[:-1]) return ziel except Exception as e: # noqa: BLE001 - ohne eigenes Symbol laeuft es weiter print("[symbol] konnte nicht gebaut werden:", e) return "" def symbol_setzen(fenster, melden=print): """Das Programmsymbol auf ein Fenster legen. True bei Erfolg. Die EXE traegt es ohnehin - das Fenster aber nicht von selbst. Ohne das steht in der Titelleiste das nackte Tk-Symbol, obwohl die Datei ein eigenes hat. Frueher gab es dafuer eine zweite, aufgehellte ICO und einen Schalter `dunkel`. Seit das Symbol eine helle Kachel ist, ist es in jeder Darstellung sichtbar - beides ist damit weggefallen. """ p = pfad("programm.ico") if not p: melden("[symbol] assets/programm.ico nicht gefunden") return False try: fenster.iconbitmap(default=p) return True except Exception as e: # noqa: BLE001 - ohne Symbol laeuft es weiter melden(f"[symbol] konnte nicht gesetzt werden: {e}") return False # Die Farben des Logos - gemessen, nicht geraten: # #004860 das dunkle Blaugruen (Wolke, Pfeile) # #181818 das fast schwarze Grau (Dokument bzw. Paragraphenzeichen) # # Seine Ansage am 07.09.2026: *"das dunkel blaue machen wir in unserm tuerkis # gruen und die andere seite in weiss ... wenn es dunkel ist, und beim hellen # machen wir weiss in dunkel grau"*. Das Zeichen traegt damit dieselbe Farbe # wie das Programm - und die zweite Haelfte steht in beiden Darstellungen im # Kontrast statt unterzugehen. TUERKIS = (0x06, 0xC6, 0xA4) # die Programmfarbe, wie col_accent NEUTRAL_DUNKEL = (0xF2, 0xF3, 0xF5) # fast weiss - auf dunklem Grund NEUTRAL_HELL = (0x33, 0x36, 0x3B) # dunkelgrau - auf hellem Grund def _umfaerben(bild, dunkel): """Das Logo auf die Programmfarben umsetzen. Unterschieden wird nach SAETTIGUNG, nicht nach dem genauen Farbwert: das Blaugruen ist kraeftig gesaettigt, das Grau gar nicht. So bleibt die Zuordnung auch dann richtig, wenn die Vorlage einmal in einem anderen Ton gezeichnet wird - und die weichen Kanten aus dem Skalieren gehen denselben Weg wie die Flaeche, zu der sie gehoeren. Die Deckkraft bleibt unangetastet; die Form kommt aus dem Alphakanal. """ import colorsys bild = bild.convert("RGBA") neutral = NEUTRAL_DUNKEL if dunkel else NEUTRAL_HELL punkte = list(bild.getdata()) zwischen = {} neu = [] for r, g, b, a in punkte: if a == 0: neu.append((r, g, b, a)) continue schluessel = (r, g, b) if schluessel not in zwischen: _, _, saettigung = colorsys.rgb_to_hls(r / 255, g / 255, b / 255) zwischen[schluessel] = TUERKIS if saettigung > 0.25 else neutral neu.append(zwischen[schluessel] + (a,)) bild.putdata(neu) return bild def hole(groesse=240, deckkraft=0.06, master=None, melden=print, dunkel=False): """Das Logo blass als PhotoImage - oder None. `deckkraft` 0..1, klein halten. Ein Wasserzeichen, das man LIEST, stoert beim Arbeiten; eines, das man ahnt, gehoert zum Programm. `master` MUSS mitgegeben werden, wenn mehr als ein Tk-Fenster laeuft: sonst gehoert das Bild dem falschen Fenster und Tk meldet spaeter, es kenne es nicht. """ fenster = master if master is not None else tk._default_root if fenster is None: return None schluessel = (id(fenster), groesse, round(deckkraft, 3), bool(dunkel)) if schluessel in _lager: return _lager[schluessel] p = pfad("programm_logo.png") if not p: melden("[wasserzeichen] assets/programm_logo.png nicht gefunden") return None try: from PIL import Image, ImageTk bild = Image.open(p).convert("RGBA").resize((groesse, groesse), Image.LANCZOS) bild = _umfaerben(bild, dunkel) bild.putalpha(bild.getchannel("A").point(lambda w: int(w * deckkraft))) fertig = ImageTk.PhotoImage(bild, master=fenster) except Exception as e: # noqa: BLE001 - ein Wasserzeichen darf nichts blockieren melden(f"[wasserzeichen] nicht ladbar: {e}") return None _lager[schluessel] = fertig return fertig def kopf(hoehe=28, master=None, melden=print, dunkel=False): """Das Logo fuer die Kopfzeile - volle Deckkraft, kleine Hoehe. Getrennt von hole(): dort geht es um ein blasses Wasserzeichen, hier um ein Erkennungszeichen. Zwei Zwecke, zwei Funktionen - sonst haette eine davon einen Schalter, den man beim Lesen nachschlagen muss. """ return hole(hoehe, 1.0, master=master, melden=melden, dunkel=dunkel)