"use strict"; /** * Assemble a Windows multi-resolution .ico from the icon.iconset PNGs. * * ICO format (PNG-embedded, supported by Windows Vista+): * ICONDIR (6 bytes): reserved=0, type=1, count=N * ICONDIRENTRY (16×N): w,h,colors,reserved,planes,bitcount,size,offset * * No native tooling (iconutil/sips) — pure Node, cross-buildable on macOS. */ const fs = require("node:fs"); const path = require("node:path"); const ICONSET = path.join(__dirname, "..", "assets", "icon.iconset"); const OUT = path.join(__dirname, "..", "assets", "icon.ico"); const SIZES = [16, 32, 48, 64, 128, 256]; const SOURCES = { 16: "icon_16x16.png", 32: "icon_32x32.png", 64: "icon_32x32@2x.png", 128: "icon_128x128.png", 256: "icon_256x256.png", // 48 has no direct source; reuse 64 — Windows scales acceptably. 48: "icon_32x32@2x.png", }; function pngDims(buf) { // PNG IHDR: width (4 bytes BE) at offset 16, height at 20. return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) }; } function build() { const entries = []; for (const size of SIZES) { const file = path.join(ICONSET, SOURCES[size]); if (!fs.existsSync(file)) { console.warn(`skip ${size}: missing ${SOURCES[size]}`); continue; } const buf = fs.readFileSync(file); if (buf.slice(0, 8).toString("hex") !== "89504e470d0a1a0a") throw new Error(`${file}: not a PNG`); const { width, height } = pngDims(buf); entries.push({ size, width, height, buf }); } if (entries.length === 0) throw new Error("no PNG sources found"); const count = entries.length; const headerSize = 6 + 16 * count; let offset = headerSize; const dir = Buffer.alloc(headerSize); dir.writeUInt16LE(0, 0); // reserved dir.writeUInt16LE(1, 2); // type = icon dir.writeUInt16LE(count, 4); entries.forEach((e, i) => { const base = 6 + 16 * i; dir.writeUInt8(e.size >= 256 ? 0 : e.size, base); // width (0 = 256) dir.writeUInt8(e.size >= 256 ? 0 : e.size, base + 1); // height dir.writeUInt8(0, base + 2); // colorCount (0 = ≥8bit) dir.writeUInt8(0, base + 3); // reserved dir.writeUInt16LE(1, base + 4); // planes dir.writeUInt16LE(32, base + 6); // bitCount dir.writeUInt32LE(e.buf.length, base + 8); // bytesInRes dir.writeUInt32LE(offset, base + 12); // imageOffset offset += e.buf.length; }); const out = Buffer.concat([dir, ...entries.map((e) => e.buf)]); fs.writeFileSync(OUT, out); console.log(`built ${OUT}: ${count} sizes [${entries.map((e) => e.size).join(",")}] (${out.length} bytes)`); } build();