Drei Dateien lagen unversioniert im Wurzelverzeichnis herum. Alle drei gehoeren ins Repo, nur nicht dorthin: logo.png -> docs/brand/logo.png ChatGPT Image 23. Juni 2025 ... -> docs/brand/studie-kopf-2025-06.png design_handoff_redesign/ -> docs/redesign/ logo.png ist NICHT irgendein Logo, sondern das Original, von dem docs/brand/README.md die ganze Zeit spricht: "ein einziges Original, 2048x2048 mit Transparenz". Nachgerechnet statt geraten -- auf den Inhalt beschnitten ergibt es 1986x1963, exakt die Masse von lockup.png. Es lag also die Quelle der Marke unversioniert daneben. Dabei aufgefallen und in der README vermerkt, nicht repariert: build.py liest das Original ueber einen festen Pfad ausserhalb des Repos (QUELLE = A:/Users/DAR/logo.png). Jetzt sind das zwei Kopien, die auseinanderlaufen koennen. Umstellen kann nur, wer weiss, welche der beiden gepflegt wird. Das zweite Bild ist eine frueherere Kopf-Studie und nicht die Vorlage der heutigen Marke -- andere Zeichnung, flacher, ohne Fell und Schulter. Sie wird nirgends benutzt und liegt als Herkunft. Vom Entwurfs-Paket ist der assets/-Ordner entfallen: er enthielt Kopien von mark.png und lockup.png, die zwei Verzeichnisse weiter im Original liegen. Die .dc.html-Referenzen zeigen jetzt auf ../brand/ und stellen das Logo weiter dar, statt mit kaputten Bildern zurueckzubleiben. Der Kopf der README sagt ausserdem, dass der Entwurf umgesetzt ist und wo bewusst abgewichen wurde -- sonst liest sich das Dokument in einem Jahr wie eine offene Aufgabe. Dazu eine Zeile, die seit dem Redesign nicht mehr stimmte: build.py bezeichnet (10, 10, 10) als "--bg der Marke". Das ist seit gestern #131316. Wirkt erst, wenn jemand build.py laufen laesst -- die erzeugten Icons in frontend/public/ tragen bis dahin den alten Grund. Neu bauen konnte ich sie hier nicht, es gibt weder Python noch Pillow auf dieser Maschine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
157 lines
6.7 KiB
Python
157 lines
6.7 KiB
Python
# Baut aus dem Original-Logo den Satz an Marken-Dateien:
|
|
# - die Katzenkopf-Marke freigestellt (fuer Favicon, Avatar, kleine Groessen)
|
|
# - das volle Lockup beschnitten (fuer Kopfzeilen, Discord, OG-Bilder)
|
|
# - Icons auf dunklem Grund (iOS und Gitea-Standardavatar moegen kein Alpha)
|
|
# - SVG-Huellen mit eingebettetem PNG (Gitea will .svg, das Original ist
|
|
# eine Rastergrafik mit Verlaeufen — nachvektorisieren wuerde matschen)
|
|
import base64
|
|
import io
|
|
import pathlib
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
QUELLE = pathlib.Path('A:/Users/DAR/logo.png')
|
|
MARKE = pathlib.Path('A:/eco/dc_devlog_bot/docs/brand')
|
|
GITEA = pathlib.Path('A:/eco/Gitea/data/gitea/public/assets/img')
|
|
FRONTEND = pathlib.Path('A:/eco/dc_devlog_bot/frontend/public')
|
|
# Das Dockerfile kopiert nur src/ und frontend/dist/ — was der Bot zur Laufzeit
|
|
# braucht, muss deshalb unter src/ liegen, nicht in docs/.
|
|
BOT_ASSETS = pathlib.Path('A:/eco/dc_devlog_bot/src/bot/assets')
|
|
|
|
# Katzenkopf mit Ohrenspitzen und etwas Schulter, ohne Schriftzug
|
|
# (Koordinaten im 2048er Original)
|
|
KOPF = (330, 20, 1740, 1430)
|
|
# --bg der Marke. Seit dem Redesign „Graphit + Orange" #131316 statt #0a0a0a.
|
|
# Die bereits erzeugten Icons tragen noch den alten Grund, bis das hier laeuft.
|
|
DUNKEL = (19, 19, 22, 255)
|
|
|
|
for d in (MARKE, GITEA, FRONTEND, BOT_ASSETS):
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
|
|
original = Image.open(QUELLE).convert('RGBA')
|
|
marke = original.crop(KOPF)
|
|
lockup = original.crop(original.getchannel('A').getbbox())
|
|
|
|
|
|
def png_bytes(bild, groesse=None):
|
|
aus = bild if groesse is None else bild.resize((groesse, groesse), Image.LANCZOS)
|
|
puffer = io.BytesIO()
|
|
aus.save(puffer, 'PNG', optimize=True)
|
|
return puffer.getvalue()
|
|
|
|
|
|
def gerundet(bild, kante, radius_anteil=0.22, rand=0.08):
|
|
"""Marke mittig auf dunklem, abgerundetem Quadrat — deckend."""
|
|
flaeche = Image.new('RGBA', (kante, kante), (0, 0, 0, 0))
|
|
maske = Image.new('L', (kante, kante), 0)
|
|
ImageDraw.Draw(maske).rounded_rectangle(
|
|
(0, 0, kante - 1, kante - 1), radius=int(kante * radius_anteil), fill=255)
|
|
flaeche.paste(Image.new('RGBA', (kante, kante), DUNKEL), (0, 0), maske)
|
|
|
|
innen = int(kante * (1 - 2 * rand))
|
|
klein = bild.resize((innen, innen), Image.LANCZOS)
|
|
flaeche.alpha_composite(klein, ((kante - innen) // 2, (kante - innen) // 2))
|
|
return flaeche
|
|
|
|
|
|
def svg_huelle(daten, kante):
|
|
"""SVG mit eingebettetem PNG — gueltig, und Gitea nimmt nur .svg."""
|
|
b64 = base64.b64encode(daten).decode('ascii')
|
|
return (
|
|
f'<svg xmlns="http://www.w3.org/2000/svg" '
|
|
f'xmlns:xlink="http://www.w3.org/1999/xlink" '
|
|
f'viewBox="0 0 {kante} {kante}" width="{kante}" height="{kante}">'
|
|
f'<image width="{kante}" height="{kante}" '
|
|
f'xlink:href="data:image/png;base64,{b64}"/>'
|
|
f'</svg>\n'
|
|
)
|
|
|
|
|
|
geschrieben = []
|
|
|
|
|
|
def schreib(pfad, daten):
|
|
pfad.write_bytes(daten if isinstance(daten, bytes) else daten.encode('utf-8'))
|
|
geschrieben.append((pfad, len(daten)))
|
|
|
|
|
|
# ── Quelldateien im Repo, versioniert ────────────────
|
|
schreib(MARKE / 'mark.png', png_bytes(marke, 1024))
|
|
schreib(MARKE / 'lockup.png', png_bytes(lockup))
|
|
|
|
# ── Gitea ────────────────────────────────────────────
|
|
# Navbar-Logo und Favicon: die Marke, das Lockup ist bei 30 px unlesbar.
|
|
# 192 px reichen — Gitea zeigt das Logo in der Navbar mit rund 30 px, und die
|
|
# Datei haengt an jedem Seitenaufruf.
|
|
schreib(GITEA / 'logo.svg', svg_huelle(png_bytes(marke, 192), 192))
|
|
schreib(GITEA / 'favicon.svg', svg_huelle(png_bytes(gerundet(marke, 128), 128), 128))
|
|
# Gitea verweist zusaetzlich auf favicon.png als "alternate icon" — ohne die
|
|
# Datei zeigen aeltere Browser dort weiter die Gitea-Tasse.
|
|
schreib(GITEA / 'favicon.png', png_bytes(gerundet(marke, 180)))
|
|
schreib(GITEA / 'apple-touch-icon.png', png_bytes(gerundet(marke, 180)))
|
|
schreib(GITEA / 'avatar_default.png', png_bytes(gerundet(marke, 512)))
|
|
|
|
# ── Hub + Bot-Produktseite ───────────────────────────
|
|
schreib(FRONTEND / 'favicon.svg', svg_huelle(png_bytes(gerundet(marke, 128), 128), 128))
|
|
schreib(FRONTEND / 'apple-touch-icon.png', png_bytes(gerundet(marke, 180)))
|
|
|
|
|
|
def og_bild(breite=1200, hoehe=630):
|
|
"""Rueckfall-Vorschaubild fuer geteilte Links (Discord, WhatsApp, Twitter).
|
|
|
|
1200x630 ist das Format, das alle Netze erwarten. Ohne so ein Bild zeigt
|
|
Discord bei einem geteilten Link gar nichts.
|
|
"""
|
|
blatt = Image.new('RGBA', (breite, hoehe), DUNKEL)
|
|
|
|
# Akzentstreifen oben und unten, wie auf der Willkommens-Karte
|
|
streifen = Image.new('RGBA', (breite, 6), (0, 0, 0, 0))
|
|
zeichner = ImageDraw.Draw(streifen)
|
|
for x in range(breite):
|
|
t = x / (breite - 1)
|
|
# Gold nach Orange, an den Raendern ausblendend
|
|
r = int(245 + (255 - 245) * t)
|
|
g = int(197 + (77 - 197) * t)
|
|
b = int(24 + (0 - 24) * t)
|
|
rand = min(t, 1 - t) / 0.3
|
|
zeichner.line([(x, 0), (x, 5)], fill=(r, g, b, int(255 * min(1.0, rand))))
|
|
blatt.alpha_composite(streifen, (0, 0))
|
|
blatt.alpha_composite(streifen, (0, hoehe - 6))
|
|
|
|
# Lockup mittig, auf 70 % der Hoehe
|
|
ziel_h = int(hoehe * 0.70)
|
|
ziel_b = int(lockup.width * ziel_h / lockup.height)
|
|
klein = lockup.resize((ziel_b, ziel_h), Image.LANCZOS)
|
|
blatt.alpha_composite(klein, ((breite - ziel_b) // 2, (hoehe - ziel_h) // 2))
|
|
return blatt
|
|
|
|
|
|
puffer = io.BytesIO()
|
|
og_bild().convert('RGB').save(puffer, 'PNG', optimize=True)
|
|
schreib(FRONTEND / 'og-default.png', puffer.getvalue())
|
|
|
|
# ── Bot zur Laufzeit ─────────────────────────────────
|
|
# Wasserzeichen der Willkommens-Karte. Klein halten: die Datei wird beim Rendern
|
|
# als Data-URI ins SVG gelegt, und die Karte laeuft bei jedem Beitritt.
|
|
schreib(BOT_ASSETS / 'mark-card.png', png_bytes(marke, 220))
|
|
|
|
for pfad, n in geschrieben:
|
|
print(f'{n/1024:8.1f} KB {pfad}')
|
|
|
|
# ── Vorschau: liest die Marke in klein noch? ─────────
|
|
# Landet neben diesem Skript, oben auf hellem, unten auf dunklem Grund —
|
|
# so sieht man vor dem Ausrollen, ab welcher Groesse es kippt.
|
|
KANTEN = (16, 32, 64, 128)
|
|
breite = sum(k + 40 for k in KANTEN) + 30
|
|
vorschau = Image.new('RGBA', (breite, 200), (24, 24, 28, 255))
|
|
x = 30
|
|
for kante in KANTEN:
|
|
for grund, y in (((250, 250, 250, 255), 20), (DUNKEL, 110)):
|
|
feld = Image.new('RGBA', (kante + 16, kante + 16), grund)
|
|
feld.alpha_composite(gerundet(marke, kante), (8, 8))
|
|
vorschau.alpha_composite(feld, (x, y))
|
|
x += kante + 40
|
|
ziel = pathlib.Path(__file__).parent / 'preview.png'
|
|
vorschau.convert('RGB').save(ziel)
|
|
print(f'\nVorschau ({"/".join(map(str, KANTEN))} px): {ziel}')
|