#!/usr/bin/env python3
"""The Android app's icon, from one description, in every form it needs.
tools/icons.py
writes, under droid/:
app/src/main/res/drawable/ic_foreground.xml
the adaptive icon's foreground, as a vector (Android 8+ launchers)
app/src/main/res/mipmap-{mdpi..xxxhdpi}/ic_launcher.png
the same icon as PNGs, 48 to 192 px. F-Droid takes an app's icon
from its APK and cannot render adaptive icons, so without these
the app is listed with a blank one.
fastlane/metadata/android/en-US/images/icon.png
512 px, where F-Droid's metadata tooling looks for a store icon
The icon is two ring families with their centres slightly apart: a
small moire, in the Her theme's accent on its base.
"""
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parent.parent
RES = ROOT / "droid/app/src/main/res"
BACKGROUND = "#661e1e"
FOREGROUND = "#fdf2c6"
# In the adaptive icon's 108-unit canvas, whose central 72 units are
# what a launcher shows, and whose central 66 are safe from any mask.
CENTRES = [(52.5, 54.0), (55.5, 54.0)]
RADII = range(4, 31, 4)
STROKE = 1.4
DENSITIES = {"mdpi": 48, "hdpi": 72, "xhdpi": 96, "xxhdpi": 144, "xxxhdpi": 192}
def vector():
paths = []
for cx, cy in CENTRES:
for r in RADII:
paths.append(
" <path\n"
f' android:pathData="M{cx - r},{cy} a{r},{r} 0 1,0 {2 * r},0 a{r},{r} 0 1,0 {-2 * r},0"\n'
f' android:strokeColor="{FOREGROUND}"\n'
f' android:strokeWidth="{STROKE}"\n'
' android:fillColor="#00000000" />'
)
return (
'<?xml version="1.0" encoding="utf-8"?>\n'
"<!-- Generated by tools/icons.py; do not edit. Two ring families\n"
" slightly apart: a small moire, within the 66dp safe zone. -->\n"
'<vector xmlns:android="http://schemas.android.com/apk/res/android"\n'
' android:width="108dp"\n'
' android:height="108dp"\n'
' android:viewportWidth="108"\n'
' android:viewportHeight="108">\n' + "\n".join(paths) + "\n</vector>\n"
)
def raster(size):
"""The icon as a launcher shows it: the adaptive canvas's central 72
units, in a rounded square, drawn at 8x and downsampled so the thin
rings stay smooth at 48 px."""
ss = 8
big = size * ss
unit = big / 72 # pixels per canvas unit, over the visible 72
offset = 18 # the visible area starts 18 units in
art = Image.new("RGBA", (big, big), BACKGROUND)
draw = ImageDraw.Draw(art)
width = max(1, round(STROKE * unit))
for cx, cy in CENTRES:
for r in RADII:
x, y = (cx - offset) * unit, (cy - offset) * unit
rr = r * unit
draw.ellipse((x - rr, y - rr, x + rr, y + rr), outline=FOREGROUND, width=width)
mask = Image.new("L", (big, big), 0)
ImageDraw.Draw(mask).rounded_rectangle((0, 0, big - 1, big - 1), radius=round(big * 0.22), fill=255)
art.putalpha(mask)
return art.resize((size, size), Image.LANCZOS)
def main():
out = RES / "drawable/ic_foreground.xml"
out.write_text(vector())
print(f"wrote {out.relative_to(ROOT)}")
for density, size in DENSITIES.items():
out = RES / f"mipmap-{density}/ic_launcher.png"
out.parent.mkdir(parents=True, exist_ok=True)
raster(size).save(out, optimize=True)
print(f"wrote {out.relative_to(ROOT)} ({size}px)")
out = ROOT / "droid/fastlane/metadata/android/en-US/images/icon.png"
out.parent.mkdir(parents=True, exist_ok=True)
raster(512).save(out, optimize=True)
print(f"wrote {out.relative_to(ROOT)} (512px)")
if __name__ == "__main__":
main()
neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
3.7 KiB · text
History
8295a73