#!/usr/bin/env bun
* 图标构建:把 `ui/icon.svg` 渲染成多尺寸 Windows 图标 `src-tauri/icons/icon.ico`,
* 同时输出预览 PNG 到 `.verify/icon/icon-256.png` 供人工与视觉模型核对。
*
* 渲染走本机 Edge/Chrome(复用执行器的 spawn + CDP 逻辑),全程离线、不需要额外依赖。
* 用法:bun run scripts/make-icon.ts
*/
import path from "node:path";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { loadConfig } from "../src/config.ts";
import { withBrowser } from "../src/runner/driver.ts";
const SIZES = [16, 24, 32, 48, 64, 128, 256] as const;
const root = path.resolve(import.meta.dir, "..");
const svgPath = path.join(root, "ui", "icon.svg");
const icoPath = path.join(root, "src-tauri", "icons", "icon.ico");
const previewPath = path.join(root, ".verify", "icon", "icon-256.png");
interface Frame {
size: number;
png: Uint8Array;
}
function buildIco(frames: Frame[]): Uint8Array {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(frames.length, 4);
const directory = Buffer.alloc(frames.length * 16);
let offset = header.length + directory.length;
frames.forEach((frame, index) => {
const entry = index * 16;
directory.writeUInt8(frame.size >= 256 ? 0 : frame.size, entry + 0);
directory.writeUInt8(frame.size >= 256 ? 0 : frame.size, entry + 1);
directory.writeUInt8(0, entry + 2);
directory.writeUInt8(0, entry + 3);
directory.writeUInt16LE(1, entry + 4);
directory.writeUInt16LE(32, entry + 6);
directory.writeUInt32LE(frame.png.length, entry + 8);
directory.writeUInt32LE(offset, entry + 12);
offset += frame.png.length;
});
return Buffer.concat([header, directory, ...frames.map((frame) => Buffer.from(frame.png))]);
}
const svg = await readFile(svgPath, "utf8");
const cfg = await loadConfig({ cwd: root });
const frames = await withBrowser(cfg.runner, async (page, context) => {
const session = await context.newCDPSession(page);
await session.send("Emulation.setDefaultBackgroundColorOverride", { color: { r: 0, g: 0, b: 0, a: 0 } });
const collected: Frame[] = [];
for (const size of SIZES) {
await session.send("Emulation.setDeviceMetricsOverride", {
width: size,
height: size,
deviceScaleFactor: 1,
mobile: false,
});
await page.setContent(
`<!doctype html><html><head><meta charset="utf-8"><style>
html,body{margin:0;padding:0;background:transparent;width:${size}px;height:${size}px;overflow:hidden}
svg{display:block;width:${size}px;height:${size}px}
</style></head><body>${svg.replace(/width="512" height="512"/, `width="${size}" height="${size}"`)}</body></html>`,
);
await page.waitForTimeout(150);
const png = await page.screenshot({
omitBackground: true,
type: "png",
clip: { x: 0, y: 0, width: size, height: size },
});
collected.push({ size, png: new Uint8Array(png) });
}
return collected;
});
const ico = buildIco(frames);
await mkdir(path.dirname(icoPath), { recursive: true });
await writeFile(icoPath, ico);
await mkdir(path.dirname(previewPath), { recursive: true });
const preview = frames.find((frame) => frame.size === 256);
if (preview) await writeFile(previewPath, preview.png);
console.log(`已生成 ${path.relative(root, icoPath)}(${frames.length} 帧:${SIZES.join("/")},${ico.length} 字节)`);
console.log(`预览图:${path.relative(root, previewPath)}`);