Steuerrechnungstool/wasserzeichen.py
TheMockTv 8cdd106a08 Windows-Symbol in den Programmfarben
Sein Befund: "hast du bei dem windows icon das gleich farbige wie im
programm, wo als wasserzeichen gilt, gemacht - weil das schaut blau aus".

Er hatte recht: auf der Kachel stand das ROHE Logo mit seinem Petrol
#004860, waehrend im Programm (Kopfzeile, Wasserzeichen) laengst die
umgefaerbte Fassung laeuft. Zwei Farben fuer dasselbe Zeichen.

Jetzt geht das Logo auch fuers Symbol durch _umfaerben() - dieselbe
Funktion, dieselben Farben: Tuerkis fuer die Wolke, Dunkelgrau fuer das
Zeichen daneben. Die Kachel ist weiss, also gilt die HELLE Fassung.

Fassung 1.9.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-07 10:52:02 +02:00

237 lines
9.7 KiB
Python

# -*- 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, grund=(0xFF, 0xFF, 0xFF), rand=0.16, radius=0.22):
"""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, `radius` die Eckenrundung - beides
als Anteil der Kantenlaenge, damit es in jeder Groesse gleich aussieht.
Die 22 % entsprechen 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 * radius), fill=255)
flaeche = Image.new("RGBA", (fein, fein), tuple(grund) + (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, dunkel=False):
"""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.
`dunkel` wird nicht mehr gebraucht und nur noch entgegengenommen, damit
bestehende Aufrufe weiterlaufen: seit das Symbol eine helle Kachel ist, ist
es in jeder Darstellung sichtbar. Vorher gab es dafuer eine zweite,
aufgehellte Datei - die ist mit der Kachel ueberfluessig geworden.
"""
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)