diff --git a/tools/icons.py b/tools/icons.py index ebbcaa1..ea00183 100644 --- a/tools/icons.py +++ b/tools/icons.py @@ -2,41 +2,62 @@ python3 tools/icons.py -> icons/icon-512.png, icons/icon-192.png, icons/apple-touch-icon.png -Needs Pillow (pip install pillow). Drawn at 4x and downsampled so the edges are clean. +Standard library only: three rotated rectangles, 3x3 supersampled, written as RGB PNG. """ import math -import sys +import struct +import zlib from pathlib import Path -try: - from PIL import Image, ImageDraw -except ImportError: # pragma: no cover - sys.exit("pip install pillow") - ROOT = Path(__file__).resolve().parent.parent -GROUND, INK = (11, 11, 14, 255), (237, 240, 240, 255) +GROUND, INK = (11, 11, 14), (237, 240, 240) +SS = 3 -def asterisk(size: int) -> Image.Image: - ss = 4 - W = size * ss - im = Image.new("RGBA", (W, W), GROUND) - d = ImageDraw.Draw(im) - c = W / 2 - half, width = 150 / 512 * W, 58 / 512 * W # arm half-length and stroke width, as in icon.svg - for k in range(3): - a = math.radians(90 + 60 * k) - dx, dy = math.cos(a), math.sin(a) - nx, ny = -dy * width / 2, dx * width / 2 - p1 = (c - dx * half, c - dy * half) - p2 = (c + dx * half, c + dy * half) - d.polygon([(p1[0] + nx, p1[1] + ny), (p2[0] + nx, p2[1] + ny), (p2[0] - nx, p2[1] - ny), (p1[0] - nx, p1[1] - ny)], fill=INK) - return im.resize((size, size), Image.LANCZOS) +def arms(size): + """The three arms as (centre, unit direction, half-length, half-width) in pixel units.""" + c = size / 2 + half, hw = 150 / 512 * size, 29 / 512 * size + return [((c, c), (math.cos(math.radians(90 + 60 * k)), math.sin(math.radians(90 + 60 * k))), half, hw) for k in range(3)] + + +def inside(x, y, shapes): + for (cx, cy), (dx, dy), half, hw in shapes: + px, py = x - cx, y - cy + along, across = px * dx + py * dy, -px * dy + py * dx + if abs(along) <= half and abs(across) <= hw: + return True + return False + + +def asterisk(size): + shapes = arms(size) + rows = [] + n = SS * SS + for y in range(size): + row = bytearray([0]) # PNG filter type 0 + for x in range(size): + hit = 0 + for sy in range(SS): + for sx in range(SS): + if inside(x + (sx + 0.5) / SS, y + (sy + 0.5) / SS, shapes): + hit += 1 + t = hit / n + row.extend(int(round(g + (i - g) * t)) for g, i in zip(GROUND, INK)) + rows.append(bytes(row)) + return b"".join(rows) + + +def png(size, raw): + def chunk(kind, data): + return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF) + ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) + return b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"") if __name__ == "__main__": out = ROOT / "icons" out.mkdir(exist_ok=True) for name, size in (("icon-512.png", 512), ("icon-192.png", 192), ("apple-touch-icon.png", 180)): - asterisk(size).convert("RGB").save(out / name, optimize=True) + (out / name).write_bytes(png(size, asterisk(size))) print("built: icons/" + name)