goon/scripts/generate_icons.py
goon-foss ad0284585b Initial commit
Goon — self-hosted aggregator for adult-content scene metadata.

Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites.
Cross-source deduplication via perceptual hash + Levenshtein distance.
FastAPI backend + APScheduler worker + React Native (Expo) mobile client.

FOSS, ad-free, donation-funded. See README for details.
2026-05-20 10:10:22 +02:00

140 lines
4.7 KiB
Python

"""Generuje ikonki Goona.
Output:
mobile/assets/icon.png — 1024x1024, full square (iOS + fallback)
mobile/assets/adaptive-icon.png — 1024x1024, foreground only (środek 66% safe)
mobile/assets/splash.png — 1024x1024, splash screen
mobile/assets/favicon.png — 48x48 (web)
Design:
- Background: dark gradient (#0E1018 → #1A1D2A) — pasuje do theme.bg/card
- Glyph: lowercase "g" w purple `#8B5CF6` (theme.accent) z accentGlow soft halo
- Adaptive icon: TYLKO glyph (Android docina canvas do koła/kwadratu z border radius)
"""
from __future__ import annotations
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
OUT = Path(__file__).resolve().parents[1] / "mobile" / "assets"
OUT.mkdir(parents=True, exist_ok=True)
ACCENT = (139, 92, 246, 255) # #8B5CF6
ACCENT_GLOW = (167, 139, 250, 255) # #A78BFA
BG_TOP = (14, 16, 24, 255) # #0E1018
BG_BOT = (26, 29, 42, 255) # #1A1D2A
TRANSPARENT = (0, 0, 0, 0)
def _vertical_gradient(size: int, top: tuple, bot: tuple) -> Image.Image:
img = Image.new("RGBA", (size, size), top)
px = img.load()
for y in range(size):
t = y / (size - 1)
r = int(top[0] * (1 - t) + bot[0] * t)
g = int(top[1] * (1 - t) + bot[1] * t)
b = int(top[2] * (1 - t) + bot[2] * t)
for x in range(size):
px[x, y] = (r, g, b, 255)
return img
def _find_font(size: int) -> ImageFont.FreeTypeFont:
candidates = [
"C:/Windows/Fonts/segoeuib.ttf", # Segoe UI Bold
"C:/Windows/Fonts/Arial Bold.ttf",
"C:/Windows/Fonts/arialbd.ttf",
"C:/Windows/Fonts/calibrib.ttf",
]
for path in candidates:
try:
return ImageFont.truetype(path, size)
except (OSError, IOError):
continue
return ImageFont.load_default()
def _draw_g(canvas: Image.Image, *, size: int, glow: bool) -> None:
"""Rysuje lowercase 'g' centered, z opcjonalnym glow halo."""
glyph_size = int(size * 0.78)
font = _find_font(glyph_size)
# 'g' lowercase ma descender (idzie poniżej baseline) — używamy textbbox żeby
# poprawnie wycentrować całość pionowo.
bbox = font.getbbox("g")
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
x = (size - text_w) // 2 - bbox[0]
y = (size - text_h) // 2 - bbox[1]
if glow:
# Glow: rysuj tę samą literę w accentGlow z dużym blur, alpha~50%
glow_layer = Image.new("RGBA", (size, size), TRANSPARENT)
gdraw = ImageDraw.Draw(glow_layer)
gdraw.text((x, y), "g", font=font, fill=(*ACCENT_GLOW[:3], 128))
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(size * 0.04))
canvas.alpha_composite(glow_layer)
draw = ImageDraw.Draw(canvas)
draw.text((x, y), "g", font=font, fill=ACCENT)
def make_icon(size: int = 1024) -> Image.Image:
"""Pełna ikonka: gradient bg + glyph z glow."""
img = _vertical_gradient(size, BG_TOP, BG_BOT)
_draw_g(img, size=size, glow=True)
return img
def make_adaptive_foreground(size: int = 1024) -> Image.Image:
"""Adaptive icon foreground: TYLKO glyph na transparent. Android docina canvas
do circle/squircle/rounded-square — glyph musi mieścić się w 66% środkowych pikseli."""
img = Image.new("RGBA", (size, size), TRANSPARENT)
_draw_g(img, size=size, glow=True)
return img
def make_splash(size: int = 1024) -> Image.Image:
"""Splash screen — full square with gradient + centered glyph (mniejszy)."""
img = _vertical_gradient(size, BG_TOP, BG_BOT)
# mniejszy glyph dla splash (centered ~ 50%)
glyph_size = int(size * 0.5)
font = _find_font(glyph_size)
bbox = font.getbbox("g")
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
x = (size - text_w) // 2 - bbox[0]
y = (size - text_h) // 2 - bbox[1]
glow_layer = Image.new("RGBA", (size, size), TRANSPARENT)
gdraw = ImageDraw.Draw(glow_layer)
gdraw.text((x, y), "g", font=font, fill=(*ACCENT_GLOW[:3], 110))
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(size * 0.05))
img.alpha_composite(glow_layer)
draw = ImageDraw.Draw(img)
draw.text((x, y), "g", font=font, fill=ACCENT)
return img
def main() -> None:
icon = make_icon(1024)
icon.save(OUT / "icon.png", "PNG")
print(f"wrote {OUT / 'icon.png'}")
adaptive = make_adaptive_foreground(1024)
adaptive.save(OUT / "adaptive-icon.png", "PNG")
print(f"wrote {OUT / 'adaptive-icon.png'}")
splash = make_splash(1024)
splash.save(OUT / "splash.png", "PNG")
print(f"wrote {OUT / 'splash.png'}")
favicon = icon.resize((48, 48), Image.LANCZOS)
favicon.save(OUT / "favicon.png", "PNG")
print(f"wrote {OUT / 'favicon.png'}")
if __name__ == "__main__":
main()