76 lines
3.1 KiB
Python
76 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Export the original app.svg offline: python3 tools/export-app-icons.py.
|
|
|
|
Requires Python 3 + Pillow (also used by local-extract). This deliberately renders
|
|
only this source's flat rect/polygon vocabulary; unsupported SVG fails loudly.
|
|
No fonts, game art, browser, network or platform image service is involved.
|
|
"""
|
|
import argparse
|
|
from pathlib import Path
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SOURCE = ROOT / "public/icons/app.svg"
|
|
MASK_SCALE = 0.875
|
|
|
|
|
|
def render(size, maskable=False):
|
|
svg = ET.parse(SOURCE).getroot()
|
|
if svg.attrib != {"width": "512", "height": "512", "viewBox": "0 0 512 512"}:
|
|
raise ValueError("Expected the 512-square SVG master")
|
|
# Eight samples per output pixel, then area averaging; no ringing beyond the mark.
|
|
scale = size * 8 / 512
|
|
image = Image.new("RGB", (size * 8, size * 8))
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
def paint(element, inset=1):
|
|
tag = element.tag.rsplit("}", 1)[-1]
|
|
attrs = element.attrib
|
|
if tag in ("title", "desc"):
|
|
if attrs or len(element):
|
|
raise ValueError("Unsupported metadata")
|
|
return
|
|
if tag == "g" and attrs == {"id": "mark"}:
|
|
for child in element:
|
|
paint(child, MASK_SCALE if maskable else 1)
|
|
return
|
|
if tag == "rect" and set(attrs) <= {"x", "y", "width", "height", "fill"}:
|
|
x, y = float(attrs.get("x", 0)), float(attrs.get("y", 0))
|
|
w, h = float(attrs["width"]), float(attrs["height"])
|
|
points = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
|
|
elif tag == "polygon" and set(attrs) == {"fill", "points"}:
|
|
points = [tuple(map(float, p.split(","))) for p in attrs["points"].split()]
|
|
else:
|
|
raise ValueError(f"Unsupported SVG element/attributes: {tag} {attrs}")
|
|
# Pillow addresses pixel centres; shift the vector edges by half a sample.
|
|
points = [((256 + (x - 256) * inset) * scale - 0.5,
|
|
(256 + (y - 256) * inset) * scale - 0.5) for x, y in points]
|
|
draw.polygon(points, fill=attrs["fill"])
|
|
|
|
for element in svg:
|
|
paint(element)
|
|
return image.resize((size, size), Image.Resampling.BOX)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--out", type=Path, default=ROOT / "public/icons")
|
|
args = parser.parse_args()
|
|
args.out.mkdir(parents=True, exist_ok=True)
|
|
for size in (192, 512):
|
|
for maskable in (False, True):
|
|
name = f"app-{'maskable-' if maskable else ''}{size}.png"
|
|
render(size, maskable).save(args.out / name)
|
|
for size in (16, 32, 48):
|
|
render(size).save(args.out / f"favicon-{size}.png")
|
|
render(180).save(args.out / "apple-touch-icon.png")
|
|
# BMP-backed frames also work in older ICO readers; PNG favicons ship separately.
|
|
render(48).save(args.out / "favicon.ico", sizes=[(16, 16), (32, 32), (48, 48)], bitmap_format="bmp")
|
|
print(f"Exported 9 icon files to {args.out}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|