Files
lektor-ai/make_icons.py
T

97 lines
3.5 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Rasterise icon.svg into the PNG sizes ui.py loads for the window icon.
Tk 8.6 reads PNG but not SVG, so the icon ships pre-rendered in assets/. The
glyph is laid on a rounded plate in the app's own colours: light on dark keeps
it legible against both a light and a dark dock, which the bare SVG (slate grey
on nothing) is not.
Needs inkscape for the render and Pillow for the downscales. Run it after
editing icon.svg, then ./install_desktop.sh to refresh the dock's copies.
"""
import re
import shutil
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
SRC = HERE / "icon.svg"
OUT = HERE / "assets"
SIZES = (256, 128, 64, 48, 32, 16)
PLATE = 64 # plate side, in the units the composite is drawn in
GLYPH = 44 # how much of that side the glyph's longest edge covers
RADIUS = 14 # corner rounding of the plate
BG = "#2b2b2b" # ui.BAR
FG = "#ededed" # ui.FG
def glyph_box(svg):
"""Bounding box of the drawing, in viewBox units.
inkscape answers in the file's own pixel units, which are viewBox units
only when width and viewBox agree — so convert.
"""
out = subprocess.run(["inkscape", "--query-all", str(svg)],
capture_output=True, text=True, check=True).stdout
line = next(l for l in out.splitlines() if l.startswith("svg"))
x, y, w, h = (float(v) for v in line.split(",")[1:5])
text = svg.read_text()
width = float(re.search(r'\swidth="([\d.]+)"', text).group(1))
vb_w = float(re.search(r'viewBox="[\d.\-]+ [\d.\-]+ ([\d.]+)', text).group(1))
u = vb_w / width
return x * u, y * u, w * u, h * u
def main():
if not SRC.is_file():
sys.exit(f"{SRC.name} not found")
if not shutil.which("inkscape"):
sys.exit("inkscape not found — needed to rasterise the SVG")
try:
from PIL import Image
except ImportError:
sys.exit("Pillow not found — needed to downscale the render")
d = re.search(r'\sd="([^"]+)"', SRC.read_text()).group(1)
x, y, w, h = glyph_box(SRC)
# Scale the glyph to GLYPH along its longest edge, then centre what that
# leaves. The offsets cancel the bbox origin, which is not at 0,0.
s = GLYPH / max(w, h)
tx = (PLATE - w * s) / 2 - x * s
ty = (PLATE - h * s) / 2 - y * s
OUT.mkdir(exist_ok=True)
composite = OUT / "_composite.svg"
composite.write_text(
f'<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" '
f'viewBox="0 0 {PLATE} {PLATE}">\n'
f' <rect width="{PLATE}" height="{PLATE}" rx="{RADIUS}" ry="{RADIUS}" '
f'fill="{BG}"/>\n'
f' <g transform="translate({tx:.4f} {ty:.4f}) scale({s:.6f})">\n'
f' <path fill="{FG}" fill-rule="evenodd" clip-rule="evenodd" '
f'd="{d}"/>\n'
f' </g>\n</svg>\n')
master = OUT / "_master.png"
subprocess.run(["inkscape", "--export-type=png",
f"--export-filename={master}", "-w", "512", "-h", "512",
str(composite)], check=True, capture_output=True)
img = Image.open(master).convert("RGBA")
for size in SIZES:
img.resize((size, size), Image.LANCZOS).save(OUT / f"icon-{size}.png")
composite.unlink()
master.unlink()
print(f"glyph bbox {w:.2f}x{h:.2f} at ({x:.2f}, {y:.2f}) "
f"-> scale {s:.3f}, offset ({tx:.2f}, {ty:.2f})")
print("wrote " + ", ".join(f"icon-{s}.png" for s in SIZES) + f" in {OUT}")
if __name__ == "__main__":
main()