godcrm/scripts/gen_appicon.py
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

165 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""
GOD Frame iOS AppIcon generator (ADR-0027 Phase 8.6).
Native pixel art at 64x64 (Tor sprite on hex-accented bg #2D3142),
upscaled NEAREST 16x to 1024x1024 master, then Lanczos-resized to every
filename referenced by AppIcon.appiconset/Contents.json.
Outputs:
- god_frame/assets/icon/icon_64.png (native pixel art)
- god_frame/assets/icon/icon_1024.png (master, RGB no alpha)
- god_frame/ios/Runner/Assets.xcassets/AppIcon.appiconset/<size>.png (all)
"""
from PIL import Image
import json
import os
import re
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SPRITE_PATH = os.path.join(ROOT, "docs/16neo/sprites/pes-tor-happy.png")
ICON_DIR = os.path.join(ROOT, "god_frame/assets/icon")
APPICON_DIR = os.path.join(
ROOT, "god_frame/ios/Runner/Assets.xcassets/AppIcon.appiconset"
)
CONTENTS_JSON = os.path.join(APPICON_DIR, "Contents.json")
os.makedirs(ICON_DIR, exist_ok=True)
# Palette — matches splash where it overlaps; bg per architect brief #2D3142.
P = {
"bg": (45, 49, 66), # #2D3142 — base
"bg_light": (66, 71, 92), # lighter inner panel
"bg_dark": (28, 32, 46), # outer rim
"accent": (192, 132, 252), # #c084fc HLTRN purple
"accent_dim": (107, 107, 141), # dim purple
"gold": (212, 160, 23), # #d4a017
"white": (245, 245, 250),
"lightsky": (168, 218, 220),
}
W, H = 64, 64
img = Image.new("RGB", (W, H), P["bg"])
px = img.load()
def setpx(x, y, c):
if 0 <= x < W and 0 <= y < H:
px[x, y] = c
def hline(x1, x2, y, c):
for x in range(x1, x2 + 1):
setpx(x, y, c)
def vline(x, y1, y2, c):
for y in range(y1, y2 + 1):
setpx(x, y, c)
# ---- Outer rim (dark) ----
for y in range(H):
for x in range(W):
if x < 1 or x > W - 2 or y < 1 or y > H - 2:
setpx(x, y, P["bg_dark"])
# ---- Inner panel (slight lighter rectangle, hex-cornered) ----
# 4px corner cuts to echo hex theme
for y in range(2, H - 2):
for x in range(2, W - 2):
# cut the 4 corners diagonally for a hex-stop feel
d_tl = (2 - x) + (2 - y)
d_tr = (x - (W - 3)) + (2 - y)
d_bl = (2 - x) + (y - (H - 3))
d_br = (x - (W - 3)) + (y - (H - 3))
if max(d_tl, d_tr, d_bl, d_br) > 1:
continue
setpx(x, y, P["bg_light"])
# ---- Hex frame: 4 short purple ticks at the panel corners ----
def hex_corner(cx, cy, dx, dy, color):
# 3-pixel L mark pointing inward
setpx(cx, cy, color)
setpx(cx + dx, cy, color)
setpx(cx, cy + dy, color)
hex_corner(3, 3, 2, 2, P["accent"])
hex_corner(W - 4, 3, -2, 2, P["accent"])
hex_corner(3, H - 4, 2, -2, P["accent"])
hex_corner(W - 4, H - 4, -2, -2, P["accent"])
# ---- Mid-purple shadow halo behind Tor (soft dot grid) ----
cx, cy = 32, 32
for y in range(cy - 14, cy + 14):
for x in range(cx - 14, cx + 14):
if 0 <= x < W and 0 <= y < H:
dx = x - cx
dy = y - cy
r2 = dx * dx + dy * dy
if 80 < r2 < 170 and (x + y) % 2 == 0:
setpx(x, y, P["accent_dim"])
# ---- Place Tor sprite cropped to bbox, fitted to ~48x44 ----
tor = Image.open(SPRITE_PATH).convert("RGBA")
bbox = tor.getbbox() # (4, 0, 60, 52) for happy sprite
tor_crop = tor.crop(bbox)
# Target ~46 wide, preserve aspect
target_w = 46
ratio = target_w / tor_crop.width
target_h = max(1, round(tor_crop.height * ratio))
tor_small = tor_crop.resize((target_w, target_h), Image.NEAREST)
tx = (W - target_w) // 2
ty = (H - target_h) // 2 + 1 # nudge down 1px to balance
tor_px = tor_small.load()
for yy in range(target_h):
for xx in range(target_w):
r, g, b, a = tor_px[xx, yy]
if a > 30:
setpx(tx + xx, ty + yy, (r, g, b))
# ---- Tiny gold sparkle in top-right (echoes splash HLTRN gold) ----
sx, sy = W - 10, 9
setpx(sx, sy, P["gold"])
setpx(sx - 1, sy, P["accent_dim"])
setpx(sx + 1, sy, P["accent_dim"])
setpx(sx, sy - 1, P["accent_dim"])
setpx(sx, sy + 1, P["accent_dim"])
# ---- Save native + master ----
native_path = os.path.join(ICON_DIR, "icon_64.png")
img.save(native_path)
print(f"native saved: {native_path} size={img.size}")
master = img.resize((1024, 1024), Image.NEAREST)
master_path = os.path.join(ICON_DIR, "icon_1024.png")
master.save(master_path)
print(f"master saved: {master_path} size={master.size}")
# ---- Resize master into every filename referenced by Contents.json ----
with open(CONTENTS_JSON) as f:
contents = json.load(f)
filenames = sorted({entry["filename"] for entry in contents["images"]})
print(f"\nWriting {len(filenames)} icon files into {APPICON_DIR}\n")
for name in filenames:
m = re.match(r"^(\d+)\.png$", name)
if not m:
print(f" skip: {name} (cannot parse size)")
continue
size = int(m.group(1))
if size == 1024:
out = master # already 1024 NEAREST upscale — pixel-perfect
else:
# Lanczos for downscale (smooth at small sizes — needed at 16/20/29)
out = master.resize((size, size), Image.LANCZOS)
# Force RGB (no alpha — App Store rejects icons with alpha channel)
if out.mode != "RGB":
out = out.convert("RGB")
out_path = os.path.join(APPICON_DIR, name)
out.save(out_path)
print(f" {name:>10} {size}x{size}")
print(f"\n{len(filenames)} icon files written.")