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 charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Vitals — workload map</title>
|
<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>
|
<style>
|
||||||
:root{
|
:root{
|
||||||
--bg:#0d1013; --panel:#12161a; --panel2:#171c21; --line:#232a31;
|
--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")
|
return self._send(f.read(), "text/html; charset=utf-8")
|
||||||
except OSError:
|
except OSError:
|
||||||
return self._send("index.html missing", "text/plain", 500)
|
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 u.path == "/api/snapshot":
|
||||||
if DEMO["data"]:
|
if DEMO["data"]:
|
||||||
snap = dict(DEMO["data"]["snapshot"])
|
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