// 从 icons/dist//icon-*.png 生成 macOS 的 .icns。 // 用途:Windows 上没有 iconutil,但 icns 自 10.7 起支持直接内嵌 PNG, // 因此容器可以手工拼出来,不必依赖 macOS 工具链。 const fs = require('fs'); const path = require('path'); const DIST = path.join(__dirname, '..', 'dist'); // OSType -> 该槽位要求的像素尺寸。ic11/ic12/ic13/ic14 是 @2x 变体, // 像素数等于逻辑尺寸的两倍,复用同一批 PNG 即可。 const SLOTS = [ ['icp4', 16], ['icp5', 32], ['ic11', 32], ['ic12', 64], ['ic07', 128], ['ic13', 256], ['ic08', 256], ['ic14', 512], ['ic09', 512], ['ic10', 1024] ]; const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); function pngSize(buf) { if (!buf.subarray(0, 8).equals(PNG_SIGNATURE)) throw new Error('不是 PNG 文件'); if (buf.readUInt32BE(8) !== 13 || buf.toString('latin1', 12, 16) !== 'IHDR') { throw new Error('PNG 缺少 IHDR'); } return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) }; } function build(theme) { const chunks = []; for (const [osType, size] of SLOTS) { const file = path.join(DIST, theme, `icon-${size}.png`); const png = fs.readFileSync(file); const dim = pngSize(png); if (dim.width !== size || dim.height !== size) { throw new Error(`${file} 期望 ${size}x${size},实际 ${dim.width}x${dim.height}`); } const header = Buffer.alloc(8); header.write(osType, 0, 4, 'latin1'); header.writeUInt32BE(png.length + 8, 4); chunks.push(header, png); } const body = Buffer.concat(chunks); const header = Buffer.alloc(8); header.write('icns', 0, 4, 'latin1'); header.writeUInt32BE(body.length + 8, 4); const out = path.join(DIST, `book-ai-${theme}.icns`); fs.writeFileSync(out, Buffer.concat([header, body])); return out; } for (const theme of ['dark', 'light']) { const out = build(theme); console.log(`${path.basename(out)} ${(fs.statSync(out).size / 1024).toFixed(1)} KB`); }