# 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'' f'' f'\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}')