Files
D4rkst3randClaude Opus 5 0d7628f14e
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Den Entwurf und das Logo-Original einsortieren
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>
2026-08-18 15:19:49 +02:00

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}')