| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- // 出正式文件(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 ? "" : `<circle cx="32" cy="32" r="26" fill="none" stroke="#fff" stroke-opacity=".55" stroke-width="1.5"/>`;
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${BLUE[0]}"/><stop offset="1" stop-color="${BLUE[1]}"/></linearGradient></defs><circle cx="32" cy="32" r="32" fill="url(#g)"/>${ring}<path fill="#fff" transform="translate(${tx.toFixed(2)} ${ty.toFixed(2)}) scale(${s.toFixed(4)})" d="${g.d}"/></svg>`;
- }
- // 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(`<style>html,body{margin:0;background:transparent}img{display:block}</style><img src="data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}" width="${n}" height="${n}">`);
- 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) => `<img src="data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}" width="${n * z}" height="${n * z}" style="image-rendering:${z > 1 && n <= 32 ? "pixelated" : "auto"};display:block">`;
- const pimg = (buf, n, z) => `<img src="data:image/png;base64,${buf.toString("base64")}" width="${n * z}" height="${n * z}" style="image-rendering:pixelated;display:block">`;
- let h = `<style>body{margin:16px;font:12px sans-serif;background:#DEE1E6}td{padding:6px 10px;text-align:center}</style><table><tr><th></th><th>选型页 C(文字)</th><th>正式 favicon.svg(路径)</th><th>16px 专用版</th><th>ico 里的 16</th><th>ico 里的 32</th></tr>`;
- for (const sys of SYS) {
- const dir = path.join(out, sys.key);
- h += `<tr><td>${sys.short}</td><td>${img(chooser(sys.key), 64, 3)}</td><td>${img(svgC(sys), 64, 3)}</td><td>${img(svgC(sys, true), 64, 3)}</td><td>${pimg(fs.readFileSync(path.join(dir, "favicon-16.png")), 16, 8)}</td><td>${pimg(fs.readFileSync(path.join(dir, "favicon-32.png")), 32, 4)}</td></tr>`;
- }
- h += "</table>";
- 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"));
|