summaryrefslogtreecommitdiff
path: root/tools/make_icon.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/make_icon.py')
-rw-r--r--tools/make_icon.py118
1 files changed, 107 insertions, 11 deletions
diff --git a/tools/make_icon.py b/tools/make_icon.py
index 6bb1cbf..f05d0c9 100644
--- a/tools/make_icon.py
+++ b/tools/make_icon.py
@@ -1,28 +1,50 @@
#!/usr/bin/env python3
+# Author: cirakg & claude ai
+# Summary: Draw the launcher icon at every size the target watches need, plus the jungle that maps devices to sizes.
"""Draw the app's launcher icon: a white padlock with three "code" dots on a
teal disc, on a transparent background.
The icon is drawn at 8x the target size and scaled down, which gives smooth
-edges at the small sizes watches use (35 to 70 px).
+edges at the small sizes watches use (38 to 70 px). Every PNG carries the
+project's Author and a Description in PNG text chunks.
-Usage: make_icon.py --size 60 --out resources/drawables/launcher_icon.png
+Two modes:
+
+ make_icon.py --size 60 --out FILE.png
+ One icon of the given size.
+
+ make_icon.py --all --devices DIR --manifest manifest.xml
+ One icon per launcher-icon size used by the products in manifest.xml
+ (read from each device's compiler.json), written to
+ resources-icons/<W>x<H>/, plus icons.jungle, which points each product
+ at its folder. The Monkey C compiler warns when it has to scale an
+ icon, and warnings fail the build, so every device needs an exact size.
"""
from __future__ import annotations
import argparse
+import json
+import xml.etree.ElementTree as ET
+from collections import defaultdict
from pathlib import Path
from PIL import Image, ImageDraw
+from PIL.PngImagePlugin import PngInfo
+AUTHOR = "cirakg & claude ai"
+DESCRIPTION = "OTP Wallet launcher icon: a padlock with a one-time code, on a teal disc."
TEAL = (14, 124, 134, 255)
WHITE = (255, 255, 255, 255)
SUPERSAMPLE = 8
+IQ_NS = "{http://www.garmin.com/xml/connectiq}"
-def draw_icon(size: int) -> Image.Image:
- """Return the icon as an RGBA image of size x size pixels."""
- s = size * SUPERSAMPLE
+def draw_icon(width: int, height: int | None = None) -> Image.Image:
+ """Return the icon as an RGBA image; non-square sizes get a centred square icon."""
+ height = width if height is None else height
+ side = min(width, height)
+ s = side * SUPERSAMPLE
img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
@@ -49,18 +71,92 @@ def draw_icon(size: int) -> Image.Image:
center_x = s * (0.38 + 0.12 * i)
d.ellipse((center_x - radius, center_y - radius, center_x + radius, center_y + radius), fill=TEAL)
- return img.resize((size, size), Image.Resampling.LANCZOS)
+ icon = img.resize((side, side), Image.Resampling.LANCZOS)
+ if (width, height) == (side, side):
+ return icon
+ canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0))
+ canvas.paste(icon, ((width - side) // 2, (height - side) // 2))
+ return canvas
+
+
+def save_png(img: Image.Image, path: Path) -> None:
+ """Save with the project's Author and Description text chunks."""
+ info = PngInfo()
+ info.add_text("Author", AUTHOR)
+ info.add_text("Description", DESCRIPTION)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ img.save(path, format="PNG", optimize=True, pnginfo=info)
+
+
+def manifest_products(manifest: Path) -> list[str]:
+ """Product IDs listed in manifest.xml."""
+ root = ET.parse(manifest).getroot()
+ return [p.attrib["id"] for p in root.iter(f"{IQ_NS}product")]
+
+
+def icon_sizes(devices: Path, products: list[str]) -> dict[tuple[int, int], list[str]]:
+ """Group products by the launcher-icon size in their compiler.json."""
+ groups: dict[tuple[int, int], list[str]] = defaultdict(list)
+ for product in products:
+ compiler = json.loads((devices / product / "compiler.json").read_text(encoding="utf-8"))
+ icon = compiler["launcherIcon"]
+ groups[(int(icon["width"]), int(icon["height"]))].append(product)
+ return dict(sorted(groups.items()))
+
+
+DRAWABLES_XML = """\
+<!--
+ Author: cirakg & claude ai
+ Summary: Launcher icon at {w}x{h} px for the devices listed in icons.jungle (generated by tools/make_icon.py).
+-->
+<drawables>
+ <bitmap id="LauncherIcon" filename="launcher_icon.png" />
+</drawables>
+"""
+
+JUNGLE_HEADER = """\
+# Author: cirakg & claude ai
+# Summary: Per-device launcher-icon resources; generated by tools/make_icon.py --all (do not edit).
+#
+# Each product gets the icon folder matching the launcher-icon size in its
+# device definition, so the compiler never has to scale (and warn).
+"""
+
+
+def write_all(devices: Path, manifest: Path, out_dir: Path, jungle: Path) -> None:
+ groups = icon_sizes(devices, manifest_products(manifest))
+ lines = [JUNGLE_HEADER]
+ for (w, h), products in groups.items():
+ folder = out_dir / f"{w}x{h}"
+ save_png(draw_icon(w, h), folder / "launcher_icon.png")
+ (folder / "drawables.xml").write_text(DRAWABLES_XML.format(w=w, h=h), encoding="utf-8")
+ lines += [f"{p}.resourcePath = $({p}.resourcePath);{folder.as_posix()}" for p in sorted(products)]
+ print(f"{w}x{h}: {len(products)} devices")
+ jungle.write_text("\n".join(lines) + "\n", encoding="utf-8")
+ print(f"wrote {jungle} and {len(groups)} icon folders under {out_dir}")
def main() -> None:
- parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
- parser.add_argument("--size", type=int, default=60, help="icon width and height in pixels")
- parser.add_argument("--out", type=Path, required=True, help="PNG file to write")
+ parser = argparse.ArgumentParser(description="Draw the OTP Wallet launcher icon(s).")
+ parser.add_argument("--size", type=int, default=60, help="single icon: width and height in pixels")
+ parser.add_argument("--out", type=Path, help="single icon: PNG file to write")
+ parser.add_argument("--all", action="store_true", help="every size used by the manifest's products")
+ parser.add_argument("--devices", type=Path, help="device definitions (~/.Garmin/ConnectIQ/Devices)")
+ parser.add_argument("--manifest", type=Path, default=Path("manifest.xml"))
+ parser.add_argument("--out-dir", type=Path, default=Path("resources-icons"))
+ parser.add_argument("--jungle", type=Path, default=Path("icons.jungle"))
args = parser.parse_args()
+
+ if args.all:
+ if args.devices is None:
+ parser.error("--all needs --devices")
+ write_all(args.devices, args.manifest, args.out_dir, args.jungle)
+ return
+ if args.out is None:
+ parser.error("give --out (single icon) or --all")
if not 16 <= args.size <= 256:
parser.error("--size must be between 16 and 256")
- args.out.parent.mkdir(parents=True, exist_ok=True)
- draw_icon(args.size).save(args.out, format="PNG", optimize=True)
+ save_png(draw_icon(args.size), args.out)
print(f"wrote {args.out} ({args.size}x{args.size})")