// Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Project: chat.spboucher.ai // Dependency-free PNG icon generator: rasterizes the ▮▯▮ instrument mark // (three rounded bars, signal teal on ink) at the sizes the PWA needs. import { deflateSync } from "node:zlib"; import { writeFileSync, mkdirSync } from "node:fs"; const INK = [0x0e, 0x11, 0x16, 255]; const TEAL = [0x3f, 0xb6, 0xa8, 255]; function crc32(buf) { let table = crc32.table; if (!table) { table = crc32.table = new Int32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; table[n] = c; } } let c = 0xffffffff; for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } function chunk(type, data) { const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const body = Buffer.concat([Buffer.from(type, "ascii"), data]); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(body)); return Buffer.concat([len, body, crc]); } function encodePng(size, pixels) { const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(size, 0); ihdr.writeUInt32BE(size, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // RGBA const raw = Buffer.alloc((size * 4 + 1) * size); for (let y = 0; y < size; y++) { raw[y * (size * 4 + 1)] = 0; // filter none pixels.copy(raw, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4); } return Buffer.concat([ sig, chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", Buffer.alloc(0)), ]); } function inRoundedRect(x, y, rx, ry, w, h, r) { if (x < rx || x >= rx + w || y < ry || y >= ry + h) return false; const cx = Math.max(rx + r, Math.min(x, rx + w - r)); const cy = Math.max(ry + r, Math.min(y, ry + h - r)); return (x - cx) ** 2 + (y - cy) ** 2 <= r * r || (x >= rx + r && x < rx + w - r) || (y >= ry + r && y < ry + h - r); } function drawIcon(size, { maskable = false } = {}) { const px = Buffer.alloc(size * size * 4); const s = size / 512; const bgRadius = maskable ? 0 : 112 * s; const bars = [ { x: 138, filled: true }, { x: 228, filled: false }, { x: 318, filled: true }, ]; const stroke = 14 * s; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { let color = [0, 0, 0, 0]; if (maskable || inRoundedRect(x, y, 0, 0, size, size, bgRadius)) color = INK; for (const b of bars) { const bx = b.x * s; const by = 156 * s; const bw = 56 * s; const bh = 200 * s; const br = 12 * s; if (inRoundedRect(x, y, bx, by, bw, bh, br)) { if (b.filled || !inRoundedRect(x, y, bx + stroke, by + stroke, bw - 2 * stroke, bh - 2 * stroke, Math.max(br - stroke, 0))) { color = TEAL; } } } const i = (y * size + x) * 4; px[i] = color[0]; px[i + 1] = color[1]; px[i + 2] = color[2]; px[i + 3] = color[3]; } } return encodePng(size, px); } mkdirSync("public/icons", { recursive: true }); writeFileSync("public/icons/icon-192.png", drawIcon(192)); writeFileSync("public/icons/icon-512.png", drawIcon(512)); writeFileSync("public/icons/icon-maskable-512.png", drawIcon(512, { maskable: true })); writeFileSync("public/icons/apple-touch-icon.png", drawIcon(180, { maskable: true })); console.log("icons written");