import sharp from 'sharp'; import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, '..'); const publicDir = join(root, 'public'); const iconsDir = join(publicDir, 'icons'); mkdirSync(iconsDir, { recursive: true }); const svgBuffer = readFileSync(join(publicDir, 'icon.svg')); const pngSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024]; console.log('Generating PNG icons...'); await Promise.all( pngSizes.map(size => sharp(svgBuffer) .resize(size, size) .png() .toFile(join(iconsDir, `${size}x${size}.png`)) .then(() => console.log(` ✓ icons/${size}x${size}.png`)) ) ); // icon.png (1024x1024) for Linux electron-builder await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(publicDir, 'icon.png')); console.log(' ✓ icon.png'); // icon-mac.png (512x512) for macOS await sharp(svgBuffer).resize(512, 512).png().toFile(join(publicDir, 'icon-mac.png')); console.log(' ✓ icon-mac.png'); // full-icon.png (1024x1024) used in app UI await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(publicDir, 'full-icon.png')); console.log(' ✓ full-icon.png'); // favicon.ico — embed 16, 32, 48 px layers console.log('Generating favicon.ico...'); const icoSizes = [16, 32, 48]; const icoBuffers = await Promise.all( icoSizes.map(size => sharp(svgBuffer).resize(size, size).png().toBuffer() ) ); writeFileSync(join(publicDir, 'favicon.ico'), buildIco(icoBuffers, icoSizes)); console.log(' ✓ favicon.ico'); // icon.ico — embed 16, 32, 48, 64, 128, 256 px layers console.log('Generating icon.ico...'); const winSizes = [16, 32, 48, 64, 128, 256]; const winBuffers = await Promise.all( winSizes.map(size => sharp(svgBuffer).resize(size, size).png().toBuffer() ) ); writeFileSync(join(publicDir, 'icon.ico'), buildIco(winBuffers, winSizes)); console.log(' ✓ icon.ico'); // icons/icon.ico and icons/icon.icns placeholders (stubs pointing to source) // electron-builder generates .icns; copy the 1024 PNG as icons/icon.png for reference await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(iconsDir, 'icon.ico').replace('icon.ico', '1024x1024.png')); // Copy icon.ico and icon.icns into icons/ as well import { copyFileSync } from 'fs'; copyFileSync(join(publicDir, 'icon.ico'), join(iconsDir, 'icon.ico')); console.log(' ✓ icons/icon.ico'); console.log('\nDone! Note: icon.icns requires macOS tools (iconutil). Use electron-builder on macOS to generate it.'); /** * Builds a minimal ICO file from PNG buffers. * @param {Buffer[]} pngBuffers * @param {number[]} sizes * @returns {Buffer} */ function buildIco(pngBuffers, sizes) { const count = pngBuffers.length; const headerSize = 6; const dirEntrySize = 16; const dirSize = headerSize + count * dirEntrySize; let offset = dirSize; const entries = pngBuffers.map((buf, i) => { const size = sizes[i]; const entry = { size, width: size > 255 ? 0 : size, height: size > 255 ? 0 : size, buf, offset }; offset += buf.length; return entry; }); const totalSize = offset; const ico = Buffer.alloc(totalSize); // ICO header ico.writeUInt16LE(0, 0); // reserved ico.writeUInt16LE(1, 2); // type: 1 = ICO ico.writeUInt16LE(count, 4); // image count // Directory entries entries.forEach((e, i) => { const base = headerSize + i * dirEntrySize; ico.writeUInt8(e.width, base); // width (0 = 256) ico.writeUInt8(e.height, base + 1); // height (0 = 256) ico.writeUInt8(0, base + 2); // color count ico.writeUInt8(0, base + 3); // reserved ico.writeUInt16LE(1, base + 4); // planes ico.writeUInt16LE(32, base + 6); // bit count ico.writeUInt32LE(e.buf.length, base + 8); // size of image data ico.writeUInt32LE(e.offset, base + 12); // offset of image data }); // Image data entries.forEach(e => e.buf.copy(ico, e.offset)); return ico; }