import { createCanvas, loadImage } from "canvas"; import { existsSync, mkdirSync, rmSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import pngToIco from "png-to-ico"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const MAC_ICONSET_ENTRIES = [ { name: "icon_16x16.png", size: 16 }, { name: "icon_16x16@2x.png", size: 32 }, { name: "icon_32x32.png", size: 32 }, { name: "icon_32x32@2x.png", size: 64 }, { name: "icon_128x128.png", size: 128 }, { name: "icon_128x128@2x.png", size: 256 }, { name: "icon_256x256.png", size: 256 }, { name: "icon_256x256@2x.png", size: 512 }, { name: "icon_512x512.png", size: 512 }, { name: "icon_512x512@2x.png", size: 1024 }, ]; const WIN_ICO_SIZES = [16, 32, 48, 256]; const LINUX_ICON_SIZE = 256; function resolveSourcePath(arg) { if (!arg) { return path.join(rootDir, "assets/farmcontrolhosticon.png"); } const candidate = path.isAbsolute(arg) ? arg : path.resolve(rootDir, arg); return candidate; } async function resizePng(sourceImage, size) { const canvas = createCanvas(size, size); const context = canvas.getContext("2d"); context.drawImage(sourceImage, 0, 0, size, size); return canvas.toBuffer("image/png"); } async function writePng(filePath, sourceImage, size) { const png = await resizePng(sourceImage, size); await Bun.write(filePath, png); return png; } function parseArgs(argv) { const sourceArg = argv.find((arg) => !arg.startsWith("-")); const outDirArgIndex = argv.indexOf("--out-dir"); const outDir = outDirArgIndex >= 0 ? argv[outDirArgIndex + 1] : path.join(rootDir, "assets"); return { sourcePath: resolveSourcePath(sourceArg), outDir: path.isAbsolute(outDir) ? outDir : path.resolve(rootDir, outDir), }; } async function main() { const { sourcePath, outDir } = parseArgs(process.argv.slice(2)); if (!existsSync(sourcePath)) { console.error(`prepare-app-icons: source image not found: ${sourcePath}`); process.exit(1); } const sourceImage = await loadImage(sourcePath); if (sourceImage.width < 256 || sourceImage.height < 256) { console.warn( `prepare-app-icons: source image is ${sourceImage.width}x${sourceImage.height}; Electrobun recommends at least 256x256`, ); } mkdirSync(outDir, { recursive: true }); const iconsetDir = path.join(outDir, "icon.iconset"); rmSync(iconsetDir, { recursive: true, force: true }); mkdirSync(iconsetDir, { recursive: true }); for (const entry of MAC_ICONSET_ENTRIES) { const iconPath = path.join(iconsetDir, entry.name); await writePng(iconPath, sourceImage, entry.size); } const icoPngs = []; for (const size of WIN_ICO_SIZES) { icoPngs.push(await resizePng(sourceImage, size)); } const icoPath = path.join(outDir, "icon.ico"); await Bun.write(icoPath, await pngToIco(icoPngs)); const linuxIconPath = path.join(outDir, "icon.png"); await writePng(linuxIconPath, sourceImage, LINUX_ICON_SIZE); console.log(`prepare-app-icons: wrote ${iconsetDir}`); console.log(`prepare-app-icons: wrote ${icoPath}`); console.log(`prepare-app-icons: wrote ${linuxIconPath}`); } main().catch((error) => { console.error(`prepare-app-icons: ${error.message}`); process.exit(1); });