Files
threadcount-community/scripts/make-staff-icon.py
T
ThreadCount 1bc2de655a ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
2026-09-13 08:45:19 +10:00

97 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Generate the ThreadCount Staff launcher icon.
python3 scripts/make-staff-icon.py
The two apps have to be the same family and still be telling apart on a home screen, where the
only other clue is a truncated label. So this is the counter app's mark exactly — four stepped
bars in the 60/48/34/22 proportions from tc-icon.svg — inverted onto ink: the leading bar keeps
the accent, the rest go from ink to paper, and the ground goes from paper to ink.
Light tile versus dark tile is the strongest difference available at 48px, it costs no new
artwork, and it stays on-brand: the app's own splash is already ink.
The geometry is measured from the counter app's own PNGs rather than guessed, and expressed as
fractions of the canvas so every density comes out identical rather than merely similar. Bars are
drawn at 4x and downsampled, which is what keeps a 43-pixel-wide bar from landing half a pixel
off at mdpi.
"""
import os
from PIL import Image
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RES = os.path.join(ROOT, "android-staff/app/src/main/res")
INK = (32, 30, 29, 255)
PAPER = (243, 242, 242, 255)
ACCENT = (236, 48, 19, 255)
# Measured from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png (432px):
# bars start at x=129, widths 174/139/98/64, first at y=147, pitch 40, height 23.
FG = {
"x0": 129 / 432,
"widths": [174 / 432, 139 / 432, 98 / 432, 64 / 432],
"y0": 147 / 432,
"pitch": 40 / 432,
"height": 23 / 432,
}
# Measured from .../mipmap-xxxhdpi/ic_launcher.png (192px): the legacy tile crops tighter, so the
# mark sits larger in frame than it does inside the adaptive icon's safe zone.
LEGACY = {
"x0": 38 / 192,
"widths": [116 / 192, 93 / 192, 66 / 192, 43 / 192],
"y0": 47 / 192,
"pitch": 27 / 192,
"height": 21 / 192,
}
SS = 4 # supersample factor
def draw(size, geom, background):
"""The mark at `size` px. `background` None gives a transparent adaptive foreground."""
n = size * SS
im = Image.new("RGBA", (n, n), background or (0, 0, 0, 0))
px = Image.new("RGBA", (1, 1)) # placeholder so the linter sees px used below
del px
for i, wfrac in enumerate(geom["widths"]):
x0 = round(geom["x0"] * n)
y0 = round((geom["y0"] + i * geom["pitch"]) * n)
w = round(wfrac * n)
h = round(geom["height"] * n)
# The leading bar carries the accent; the rest are paper, because the ground is now ink.
colour = ACCENT if i == 0 else PAPER
im.paste(colour, (x0, y0, x0 + w, y0 + h))
return im.resize((size, size), Image.LANCZOS)
DENSITIES = {"mdpi": (48, 108), "hdpi": (72, 162), "xhdpi": (96, 216), "xxhdpi": (144, 324), "xxxhdpi": (192, 432)}
def main():
if not os.path.isdir(RES):
raise SystemExit(f"no android-staff res dir at {RES}")
for density, (legacy_px, fg_px) in DENSITIES.items():
d = os.path.join(RES, f"mipmap-{density}")
os.makedirs(d, exist_ok=True)
draw(fg_px, FG, None).save(os.path.join(d, "ic_launcher_foreground.png"))
tile = draw(legacy_px, LEGACY, INK)
tile.save(os.path.join(d, "ic_launcher.png"))
# The launcher applies its own mask, so the round variant is the same tile — which is what
# the counter app ships too.
tile.save(os.path.join(d, "ic_launcher_round.png"))
print(f" {density}: foreground {fg_px}px, launcher {legacy_px}px")
bg = os.path.join(RES, "values/ic_launcher_background.xml")
with open(bg, "w", encoding="utf-8") as f:
f.write('<?xml version="1.0" encoding="utf-8"?>\n<resources>\n'
' <!-- Ink, where the counter app is paper. This is what tells the two apps\n'
' apart on a home screen. -->\n'
' <color name="ic_launcher_background">#201E1D</color>\n</resources>\n')
print(f" background -> #201E1D")
if __name__ == "__main__":
main()