Files

136 lines
4.4 KiB
JavaScript
Raw Permalink Normal View History

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();