Files
LobsterAI/scripts/generate-app-icon.js

114 lines
3.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Generate a multi-size .ico file from a source PNG.
*
* ICO format with embedded PNGs:
* ICONDIR (6 bytes) – reserved(2) + type(2)=1 + count(2)
* ICONDIRENTRY * N (16 bytes each)
* PNG data blobs
*
* We use System.Drawing via PowerShell to resize the source image into
* several sizes, save them as temporary PNGs, then pack them into .ico
* purely with Node buffers.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const SOURCE = path.join(__dirname, '..', 'public', 'logo.png');
const OUT_DIR = path.join(__dirname, '..', 'build', 'icons', 'win');
const OUT_ICO = path.join(OUT_DIR, 'icon.ico');
const SIZES = [256, 128, 64, 48, 32, 16];
// Ensure output directory exists
fs.mkdirSync(OUT_DIR, { recursive: true });
// Step 1: Use PowerShell + System.Drawing to resize the source PNG
const tmpDir = path.join(__dirname, '..', 'build', 'icons', '_tmp');
fs.mkdirSync(tmpDir, { recursive: true });
const psScript = `
Add-Type -AssemblyName System.Drawing
$src = [System.Drawing.Image]::FromFile("${SOURCE.replace(/\\/g, '\\\\')}")
$sizes = @(${SIZES.join(',')})
foreach ($s in $sizes) {
$bmp = New-Object System.Drawing.Bitmap($s, $s)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
$g.DrawImage($src, 0, 0, $s, $s)
$g.Dispose()
$outPath = "${tmpDir.replace(/\\/g, '\\\\')}\\\\icon_$s.png"
$bmp.Save($outPath, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
}
$src.Dispose()
`;
const psFile = path.join(tmpDir, 'resize.ps1');
fs.writeFileSync(psFile, psScript, 'utf8');
execSync(`powershell -ExecutionPolicy Bypass -File "${psFile}"`, { stdio: 'inherit' });
// Step 2: Read resized PNGs and pack into ICO
const pngBuffers = SIZES.map(s => {
const p = path.join(tmpDir, `icon_${s}.png`);
return { size: s, data: fs.readFileSync(p) };
});
const count = pngBuffers.length;
const headerSize = 6;
const entrySize = 16;
const dataOffset0 = headerSize + entrySize * count;
// Calculate offsets
let currentOffset = dataOffset0;
const entries = pngBuffers.map(({ size, data }) => {
const entry = {
width: size >= 256 ? 0 : size, // 0 means 256 in ICO format
height: size >= 256 ? 0 : size,
dataSize: data.length,
offset: currentOffset,
data,
};
currentOffset += data.length;
return entry;
});
// Build ICO buffer
const totalSize = currentOffset;
const ico = Buffer.alloc(totalSize);
// ICONDIR
ico.writeUInt16LE(0, 0); // reserved
ico.writeUInt16LE(1, 2); // type = ICO
ico.writeUInt16LE(count, 4); // image count
// ICONDIRENTRY for each image
entries.forEach((e, i) => {
const off = headerSize + i * entrySize;
ico.writeUInt8(e.width, off + 0); // width
ico.writeUInt8(e.height, off + 1); // height
ico.writeUInt8(0, off + 2); // color palette
ico.writeUInt8(0, off + 3); // reserved
ico.writeUInt16LE(1, off + 4); // color planes
ico.writeUInt16LE(32, off + 6); // bits per pixel
ico.writeUInt32LE(e.dataSize, off + 8); // image data size
ico.writeUInt32LE(e.offset, off + 12); // image data offset
});
// Image data
entries.forEach(e => {
e.data.copy(ico, e.offset);
});
fs.writeFileSync(OUT_ICO, ico);
console.log(`Generated ${OUT_ICO} (${SIZES.join(', ')}px) — ${ico.length} bytes`);
// Cleanup temp files
fs.rmSync(tmpDir, { recursive: true, force: true });