const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); const buildDir = path.join(__dirname, '..', 'build'); const pngPath = path.join(buildDir, 'icon.png'); const icoPath = path.join(buildDir, 'icon.ico'); function crc32(buffer) { let crc = 0xffffffff; for (let i = 0; i < buffer.length; i++) { crc ^= buffer[i]; for (let j = 0; j < 8; j++) { const mask = -(crc & 1); crc = (crc >>> 1) ^ (0xedb88320 & mask); } } return (crc ^ 0xffffffff) >>> 0; } function makeChunk(type, data) { const length = Buffer.alloc(4); length.writeUInt32BE(data.length, 0); const typeBuffer = Buffer.from(type, 'ascii'); const crcBuffer = Buffer.alloc(4); const crc = crc32(Buffer.concat([typeBuffer, data])); crcBuffer.writeUInt32BE(crc, 0); return Buffer.concat([length, typeBuffer, data, crcBuffer]); } function drawLine(raw, rowStride, color, x0, y0, x1, y1, thickness = 2) { const dx = Math.abs(x1 - x0); const dy = Math.abs(y1 - y0); const sx = x0 < x1 ? 1 : -1; const sy = y0 < y1 ? 1 : -1; let err = dx - dy; const setPixel = (x, y) => { if (x < 0 || y < 0 || x >= 256 || y >= 256) return; for (let ty = -Math.floor(thickness / 2); ty <= Math.floor(thickness / 2); ty++) { for (let tx = -Math.floor(thickness / 2); tx <= Math.floor(thickness / 2); tx++) { const xx = x + tx; const yy = y + ty; if (xx < 0 || yy < 0 || xx >= 256 || yy >= 256) continue; const rowStart = yy * rowStride; const idx = rowStart + 1 + xx * 4; raw[idx] = color[0]; raw[idx + 1] = color[1]; raw[idx + 2] = color[2]; raw[idx + 3] = color[3]; } } }; while (true) { setPixel(x0, y0); if (x0 === x1 && y0 === y1) break; const e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } function createIconPng(width, height) { const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const ihdrData = Buffer.alloc(13); ihdrData.writeUInt32BE(width, 0); ihdrData.writeUInt32BE(height, 4); ihdrData[8] = 8; // Bit depth ihdrData[9] = 6; // Color type RGBA const ihdrChunk = makeChunk('IHDR', ihdrData); const rowStride = width * 4 + 1; const raw = Buffer.alloc(rowStride * height); const bg = [79, 70, 229, 255]; // blue/indigo for (let y = 0; y < height; y++) { const rowStart = y * rowStride; raw[rowStart] = 0; // No filter for (let x = 0; x < width; x++) { const idx = rowStart + 1 + x * 4; raw[idx] = bg[0]; raw[idx + 1] = bg[1]; raw[idx + 2] = bg[2]; raw[idx + 3] = bg[3]; } } // White checkmark const check = [255, 255, 255, 255]; drawLine(raw, rowStride, check, Math.floor(width * 0.22), Math.floor(height * 0.55), Math.floor(width * 0.42), Math.floor(height * 0.72), 10); drawLine(raw, rowStride, check, Math.floor(width * 0.42), Math.floor(height * 0.72), Math.floor(width * 0.78), Math.floor(height * 0.32), 10); const idatData = zlib.deflateSync(raw); const idatChunk = makeChunk('IDAT', idatData); const iendChunk = makeChunk('IEND', Buffer.alloc(0)); return Buffer.concat([signature, ihdrChunk, idatChunk, iendChunk]); } function buildIcoFromPng(pngBuffer) { const header = Buffer.alloc(6); header.writeUInt16LE(0, 0); // Reserved header.writeUInt16LE(1, 2); // Type = icon header.writeUInt16LE(1, 4); // Image count const entry = Buffer.alloc(16); entry[0] = 0; // Width 256 (0 means 256 in ICO format) entry[1] = 0; // Height 256 entry[2] = 0; // Palette entry[3] = 0; // Reserved entry.writeUInt16LE(1, 4); // Color planes entry.writeUInt16LE(32, 6); // Bits per pixel entry.writeUInt32LE(pngBuffer.length, 8); // PNG byte size entry.writeUInt32LE(header.length + entry.length, 12); // Offset to PNG return Buffer.concat([header, entry, pngBuffer]); } function ensureBuildAssets() { fs.mkdirSync(buildDir, { recursive: true }); // Always regenerate to ensure latest icon style const pngBuffer = createIconPng(256, 256); fs.writeFileSync(pngPath, pngBuffer); console.log(`Wrote PNG icon at ${pngPath}`); const icoBuffer = buildIcoFromPng(pngBuffer); fs.writeFileSync(icoPath, icoBuffer); console.log(`Wrote ICO icon at ${icoPath}`); } ensureBuildAssets();