136 lines
4.4 KiB
JavaScript
136 lines
4.4 KiB
JavaScript
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();
|
||
|
|
|
||
|
|
|