/** * Generate simple PNG icons for the extension. * Creates colored circle icons with a "C" letter. * Run: node generate-icons.mjs */ import { writeFileSync } from "fs"; // Minimal PNG encoder for simple icons function createPNG(size, r, g, b) { // Create raw RGBA pixel data const pixels = new Uint8Array(size * size * 4); const center = size / 2; const radius = size / 2 - 1; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const idx = (y * size + x) * 4; const dist = Math.sqrt((x - center) ** 2 + (y - center) ** 2); if (dist <= radius) { pixels[idx] = r; pixels[idx + 1] = g; pixels[idx + 2] = b; pixels[idx + 3] = 255; } else if (dist <= radius + 1) { // Anti-aliased edge const alpha = Math.max(0, Math.round((radius + 1 - dist) * 255)); pixels[idx] = r; pixels[idx + 1] = g; pixels[idx + 2] = b; pixels[idx + 3] = alpha; } } } // Encode as PNG return encodePNG(size, size, pixels); } function encodePNG(width, height, pixels) { const SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); function crc32(buf) { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) { c ^= buf[i]; for (let j = 0; j < 8; j++) { c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0); } } return (c ^ 0xffffffff) >>> 0; } function chunk(type, data) { const typeBytes = Buffer.from(type); const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const combined = Buffer.concat([typeBytes, data]); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(combined)); return Buffer.concat([len, combined, crc]); } function adler32(buf) { let a = 1, b = 0; for (let i = 0; i < buf.length; i++) { a = (a + buf[i]) % 65521; b = (b + a) % 65521; } return ((b << 16) | a) >>> 0; } // IHDR const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // RGBA ihdr[10] = 0; // compression ihdr[11] = 0; // filter ihdr[12] = 0; // interlace // IDAT - raw pixel data with filter bytes const rawData = Buffer.alloc(height * (1 + width * 4)); for (let y = 0; y < height; y++) { rawData[y * (1 + width * 4)] = 0; // no filter for (let x = 0; x < width * 4; x++) { rawData[y * (1 + width * 4) + 1 + x] = pixels[y * width * 4 + x]; } } // Deflate with store (no compression) - simple but works const blocks = []; let offset = 0; while (offset < rawData.length) { const remaining = rawData.length - offset; const blockSize = Math.min(remaining, 65535); const isLast = offset + blockSize >= rawData.length; const header = Buffer.alloc(5); header[0] = isLast ? 1 : 0; header.writeUInt16LE(blockSize, 1); header.writeUInt16LE(blockSize ^ 0xffff, 3); blocks.push(header); blocks.push(rawData.subarray(offset, offset + blockSize)); offset += blockSize; } const zlibHeader = Buffer.from([0x78, 0x01]); // deflate, no compression const adler = Buffer.alloc(4); adler.writeUInt32BE(adler32(rawData)); const compressed = Buffer.concat([zlibHeader, ...blocks, adler]); // IEND const iend = Buffer.alloc(0); return Buffer.concat([ SIGNATURE, chunk("IHDR", ihdr), chunk("IDAT", compressed), chunk("IEND", iend), ]); } const colors = { gray: [156, 163, 175], blue: [59, 130, 246], green: [34, 197, 94], red: [239, 68, 68], }; const sizes = [16, 48, 128]; for (const [name, [r, g, b]] of Object.entries(colors)) { for (const size of sizes) { const png = createPNG(size, r, g, b); const path = `src/icons/icon-${name}-${size}.png`; writeFileSync(path, png); console.log(`Generated ${path}`); } }