deliver.mjs 6.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. // 出正式文件(2026-10-14 用户选定 C 圆形徽章,三端统一):
  2. // ../交付/<端>/favicon.svg(字已转路径,不依赖字体)、favicon-16/32/48.png、favicon.ico(16/32/48 三档 BMP)、apple-touch-icon.png 180、icon-192.png、icon-512.png
  3. // 字形来自 glyphs.json(glyphs.py 用 fontTools 从 PingFang SC Semibold / Inter SemiBold 抽出,1000 UPM,y 向下)。
  4. // 用法:node deliver.mjs
  5. import fs from "fs";
  6. import path from "path";
  7. import { fileURLToPath } from "url";
  8. import { chromium } from "playwright";
  9. import { PNG } from "pngjs";
  10. import { SYS, BLUE } from "./icons.mjs";
  11. const here = path.dirname(fileURLToPath(import.meta.url));
  12. const out = path.resolve(here, "../交付");
  13. const G = JSON.parse(fs.readFileSync(path.join(here, "glyphs.json"), "utf8"));
  14. // 和选型页 C 一致:圆底渐变、r=26 的 55% 白环、白字;汉字 36 号、拉丁 42 号。16px 专用版去掉白环、字放大(汉字 42、拉丁 48)
  15. function svgC(sys, small = false) {
  16. const g = G[sys.key];
  17. const size = small ? (sys.cjk ? 42 : 48) : (sys.cjk ? 36 : 42);
  18. const s = size / 1000;
  19. const [x0, y0, x1, y1] = g.bbox;
  20. const tx = 32 - s * (x0 + x1) / 2, ty = 32 - s * (y0 + y1) / 2; // 墨迹包围盒居中
  21. const ring = small ? "" : `<circle cx="32" cy="32" r="26" fill="none" stroke="#fff" stroke-opacity=".55" stroke-width="1.5"/>`;
  22. 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>`;
  23. }
  24. // ICO:每档一个 32 位 BMP(BITMAPINFOHEADER + 自下而上 BGRA + 全 0 的 AND 掩码)
  25. function ico(pngBuffers) {
  26. const imgs = pngBuffers.map((b) => PNG.sync.read(b));
  27. const entries = imgs.map((im) => {
  28. const { width: w, height: h, data } = im;
  29. const maskRow = Math.ceil(w / 32) * 4;
  30. const body = Buffer.alloc(40 + w * h * 4 + maskRow * h);
  31. body.writeUInt32LE(40, 0); body.writeInt32LE(w, 4); body.writeInt32LE(h * 2, 8);
  32. body.writeUInt16LE(1, 12); body.writeUInt16LE(32, 14); body.writeUInt32LE(0, 16); body.writeUInt32LE(w * h * 4 + maskRow * h, 20);
  33. let o = 40;
  34. for (let y = h - 1; y >= 0; y--) for (let x = 0; x < w; x++) {
  35. const i = (y * w + x) * 4;
  36. body[o++] = data[i + 2]; body[o++] = data[i + 1]; body[o++] = data[i]; body[o++] = data[i + 3];
  37. }
  38. return { w, h, body };
  39. });
  40. const headLen = 6 + 16 * entries.length;
  41. const head = Buffer.alloc(headLen);
  42. head.writeUInt16LE(0, 0); head.writeUInt16LE(1, 2); head.writeUInt16LE(entries.length, 4);
  43. let off = headLen;
  44. entries.forEach((e, i) => {
  45. const p = 6 + 16 * i;
  46. 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;
  47. head.writeUInt16LE(1, p + 4); head.writeUInt16LE(32, p + 6); head.writeUInt32LE(e.body.length, p + 8); head.writeUInt32LE(off, p + 12);
  48. off += e.body.length;
  49. });
  50. return Buffer.concat([head, ...entries.map((e) => e.body)]);
  51. }
  52. const browser = await chromium.launch();
  53. const page = await browser.newPage();
  54. async function raster(svg, n) {
  55. await page.setViewportSize({ width: n, height: n });
  56. 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}">`);
  57. return page.screenshot({ omitBackground: true, clip: { x: 0, y: 0, width: n, height: n } });
  58. }
  59. for (const sys of SYS) {
  60. const dir = path.join(out, sys.key); fs.mkdirSync(dir, { recursive: true });
  61. const svg = svgC(sys), svgSmall = svgC(sys, true);
  62. fs.writeFileSync(path.join(dir, "favicon.svg"), svg + "\n");
  63. const p16 = await raster(svgSmall, 16), p32 = await raster(svg, 32), p48 = await raster(svg, 48);
  64. fs.writeFileSync(path.join(dir, "favicon-16.png"), p16);
  65. fs.writeFileSync(path.join(dir, "favicon-32.png"), p32);
  66. fs.writeFileSync(path.join(dir, "favicon-48.png"), p48);
  67. fs.writeFileSync(path.join(dir, "favicon.ico"), ico([p16, p32, p48]));
  68. fs.writeFileSync(path.join(dir, "apple-touch-icon.png"), await raster(svg, 180));
  69. fs.writeFileSync(path.join(dir, "icon-192.png"), await raster(svg, 192));
  70. fs.writeFileSync(path.join(dir, "icon-512.png"), await raster(svg, 512));
  71. console.log("✓", sys.key, fs.readdirSync(dir).join(" "));
  72. }
  73. // 核对图:左 = 选型页里的 C(文字渲染),右 = 正式文件(路径),64px 放大 3 倍并排;再把 16px 放大 8 倍
  74. const chooser = (k) => fs.readFileSync(path.resolve(here, "../svg", `C-${k}.svg`), "utf8");
  75. 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">`;
  76. 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">`;
  77. 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>`;
  78. for (const sys of SYS) {
  79. const dir = path.join(out, sys.key);
  80. 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>`;
  81. }
  82. h += "</table>";
  83. await page.setViewportSize({ width: 1100, height: 760 });
  84. await page.setContent(h);
  85. await page.screenshot({ path: path.join(out, "核对.png"), fullPage: true });
  86. await browser.close();
  87. console.log("✓ 核对图", path.join(out, "核对.png"));