/** * # 脚本 gen-favicons.mjs * 说明:以各站点 public/icon.svg(已有差异化品牌设计)为唯一设计源,生成: * 1) sites//src/app/favicon.ico —— 16/32/48 三档 PNG-in-ICO(Next App Router 约定,自动注入 ) * 2) sites//public/icon-192.png / icon-512.png —— 覆盖原五站同图的通用 PWA 图标(manifest.webmanifest 引用) * 用法:bun packages/core/scripts/gen-favicons.mjs */ import { readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import sharp from "sharp"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "sites"); const SITES = ["ai-stack", "launchai", "the-ai-ledger", "devai-tools", "findai"]; const ICO_SIZES = [16, 32, 48]; const PNG_SIZES = [192, 512]; /** # 函数 renderSvg * 说明:SVG 按目标尺寸矢量栅格化为 PNG(按 viewBox 64 推导 density 保证清晰度)。 * 参数:svgBuffer 源 SVG;size 目标像素 * 返回:PNG Buffer */ async function renderSvg(svgBuffer, size) { const density = Math.round((72 * size) / 64); return sharp(svgBuffer, { density }).resize(size, size).png().toBuffer(); } /** # 函数 packIco * 说明:把多档 PNG 打包为 ICO 容器(Vista+ 支持 PNG 压缩条目)。 * 参数:pngs [{ size, buf }] * 返回:ICO Buffer */ function packIco(pngs) { const header = Buffer.alloc(6); header.writeUInt16LE(0, 0); // 保留位 header.writeUInt16LE(1, 2); // 类型:图标 header.writeUInt16LE(pngs.length, 4); const entries = Buffer.alloc(16 * pngs.length); let offset = 6 + entries.length; const blobs = []; pngs.forEach((p, i) => { const e = entries.subarray(i * 16, (i + 1) * 16); e[0] = p.size >= 256 ? 0 : p.size; // 宽(0 表示 256) e[1] = p.size >= 256 ? 0 : p.size; // 高 e[2] = 0; // 调色板色数 e[3] = 0; // 保留位 e.writeUInt16LE(1, 4); // 色彩平面 e.writeUInt16LE(32, 6); // 位深 e.writeUInt32LE(p.buf.length, 8); e.writeUInt32LE(offset, 12); offset += p.buf.length; blobs.push(p.buf); }); return Buffer.concat([header, entries, ...blobs]); } for (const site of SITES) { const svg = readFileSync(join(ROOT, site, "public", "icon.svg")); const ico = packIco( await Promise.all( ICO_SIZES.map(async (size) => ({ size, buf: await renderSvg(svg, size) })) ) ); writeFileSync(join(ROOT, site, "src", "app", "favicon.ico"), ico); for (const size of PNG_SIZES) { const buf = await renderSvg(svg, size); writeFileSync(join(ROOT, site, "public", `icon-${size}.png`), buf); } console.log(`✓ ${site}: favicon.ico(16/32/48) + icon-192.png + icon-512.png`); }