Files
ElyOffice/tests/scripts/icons.sh
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

88 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Each app: macOS squircle, shapeless Linux and Windows icons.
set -euo pipefail
root=$(cd "$(dirname "$0")/../.." && pwd)
icons=$root/assets/icons
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
[ "$(uname)" = Darwin ] || {
echo "SKIP icons: macOS only (AppKit renders the masters)"
exit 0
}
python3 -c 'import PIL' 2>/dev/null || {
echo "SKIP icons: no Pillow"
exit 0
}
swiftc -O -o "$work/icon" "$root/packaging/icons/icon.swift"
# The master build.sh uses at `px`; small cut below 48.
master() { # slug, variant, px
local small=$icons/$1-small$2.svg
if [ "$3" -lt 48 ] && [ -f "$small" ]; then echo "$small"; else echo "$icons/$1$2.svg"; fi
}
count=0
while IFS=$'\t' read -r slug _; do
for file in "$slug.svg" "$slug-macos.svg" "$slug.icns" "$slug.png" "$slug.ico"; do
[ -s "$icons/$file" ] || fail "no $icons/$file; run packaging/icons/build.sh"
done
for made in icns png ico; do
for source in "$icons/$slug".svg "$icons/$slug"-*.svg; do
[ "$source" -nt "$icons/$slug.$made" ] && fail "$slug.$made is older than $(basename "$source"); run packaging/icons/build.sh"
done
done
# Every shipped size, rendered afresh from its master.
mkdir "$work/$slug"
for px in 16 24 32 48 64 128 256 512 1024; do
"$work/icon" "$px" "$(master "$slug" "" "$px")" "$work/$slug/plain-$px.png"
"$work/icon" "$px" "$(master "$slug" -macos "$px")" "$work/$slug/macos-$px.png"
done
iconutil -c iconset -o "$work/$slug.iconset" "$icons/$slug.icns"
python3 - "$icons" "$slug" "$work" <<'EOF' || fail "$slug: the rendered icons are wrong"
import os, sys
from PIL import Image
icons, slug, work = sys.argv[1:]
def raw(image):
# Premultiplied: colour under zero alpha is not drawn.
data = image.convert("RGBA").tobytes()
out = bytearray(data)
for i in range(0, len(data), 4):
for c in range(3):
out[i + c] = data[i + c] * data[i + 3] // 255
return out
def same(made, px, variant, what, most=1.0):
want = raw(Image.open(f"{work}/{slug}/{variant}-{px}.png"))
got = raw(made)
assert len(got) == len(want), f"{what}: {made.size}, not {px}"
off = sum(abs(a - b) for a, b in zip(got, want)) / len(got)
assert off < most, f"{what} is not its master (off {off:.2f})"
# Shapeless: the mark alone, no container behind it.
mark = Image.open(f"{icons}/{slug}.png")
same(mark, 512, "plain", "png")
assert mark.convert("RGBA").getpixel((0, 0))[3] == 0, "a container behind the mark"
ico = Image.open(f"{icons}/{slug}.ico")
assert sorted(ico.info["sizes"]) == [(s, s) for s in (16, 24, 32, 48, 64, 128, 256)], ico.info["sizes"]
for size in ico.info["sizes"]:
layer = ico.ico.getimage(size)
same(layer, size[0], "plain", f"ico {size[0]}")
corners = [(0, 0), (size[0] - 1, 0), (0, size[0] - 1), (size[0] - 1, size[0] - 1)]
assert all(layer.convert("RGBA").getpixel(at)[3] == 0 for at in corners), f"ico {size[0]}: a container"
names = sorted(os.listdir(f"{work}/{slug}.iconset"))
assert len(names) == 10, names
for name in names:
made = Image.open(f"{work}/{slug}.iconset/{name}")
# macOS stores 16 and 32 px lossily.
same(made, made.width, "macos", f"icns {name}", 8.0 if name in ("icon_16x16.png", "icon_32x32.png") else 1.0)
# Squircle: Apple's margin is clear, the body's middle is solid.
big = Image.open(f"{work}/{slug}.iconset/icon_512x512@2x.png").convert("RGBA")
assert big.getpixel((40, 512))[3] == 0 and big.getpixel((150, 150))[3] == 0, "not a squircle"
assert big.getpixel((512, 110))[3] == 255, "no body"
EOF
count=$((count + 1))
done <"$root/packaging/apps.tsv"
[ "$count" -eq 8 ] || fail "checked $count apps, expected 8"
echo "icons: 8 apps, squircle on macOS, shapeless on Linux and Windows"