"""Every icon Inkwell ships, from one drawing.
The mark is a black inkpot with a quill standing in it, on the brand yellow
(Scribe note 5071). It is drawn ONCE below, in a 512 box, and each variant places it
with its own scale, because each platform crops differently:
- icon.svg the rounded tile: web favicon/PWA "any", desktop app icon
- icon-maskable.svg full bleed, mark inside the central 80% (PWA "maskable")
- Android foreground mark alone on transparency inside the 66dp safe circle of a
108dp adaptive canvas; the yellow is the background layer, and
the transparent mark is also what themed (monochrome) icons need
Run from the repo root after changing the drawing; needs rsvg-convert (librsvg).
Generated output is committed, like any other generated file.
python3 packaging/icons.py
"""
import subprocess
from pathlib import Path
INK = "#18181b"
GOLD = "#F5C518"
# The quill is drawn upright at the pot mouth (0,0), then rotated into place.
QUILL_AT = "translate(256 232) rotate(34)"
QUILL_INK = f'''
'''
POT_INK = f'''
'''
# The detail — the quill's shaft, two splits in its vane, the glint on the pot — is
# CUT OUT of the ink rather than painted over it in yellow. On the tile the two look
# the same; on Android's transparent foreground only a cut-out is right, because a
# themed icon draws the foreground's alpha as a silhouette, and painted detail
# would come out as solid, with the vane splits sticking out past its edge.
CUTS = '''
'''.format(at=QUILL_AT)
GLYPH = f'''
{CUTS}
{QUILL_INK}{POT_INK}
'''
# Glyph bounds, measured from a render: centre the mark, then scale per variant.
CX, CY = 262, 226
def place(scale, cy=256):
return f'translate(256 {cy}) scale({scale}) translate({-CX} {-CY})'
def svg(body, label=True):
aria = ' role="img" aria-label="Inkwell"' if label else ''
return f'\n'
def tile(scale=0.92):
return svg(f'''
{GLYPH}
''')
def maskable(scale=0.8):
return svg(f'''
{GLYPH}
''')
def foreground(scale=0.64):
# Android adaptive foreground: the mark alone on transparency, inside the 66dp
# safe circle of a 108dp canvas. The yellow lives in the background layer.
return svg(f''' {GLYPH}
''', label=False)
ROOT = Path(__file__).resolve().parent.parent
WEB = ROOT / "frontend" / "public"
DESKTOP = ROOT / "desktop" / "src-tauri"
ANDROID = ROOT / "android" / "app" / "src" / "main" / "res" / "mipmap-xxxhdpi"
def render(svg_text: str, size: int, out: Path, background: str | None = None) -> None:
cmd = ["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(out)]
if background:
cmd += ["-b", background]
subprocess.run(cmd, input=svg_text.encode(), check=True)
def main() -> None:
icon, mask, fg = tile(), maskable(), foreground()
(WEB / "icon.svg").write_text(icon)
(WEB / "icon-maskable.svg").write_text(mask)
render(icon, 32, WEB / "favicon-32.png")
render(icon, 192, WEB / "icon-192.png")
render(icon, 512, WEB / "icon-512.png")
render(mask, 512, WEB / "icon-maskable-512.png")
# iOS rounds the corners itself and shows transparency as black.
render(mask, 180, WEB / "apple-touch-icon.png")
render(icon, 1024, DESKTOP / "app-icon.png")
render(icon, 32, DESKTOP / "icons" / "32x32.png")
render(icon, 128, DESKTOP / "icons" / "128x128.png")
render(icon, 256, DESKTOP / "icons" / "128x128@2x.png")
render(icon, 512, DESKTOP / "icons" / "icon.png")
render(fg, 512, ANDROID / "ic_launcher_foreground.png")
if __name__ == "__main__":
main()