Files
podlite/tools/make_icons.py
XoséandClaude Opus 5.5 db1175d25c Podlite 1.10: amber instead of purple, to match the radio icon
The purple came from the old Audiobookshelf icon. Accent and button gradient now take the radio's
brass and dial glow (#C27A1E, light #E8A64A, buttons #D98C2B to #A35F14), and the backgrounds,
lines and greys lose their purple tint for warm ones. The blue 'new episode' dot stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:05:22 +02:00

100 lines
3.8 KiB
Python

"""Draws Podlite's PNG icons for ldpi (Galaxy Mini), mdpi and hdpi.
The launcher icon (res/drawable-*/ic_launcher.png) comes from the supplied
"Podcasts Radio" icon pack (master in art/icon-master.png) and is not generated
here. The notification icon is a white silhouette of that radio, as Android
2.3's status bar expects.
Android 2.x has no vector drawables, so the player icons are rendered at 4x and
downsampled for smooth edges.
"""
import os
import sys
from PIL import Image, ImageDraw
RES = sys.argv[1]
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
DENS = {"ldpi": 0.75, "mdpi": 1.0, "hdpi": 1.5}
AMBER = (232, 166, 74, 255) # accent_light (#E8A64A), from the radio icon's brass
WHITE = (255, 255, 255, 255)
SS = 4 # supersampling
def canvas(px):
s = px * SS
return Image.new("RGBA", (s, s), (0, 0, 0, 0)), s
def save(img, px, folder, name):
out = img.resize((px, px), Image.LANCZOS)
d = os.path.join(RES, "drawable-" + folder)
os.makedirs(d, exist_ok=True)
out.save(os.path.join(d, name + ".png"))
def status_silhouette():
"""White silhouette of the launcher icon's old radio (handle, grille, two knobs) on transparency:
Android 2.3's status bar expects a light glyph, not the colour artwork."""
S = 24 * 32 # drawn on a 24-unit grid, supersampled
u = lambda v: int(round(v * 32))
m = Image.new("L", (S, S), 0)
d = ImageDraw.Draw(m)
d.arc((u(6), u(1.2), u(18), u(11)), 200, 340, fill=255, width=u(1.8)) # carrying handle
d.rounded_rectangle((u(1.5), u(6.5), u(22.5), u(21.5)), radius=u(3.5), fill=255)
d.rounded_rectangle((u(4), u(9), u(14), u(19)), radius=u(1.2), fill=0) # grille window
for i in range(3): # grille bars
y = 10.6 + i * 2.9
d.rounded_rectangle((u(5.2), u(y), u(12.8), u(y + 1.5)), radius=u(0.7), fill=255)
for cy, r in ((11.6, 2.4), (17.0, 2.0)): # knobs as rings
d.ellipse((u(18 - r), u(cy - r), u(18 + r), u(cy + r)), fill=0)
d.ellipse((u(18 - r + 1.1), u(cy - r + 1.1), u(18 + r - 1.1), u(cy + r - 1.1)), fill=255)
out = Image.new("RGBA", (S, S), (255, 255, 255, 0))
out.putalpha(m)
return out
def status(sil, px, folder):
pad = max(1, round(px * 0.06))
inner = sil.resize((px - 2 * pad, px - 2 * pad), Image.LANCZOS)
img = Image.new("RGBA", (px, px), (0, 0, 0, 0))
img.paste(inner, (pad, pad))
d = os.path.join(RES, "drawable-" + folder)
img.save(os.path.join(d, "ic_stat.png"))
def play(px, folder, name, color):
img, s = canvas(px)
d = ImageDraw.Draw(img)
d.polygon([(s * 0.32, s * 0.2), (s * 0.32, s * 0.8), (s * 0.82, s * 0.5)], fill=color)
save(img, px, folder, name)
def pause(px, folder, name, color):
img, s = canvas(px)
d = ImageDraw.Draw(img)
d.rectangle([s * 0.26, s * 0.2, s * 0.43, s * 0.8], fill=color)
d.rectangle([s * 0.57, s * 0.2, s * 0.74, s * 0.8], fill=color)
save(img, px, folder, name)
def refresh(px, folder):
img, s = canvas(px)
d = ImageDraw.Draw(img)
w = int(s * 0.1)
box = [s * 0.2, s * 0.2, s * 0.8, s * 0.8]
d.arc(box, 30, 320, fill=WHITE, width=w)
# arrow head at the arc's end (top right)
d.polygon([(s * 0.86, s * 0.14), (s * 0.88, s * 0.48), (s * 0.56, s * 0.36)], fill=WHITE)
save(img, px, folder, "ic_refresh")
sil = status_silhouette()
for folder, k in DENS.items():
status(sil, round(25 * k), folder)
play(round(32 * k), folder, "ic_play", WHITE) # on the amber play button
pause(round(32 * k), folder, "ic_pause", WHITE)
play(round(38 * k), folder, "ic_play_small", AMBER) # mini player bar, on dark
pause(round(38 * k), folder, "ic_pause_small", AMBER)
refresh(round(30 * k), folder)
print("icons ok")