icons: one mark in, every size out, and a place to serve them
Three surfaces were showing generated identicons: the forge org avatar (which is what Discord unfurls), the GitHub org avatar, and the browser tab, because the app shipped no favicon at all. make_icons.py turns one SVG or PNG into the seven sizes plus a multi-resolution favicon.ico. The server now serves /favicon.ico and /icons/*.png from icons/ when they exist, and index.html asks for them, so dropping the real mark into icons/ is the whole integration. Co-developed-with: CLU <clu@binaryranch.com>
This commit is contained in:
parent
9cdbb43394
commit
52885cad78
3 changed files with 104 additions and 0 deletions
|
|
@ -4,6 +4,12 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Vitals — workload map</title>
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
<link rel="apple-touch-icon" href="/icons/icon-180.png">
|
||||
<meta property="og:image" content="/icons/icon-512.png">
|
||||
<meta property="og:title" content="Vitals — workload map">
|
||||
<meta property="og:description" content="Group processes by human identity, then cross the browser boundary and give every tab its own CPU number.">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0d1013; --panel:#12161a; --panel2:#171c21; --line:#232a31;
|
||||
|
|
|
|||
13
server.py
13
server.py
|
|
@ -744,6 +744,19 @@ class Handler(BaseHTTPRequestHandler):
|
|||
return self._send(f.read(), "text/html; charset=utf-8")
|
||||
except OSError:
|
||||
return self._send("index.html missing", "text/plain", 500)
|
||||
if u.path == "/favicon.ico":
|
||||
path = os.path.join(HERE, "icons", "favicon.ico")
|
||||
if os.path.exists(path):
|
||||
with open(path, "rb") as f:
|
||||
return self._send(f.read(), "image/x-icon")
|
||||
return self._send("no icon yet", "text/plain", 404)
|
||||
if u.path.startswith("/icons/"):
|
||||
name = os.path.basename(u.path) # never trust the path
|
||||
path = os.path.join(HERE, "icons", name)
|
||||
if name.endswith(".png") and os.path.exists(path):
|
||||
with open(path, "rb") as f:
|
||||
return self._send(f.read(), "image/png")
|
||||
return self._send("no icon yet", "text/plain", 404)
|
||||
if u.path == "/api/snapshot":
|
||||
if DEMO["data"]:
|
||||
snap = dict(DEMO["data"]["snapshot"])
|
||||
|
|
|
|||
85
tools/make_icons.py
Normal file
85
tools/make_icons.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
make_icons.py — turn one source mark into every icon size the surfaces need.
|
||||
|
||||
One file in, the full set out, because the alternative is discovering three
|
||||
weeks later that the forge unfurl, the GitHub avatar, and the browser tab are
|
||||
all still showing generated identicons.
|
||||
|
||||
python3 tools/make_icons.py brand/mark.svg
|
||||
python3 tools/make_icons.py brand/mark.png --out-dir icons
|
||||
|
||||
Produces, in the output directory:
|
||||
icon-16.png icon-32.png icon-48.png icon-180.png icon-256.png
|
||||
icon-512.png icon-1024.png favicon.ico (16/32/48 multi-size)
|
||||
|
||||
Where each one goes:
|
||||
1024 + 512 store listings, social cards, press kit
|
||||
512 Forgejo org avatar, GitHub org avatar
|
||||
180 apple-touch-icon (iOS home screen)
|
||||
16/32/48 favicon.ico, browser tabs, bookmarks
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
RESAMPLE = getattr(Image, "Resampling", Image).LANCZOS
|
||||
|
||||
SIZES = [16, 32, 48, 180, 256, 512, 1024]
|
||||
ICO_SIZES = [16, 32, 48, 64]
|
||||
|
||||
|
||||
def load_square(path, size=1024):
|
||||
"""Return a square RGBA image, rendering SVG at high resolution first."""
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
tmp = None
|
||||
if ext == ".svg":
|
||||
tmp = f"/tmp/.icon-src-{os.getpid()}.png"
|
||||
if shutil.which("inkscape"):
|
||||
subprocess.run(["inkscape", path, "--export-type=png", f"--export-filename={tmp}",
|
||||
f"--export-width={size}", f"--export-height={size}"],
|
||||
check=True, capture_output=True)
|
||||
else:
|
||||
subprocess.run(["convert", "-background", "none", "-density", "600", path,
|
||||
"-resize", f"{size}x{size}", tmp], check=True, capture_output=True)
|
||||
path = tmp
|
||||
img = Image.open(path).convert("RGBA")
|
||||
if tmp:
|
||||
os.unlink(tmp)
|
||||
side = max(img.size)
|
||||
canvas = Image.new("RGBA", (side, side), (0, 0, 0, 0))
|
||||
canvas.paste(img, ((side - img.width) // 2, (side - img.height) // 2), img)
|
||||
return canvas.resize((size, size), RESAMPLE)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("source", help="square-ish .svg or .png/.jpg mark")
|
||||
ap.add_argument("--out-dir", default="icons")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.source):
|
||||
sys.exit(f"no such file: {args.source}")
|
||||
|
||||
master = load_square(args.source)
|
||||
os.makedirs(args.out_dir, exist_ok=True)
|
||||
for size in SIZES:
|
||||
out = os.path.join(args.out_dir, f"icon-{size}.png")
|
||||
master.resize((size, size), RESAMPLE).save(out)
|
||||
print(f" {out}")
|
||||
|
||||
ico_path = os.path.join(args.out_dir, "favicon.ico")
|
||||
master.resize((256, 256), RESAMPLE).save(ico_path, sizes=[(s, s) for s in ICO_SIZES])
|
||||
print(f" {ico_path} ({', '.join(str(s) for s in ICO_SIZES)})")
|
||||
|
||||
print(f"\n{len(SIZES) + 1} files in {args.out_dir}/")
|
||||
print("upload icon-512.png as the org avatar on both forges; the rest ship with the app")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue