Files
d4rkbot/docs/brand/build.py
T
D4rkst3randClaude Fable 5 c0a56c1eb5
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Logo auf der Willkommens-Karte und in geteilten Links
Zwei Stellen, an denen die Marke bisher fehlte.

Willkommens-Karte: Das Wasserzeichen war der Markenname als Text mit 5 %
Deckkraft — jetzt der Katzenkopf. Die Datei wird einmal beim Start gelesen
statt bei jedem Beitritt, und fehlt sie, rendert die Karte wie bisher ohne
Wasserzeichen. Der Schalter heisst entsprechend "Logo im Hintergrund".

Link-Vorschau: og:image war nur gesetzt, wenn ein Devlog einen Screenshot
hatte. Startseite, Roadmap, Funktionen und jedes Devlog ohne Bild kamen in
Discord als graue Textzeile. Jetzt faellt alles ohne eigenes Bild auf
og-default.png zurueck (1200x630, Lockup auf Schwarz mit Akzentstreifen),
und twitter:card ist durchgehend summary_large_image.

Die Laufzeit-Datei liegt unter src/bot/assets/, nicht in docs/ — das
Dockerfile kopiert nur src/ und frontend/dist/.

Geprueft: librsvg zeichnet das eingebettete <image> wirklich (42726 Pixel
unterscheiden sich gegen eine Fassung ohne), das OG-Bild landet im dist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 03:55:28 +02:00

155 lines
6.6 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)
DUNKEL = (10, 10, 10, 255) # --bg der Marke
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}')