Some checks are pending
Tests & Quality Checks / Test on Python 3.11 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.10 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-2 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-2 (push) Waiting to run
Tests & Quality Checks / Build Artifacts (push) Blocked by required conditions
Tests & Quality Checks / Build Artifacts-1 (push) Blocked by required conditions
150 lines
4.1 KiB
Python
150 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate application icons for Windows (.ico) and macOS (.icns).
|
|
|
|
This script creates both platform icon formats from a single master PNG so
|
|
branding stays visually consistent across installers.
|
|
|
|
Requirements:
|
|
- Pillow (for .ico generation)
|
|
- macOS tools `sips` and `iconutil` (for .icns generation)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
ICONSET_ENTRIES: list[tuple[str, int]] = [
|
|
("icon_16x16.png", 16),
|
|
("icon_16x16@2x.png", 32),
|
|
("icon_32x32.png", 32),
|
|
("icon_32x32@2x.png", 64),
|
|
("icon_128x128.png", 128),
|
|
("icon_128x128@2x.png", 256),
|
|
("icon_256x256.png", 256),
|
|
("icon_256x256@2x.png", 512),
|
|
("icon_512x512.png", 512),
|
|
("icon_512x512@2x.png", 1024),
|
|
]
|
|
|
|
ICO_SIZES = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
|
|
|
|
|
def _run(cmd: list[str]) -> None:
|
|
subprocess.run(cmd, check=True)
|
|
|
|
|
|
def _require_tool(name: str) -> None:
|
|
if shutil.which(name):
|
|
return
|
|
raise RuntimeError(
|
|
f"Required tool '{name}' not found in PATH. "
|
|
f"Install it and retry."
|
|
)
|
|
|
|
|
|
def generate_icns(source_png: Path, target_icns: Path) -> None:
|
|
"""Generate macOS .icns using iconutil and sips."""
|
|
_require_tool("sips")
|
|
_require_tool("iconutil")
|
|
|
|
target_icns.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="webdrop_iconset_") as tmp:
|
|
iconset_dir = Path(tmp) / "app.iconset"
|
|
iconset_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for filename, size in ICONSET_ENTRIES:
|
|
out_file = iconset_dir / filename
|
|
_run(
|
|
[
|
|
"sips",
|
|
"-z",
|
|
str(size),
|
|
str(size),
|
|
str(source_png),
|
|
"--out",
|
|
str(out_file),
|
|
]
|
|
)
|
|
|
|
_run(["iconutil", "-c", "icns", str(iconset_dir), "-o", str(target_icns)])
|
|
|
|
|
|
def generate_ico(source_png: Path, target_ico: Path) -> None:
|
|
"""Generate Windows .ico using Pillow."""
|
|
try:
|
|
from PIL import Image
|
|
except ImportError as exc:
|
|
raise RuntimeError(
|
|
"Pillow is required for .ico generation. "
|
|
"Install it with: pip install Pillow"
|
|
) from exc
|
|
|
|
target_ico.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with Image.open(source_png) as img:
|
|
rgba = img.convert("RGBA")
|
|
rgba.save(target_ico, format="ICO", sizes=ICO_SIZES)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate .ico and .icns icons from a single PNG source"
|
|
)
|
|
parser.add_argument(
|
|
"--source",
|
|
default="resources/icons/app.png",
|
|
help="Master source PNG (default: resources/icons/app.png)",
|
|
)
|
|
parser.add_argument(
|
|
"--icon-ico",
|
|
default="resources/icons/app.ico",
|
|
help="Target .ico path (default: resources/icons/app.ico)",
|
|
)
|
|
parser.add_argument(
|
|
"--icon-icns",
|
|
default="resources/icons/app.icns",
|
|
help="Target .icns path (default: resources/icons/app.icns)",
|
|
)
|
|
parser.add_argument(
|
|
"--only",
|
|
choices=["all", "ico", "icns"],
|
|
default="all",
|
|
help="Generate all icons or only one format",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
|
|
source = Path(args.source).resolve()
|
|
icon_ico = Path(args.icon_ico).resolve()
|
|
icon_icns = Path(args.icon_icns).resolve()
|
|
|
|
if not source.exists():
|
|
print(f"ERROR: Source icon not found: {source}")
|
|
return 1
|
|
|
|
try:
|
|
if args.only in {"all", "ico"}:
|
|
generate_ico(source, icon_ico)
|
|
print(f"OK: Generated ICO: {icon_ico}")
|
|
|
|
if args.only in {"all", "icns"}:
|
|
generate_icns(source, icon_icns)
|
|
print(f"OK: Generated ICNS: {icon_icns}")
|
|
|
|
return 0
|
|
except (subprocess.CalledProcessError, RuntimeError) as exc:
|
|
print(f"ERROR: {exc}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|