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>
179 lines
7.6 KiB
Python
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()
|