@@ -0,0 +1,44 @@
2+"""Renders icons/icon.png to the PNG sizes the manifest lists. Needs Pillow (python3 -m pip install pillow).
4+The source is the full-size app icon. Its transparent margin is trimmed first so the rounded square fills the
5+16 and 32 px toolbar icons; the larger sizes keep a little padding, as Chrome's extension page expects.
7+from pathlib import Path
9+from PIL import Image, ImageFilter
11+ICONS = Path(__file__).resolve().parent.parent / "icons"
12+# size -> padding in px on each side at that size
13+SIZES = {16: 0, 32: 0, 48: 2, 128: 8}
16+def trimmed(src: Image.Image) -> Image.Image:
17+ # Near-invisible specks around the shape would otherwise widen the bounding box.
18+ solid = src.getchannel("A").point(lambda a: 255 if a > 32 else 0)
19+ left, top, right, bottom = solid.getbbox()
20+ side = max(right - left, bottom - top)
21+ cx, cy = (left + right) / 2, (top + bottom) / 2
22+ box = (round(cx - side / 2), round(cy - side / 2), round(cx + side / 2), round(cy + side / 2))
26+def render(art: Image.Image, size: int, pad: int) -> Image.Image:
27+ inner = size - 2 * pad
28+ small = art.resize((inner, inner), Image.LANCZOS)
30+ small = small.filter(ImageFilter.UnsharpMask(radius=0.6, percent=60, threshold=0))
31+ out = Image.new("RGBA", (size, size), (0, 0, 0, 0))
32+ out.paste(small, (pad, pad))
37+ art = trimmed(Image.open(ICONS / "icon.png").convert("RGBA"))
38+ for size, pad in SIZES.items():
39+ render(art, size, pad).save(ICONS / f"{size}.png", optimize=True)
40+ print("icons written")
43+if __name__ == "__main__":