from __future__ import annotations
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
ROOT = Path(__file__).resolve().parent
FONT_CANDIDATES = (
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
"C:\\Windows\\Fonts\\arialbd.ttf",
)
FONT = next((p for p in FONT_CANDIDATES if Path(p).is_file()), None)
if FONT is None:
raise SystemExit("no bold sans font found for icon render")
DIM = (91, 33, 182, 255)
BRAND = (167, 139, 250, 255)
INK = (30, 16, 53, 255)
LIGHT = (196, 181, 253, 255)
PIXEL_F3 = [
"................",
".bbbbbbbbbbbbbb.",
".bbbbbbbbbbbbbb.",
".bbffffbbffffbb.",
".bbfbbbbbbbbfbb.",
".bbfbbbbbbbbfbb.",
".bbffffbbffffbb.",
".bbfbbbbbbbbfbb.",
".bbfbbbbbbbbfbb.",
".bbfbbbbbffffbb.",
".bbbbbbbbbbbbbb.",
".bbbbbbbbbbbbbb.",
".bbbbbbbbbbbbbb.",
".bbbbbbbbbbbbbb.",
".bbbbbbbbbbbbbb.",
"................",
]
def lerp(a: int, b: int, t: float) -> int:
return int(round(a + (b - a) * t))
def multiply_l(a: Image.Image, b: Image.Image) -> Image.Image:
return Image.frombytes(
"L",
a.size,
bytes(int(x * y / 255) for x, y in zip(a.tobytes(), b.tobytes())),
)
def gradient_bg(size: int) -> Image.Image:
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
px = img.load()
denom = max(size - 1, 1)
for y in range(size):
for x in range(size):
t = (x + y) / (2 * denom)
px[x, y] = (
lerp(DIM[0], BRAND[0], t),
lerp(DIM[1], BRAND[1], t),
lerp(DIM[2], BRAND[2], t),
255,
)
return img
def rounded_mask(size: int, radius: int) -> Image.Image:
mask = Image.new("L", (size, size), 0)
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, size - 1, size - 1), radius=radius, fill=255
)
return mask
def sheen(size: int) -> Image.Image:
overlay = Image.new("RGBA", (size, size), (0, 0, 0, 0))
ImageDraw.Draw(overlay).ellipse(
(-size * 0.15, -size * 0.55, size * 1.05, size * 0.55),
fill=(255, 255, 255, 48),
)
return overlay.filter(ImageFilter.GaussianBlur(radius=max(size // 24, 1)))
def draw_f3(size: int) -> Image.Image:
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
font = ImageFont.truetype(FONT, int(size * 0.56))
bbox = draw.textbbox((0, 0), "F3", font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
x = (size - tw) / 2 - bbox[0]
y = (size - th) / 2 - bbox[1] + size * 0.02
draw.text((x, y), "F3", font=font, fill=INK)
return img
def clip_to_mask(img: Image.Image, mask: Image.Image) -> Image.Image:
r, g, b, a = img.split()
return Image.merge("RGBA", (r, g, b, multiply_l(a, mask)))
def render_large(size: int) -> Image.Image:
radius = max(int(round(size * 0.22)), 3)
mask = rounded_mask(size, radius)
base = gradient_bg(size)
base.putalpha(mask)
out = Image.alpha_composite(base, clip_to_mask(sheen(size), mask))
return Image.alpha_composite(out, clip_to_mask(draw_f3(size), mask))
def render_16() -> Image.Image:
if any(len(row) != 16 for row in PIXEL_F3) or len(PIXEL_F3) != 16:
raise SystemExit("PIXEL_F3 must be 16x16")
img = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
px = img.load()
for y, row in enumerate(PIXEL_F3):
for x, ch in enumerate(row):
t = (x + y) / 30
if ch == "f":
px[x, y] = LIGHT
elif ch == "b":
px[x, y] = (
lerp(DIM[0], BRAND[0], t),
lerp(DIM[1], BRAND[1], t),
lerp(DIM[2], BRAND[2], t),
255,
)
for x, y in ((1, 1), (14, 1), (1, 14), (14, 14)):
px[x, y] = (0, 0, 0, 0)
return img
def save_rgba(img: Image.Image, path: Path) -> None:
path.write_bytes(img.convert("RGBA").tobytes())
def main() -> None:
sizes = (16, 32, 48, 64, 128, 256)
for size in sizes:
img = render_16() if size == 16 else render_large(size)
img.save(ROOT / f"icon-{size}.png")
if size in (16, 32, 64):
dest = ROOT / f"icon-{size}.rgba"
save_rgba(img, dest)
expected = size * size * 4
got = dest.stat().st_size
if got != expected:
raise SystemExit(f"{size} rgba size {got} != {expected}")
print("wrote", ", ".join(f"icon-{s}.png" for s in sizes))
if __name__ == "__main__":
main()