// 出正式文件(2026-10-14 用户选定 C 圆形徽章,三端统一): // ../交付/<端>/favicon.svg(字已转路径,不依赖字体)、favicon-16/32/48.png、favicon.ico(16/32/48 三档 BMP)、apple-touch-icon.png 180、icon-192.png、icon-512.png // 字形来自 glyphs.json(glyphs.py 用 fontTools 从 PingFang SC Semibold / Inter SemiBold 抽出,1000 UPM,y 向下)。 // 用法:node deliver.mjs import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { chromium } from "playwright"; import { PNG } from "pngjs"; import { SYS, BLUE } from "./icons.mjs"; const here = path.dirname(fileURLToPath(import.meta.url)); const out = path.resolve(here, "../交付"); const G = JSON.parse(fs.readFileSync(path.join(here, "glyphs.json"), "utf8")); // 和选型页 C 一致:圆底渐变、r=26 的 55% 白环、白字;汉字 36 号、拉丁 42 号。16px 专用版去掉白环、字放大(汉字 42、拉丁 48) function svgC(sys, small = false) { const g = G[sys.key]; const size = small ? (sys.cjk ? 42 : 48) : (sys.cjk ? 36 : 42); const s = size / 1000; const [x0, y0, x1, y1] = g.bbox; const tx = 32 - s * (x0 + x1) / 2, ty = 32 - s * (y0 + y1) / 2; // 墨迹包围盒居中 const ring = small ? "" : ``; return `${ring}`; } // ICO:每档一个 32 位 BMP(BITMAPINFOHEADER + 自下而上 BGRA + 全 0 的 AND 掩码) function ico(pngBuffers) { const imgs = pngBuffers.map((b) => PNG.sync.read(b)); const entries = imgs.map((im) => { const { width: w, height: h, data } = im; const maskRow = Math.ceil(w / 32) * 4; const body = Buffer.alloc(40 + w * h * 4 + maskRow * h); body.writeUInt32LE(40, 0); body.writeInt32LE(w, 4); body.writeInt32LE(h * 2, 8); body.writeUInt16LE(1, 12); body.writeUInt16LE(32, 14); body.writeUInt32LE(0, 16); body.writeUInt32LE(w * h * 4 + maskRow * h, 20); let o = 40; for (let y = h - 1; y >= 0; y--) for (let x = 0; x < w; x++) { const i = (y * w + x) * 4; body[o++] = data[i + 2]; body[o++] = data[i + 1]; body[o++] = data[i]; body[o++] = data[i + 3]; } return { w, h, body }; }); const headLen = 6 + 16 * entries.length; const head = Buffer.alloc(headLen); head.writeUInt16LE(0, 0); head.writeUInt16LE(1, 2); head.writeUInt16LE(entries.length, 4); let off = headLen; entries.forEach((e, i) => { const p = 6 + 16 * i; head[p] = e.w >= 256 ? 0 : e.w; head[p + 1] = e.h >= 256 ? 0 : e.h; head[p + 2] = 0; head[p + 3] = 0; head.writeUInt16LE(1, p + 4); head.writeUInt16LE(32, p + 6); head.writeUInt32LE(e.body.length, p + 8); head.writeUInt32LE(off, p + 12); off += e.body.length; }); return Buffer.concat([head, ...entries.map((e) => e.body)]); } const browser = await chromium.launch(); const page = await browser.newPage(); async function raster(svg, n) { await page.setViewportSize({ width: n, height: n }); await page.setContent(``); return page.screenshot({ omitBackground: true, clip: { x: 0, y: 0, width: n, height: n } }); } for (const sys of SYS) { const dir = path.join(out, sys.key); fs.mkdirSync(dir, { recursive: true }); const svg = svgC(sys), svgSmall = svgC(sys, true); fs.writeFileSync(path.join(dir, "favicon.svg"), svg + "\n"); const p16 = await raster(svgSmall, 16), p32 = await raster(svg, 32), p48 = await raster(svg, 48); fs.writeFileSync(path.join(dir, "favicon-16.png"), p16); fs.writeFileSync(path.join(dir, "favicon-32.png"), p32); fs.writeFileSync(path.join(dir, "favicon-48.png"), p48); fs.writeFileSync(path.join(dir, "favicon.ico"), ico([p16, p32, p48])); fs.writeFileSync(path.join(dir, "apple-touch-icon.png"), await raster(svg, 180)); fs.writeFileSync(path.join(dir, "icon-192.png"), await raster(svg, 192)); fs.writeFileSync(path.join(dir, "icon-512.png"), await raster(svg, 512)); console.log("✓", sys.key, fs.readdirSync(dir).join(" ")); } // 核对图:左 = 选型页里的 C(文字渲染),右 = 正式文件(路径),64px 放大 3 倍并排;再把 16px 放大 8 倍 const chooser = (k) => fs.readFileSync(path.resolve(here, "../svg", `C-${k}.svg`), "utf8"); const img = (svg, n, z) => ``; const pimg = (buf, n, z) => ``; let h = ``; for (const sys of SYS) { const dir = path.join(out, sys.key); h += ``; } h += "
选型页 C(文字)正式 favicon.svg(路径)16px 专用版ico 里的 16ico 里的 32
${sys.short}${img(chooser(sys.key), 64, 3)}${img(svgC(sys), 64, 3)}${img(svgC(sys, true), 64, 3)}${pimg(fs.readFileSync(path.join(dir, "favicon-16.png")), 16, 8)}${pimg(fs.readFileSync(path.join(dir, "favicon-32.png")), 32, 4)}
"; await page.setViewportSize({ width: 1100, height: 760 }); await page.setContent(h); await page.screenshot({ path: path.join(out, "核对.png"), fullPage: true }); await browser.close(); console.log("✓ 核对图", path.join(out, "核对.png"));