igneum/brand/icons/make-icons.py
igneum-labs 3baf399615 Packaging: Igneum Miner for Mac and Windows, one-tap installs with the coin on every file and window
Mac: bundle renamed Igneum Miner.app (network.igneum.miner, 0.1.0, LSMinimumSystemVersion 11.0, igneum.icns),
Terminal title Igneum Miner, seed line in the first-run text, branded DMG (volume icon, background, icon slots via
dmgbuild), dist/Igneum-Miner-0.1.0.dmg at 16.2 MB, tested synced against the live node on 27400/27401.
Windows: packaging/windows with windres .rc files and embed-resources.sh (relink commands for the next cross-build),
Igneum-Miner.iss (Program Files, Start Menu group, firewall rule, uninstall stops the processes, licence, seed line),
BUILD-INSTALLER.bat and build-installer.ps1 for the project lead's PC (winget Inno Setup 6, rcedit fallback for the exe icon).
Icons: brand/icons/make-icons.py makes igneum.icns, igneum.ico, the Inno wizard art and the DMG background.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 22:26:57 +00:00

179 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Builds the Igneum Miner icon and installer art from brand/igneum-coin-1024.png (3 October 2026).
Outputs, all in brand/icons/:
igneum.icns Mac app and DMG volume icon (iconutil from the full 10-file iconset)
igneum.ico Windows icon, 16 24 32 48 64 128 256 px, uncompressed BMP entries
igneum-256.png the coin cut out on transparency (the source of both)
inno-wizard-164x314.bmp Inno Setup WizardImageFile (left side of the welcome and finish pages)
inno-wizard-small-55x58.bmp Inno Setup WizardSmallImageFile (top right of the other pages)
inno-banner-150x57.bmp header banner (NSIS size; kept for the Windows README and any header-style page)
dmg-background.png 660x400 DMG window background
dmg-background@2x.png the same at 1320x800 for Retina; dmg-background.tiff holds both when tiffutil is present
Needs Pillow and, on a Mac, iconutil and tiffutil (both ship with macOS).
python3 brand/icons/make-icons.py
"""
import os, subprocess, sys, shutil, tempfile
from PIL import Image, ImageDraw, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__))
BRAND = os.path.dirname(HERE)
COIN = os.path.join(BRAND, 'igneum-coin-1024.png')
FONT = os.path.join(BRAND, 'Unbounded.ttf')
OBSIDIAN = (12, 12, 14)
EMBER = (242, 84, 27)
MOLTEN = (255, 179, 92)
BONE = (244, 241, 236)
RADIUS = 480.0 # the ember ring's outer edge, measured on the 1024 px coin
CENTRE = (511.5, 511.5)
def font(size, weight='SemiBold'):
f = ImageFont.truetype(FONT, size)
try:
f.set_variation_by_name(weight)
except Exception:
pass
return f
def coin_rgba():
"""The coin on transparency: a 4x supersampled circular mask at the ring's outer edge."""
im = Image.open(COIN).convert('RGBA')
big = Image.new('L', (4096, 4096), 0)
d = ImageDraw.Draw(big)
cx, cy, r = CENTRE[0] * 4 + 2, CENTRE[1] * 4 + 2, (RADIUS - 0.5) * 4
d.ellipse((cx - r, cy - r, cx + r, cy + r), fill=255)
mask = big.resize((1024, 1024), Image.LANCZOS)
im.putalpha(mask)
return im
def fit(im, size):
return im.resize((size, size), Image.LANCZOS)
def make_icns(coin):
tmp = tempfile.mkdtemp()
iconset = os.path.join(tmp, 'igneum.iconset')
os.mkdir(iconset)
for s in (16, 32, 128, 256, 512):
fit(coin, s).save(os.path.join(iconset, f'icon_{s}x{s}.png'))
fit(coin, s * 2).save(os.path.join(iconset, f'icon_{s}x{s}@2x.png'))
out = os.path.join(HERE, 'igneum.icns')
subprocess.run(['iconutil', '-c', 'icns', iconset, '-o', out], check=True)
shutil.rmtree(tmp)
return out
def make_ico(coin):
out = os.path.join(HERE, 'igneum.ico')
sizes = [(s, s) for s in (16, 24, 32, 48, 64, 128, 256)]
base = fit(coin, 256)
try:
base.save(out, format='ICO', sizes=sizes, bitmap_format='bmp')
except TypeError:
base.save(out, format='ICO', sizes=sizes)
return out
def wordmark(text, size, colour=BONE, weight='SemiBold'):
f = font(size, weight)
l, t, r, b = f.getbbox(text)
im = Image.new('RGBA', (r - l + 4, b - t + 4), (0, 0, 0, 0))
ImageDraw.Draw(im).text((2 - l, 2 - t), text, font=f, fill=colour)
return im
def paste_centre(dst, src, cx, cy):
dst.alpha_composite(src, (int(cx - src.width / 2), int(cy - src.height / 2)))
def bmp(im, path):
im.convert('RGB').save(path, format='BMP')
def make_inno(coin):
# 164x314: the coin above a two-line wordmark
big = Image.new('RGBA', (164, 314), OBSIDIAN + (255,))
paste_centre(big, fit(coin, 112), 82, 110)
paste_centre(big, wordmark('IGNEUM', 19, BONE, 'Bold'), 82, 205)
paste_centre(big, wordmark('MINER', 13, MOLTEN, 'Medium'), 82, 232)
bmp(big, os.path.join(HERE, 'inno-wizard-164x314.bmp'))
# 55x58: the coin alone
small = Image.new('RGBA', (55, 58), OBSIDIAN + (255,))
paste_centre(small, fit(coin, 50), 27.5, 29)
bmp(small, os.path.join(HERE, 'inno-wizard-small-55x58.bmp'))
# 150x57: coin left, wordmark right
banner = Image.new('RGBA', (150, 57), OBSIDIAN + (255,))
paste_centre(banner, fit(coin, 44), 28, 28.5)
w1 = wordmark('IGNEUM', 15, BONE, 'Bold')
w2 = wordmark('MINER', 10, MOLTEN, 'Medium')
banner.alpha_composite(w1, (56, 14))
banner.alpha_composite(w2, (57, 34))
bmp(banner, os.path.join(HERE, 'inno-banner-150x57.bmp'))
def arrow(draw, x0, x1, y, colour, width):
draw.line((x0, y, x1 - width * 2, y), fill=colour, width=width)
h = width * 3
draw.polygon([(x1, y), (x1 - h * 1.6, y - h), (x1 - h * 1.6, y + h)], fill=colour)
# Finder icon slots on the DMG (1x pixels, icon size 96): the app at APP_SLOT, the Applications link at APPS_SLOT,
# README.txt and the Stop command in the row below. build-dmg.sh uses the same numbers.
ICON = 96
APP_SLOT = (165, 130)
APPS_SLOT = (495, 130)
ROW2_Y = 330
def dashed_box(im, cx, cy, r, colour, width=3, dash=16, gap=10, radius=18):
"""A dashed rounded square: the outline drawn once, then masked by diagonal bars."""
W, H = im.size
layer = Image.new('RGBA', (W, H), (0, 0, 0, 0))
ImageDraw.Draw(layer).rounded_rectangle((cx - r, cy - r, cx + r, cy + r), radius=radius, outline=colour, width=width)
bars = Image.new('L', (W, H), 0)
bd = ImageDraw.Draw(bars)
k = -H
while k < W + H:
bd.line((k, 0, k + H, H), fill=255, width=dash)
k += dash + gap
alpha = layer.split()[3]
alpha = Image.composite(alpha, Image.new('L', (W, H), 0), bars)
layer.putalpha(alpha)
im.alpha_composite(layer)
def make_dmg_background(coin):
"""660x400 at 1x; drawn at 2x and downsampled. The coin is painted under the app slot, so the slot reads as the
product even before Finder draws the app icon on top of it."""
S = 2
W, H = 660 * S, 400 * S
im = Image.new('RGBA', (W, H), OBSIDIAN + (255,))
d = ImageDraw.Draw(im)
ax, ay = APP_SLOT[0] * S, APP_SLOT[1] * S
bx, by = APPS_SLOT[0] * S, APPS_SLOT[1] * S
glow = Image.new('RGBA', (W, H), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
for i, a in ((76 * S, 10), (66 * S, 12), (58 * S, 14)):
gd.ellipse((ax - i, ay - i, ax + i, ay + i), fill=EMBER + (a,))
im.alpha_composite(glow)
paste_centre(im, fit(coin, ICON * S), ax, ay)
dashed_box(im, bx, by, (ICON // 2 + 14) * S, MOLTEN + (190,), width=2 * S, dash=9 * S, gap=6 * S, radius=9 * S)
arrow(d, (APP_SLOT[0] + 78) * S, (APPS_SLOT[0] - 78) * S, ay, EMBER + (255,), 4 * S)
paste_centre(im, wordmark('Drag Igneum Miner to Applications', 16 * S, BONE, 'Medium'), 330 * S, 236 * S)
paste_centre(im, wordmark('Then open it from Applications. Nobody from Igneum will ever ask for your seed.', 8 * S, (150, 148, 145), 'Regular'), 330 * S, 262 * S)
im2x = im.convert('RGB')
im2x.save(os.path.join(HERE, 'dmg-background@2x.png'))
im1x = im2x.resize((660, 400), Image.LANCZOS)
im1x.save(os.path.join(HERE, 'dmg-background.png'))
tiff = os.path.join(HERE, 'dmg-background.tiff')
if shutil.which('tiffutil'):
subprocess.run(['tiffutil', '-cathidpicheck', os.path.join(HERE, 'dmg-background.png'),
os.path.join(HERE, 'dmg-background@2x.png'), '-out', tiff], check=True)
def main():
coin = coin_rgba()
fit(coin, 256).save(os.path.join(HERE, 'igneum-256.png'))
if sys.platform == 'darwin' and shutil.which('iconutil'):
print('icns:', make_icns(coin))
else:
print('icns skipped: iconutil is a macOS tool')
print('ico:', make_ico(coin))
make_inno(coin)
make_dmg_background(coin)
for f in sorted(os.listdir(HERE)):
p = os.path.join(HERE, f)
if os.path.isfile(p) and not f.endswith('.py'):
print(f'{os.path.getsize(p):>8} {f}')
if __name__ == '__main__':
main()