wechat-robot-skills/skills/pptx/scripts/_icon_renderer.js

107 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const sharp = require("sharp");
const LIBRARIES = {
fa6: "react-icons/fa6",
fi: "react-icons/fi",
hi2: "react-icons/hi2",
io5: "react-icons/io5",
lu: "react-icons/lu",
md: "react-icons/md",
ri: "react-icons/ri",
tb: "react-icons/tb",
};
function fail(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}
function parseArgs(argv) {
const values = {};
for (let index = 0; index < argv.length; index += 2) {
if (!argv[index]?.startsWith("--") || argv[index + 1] === undefined) {
fail("参数必须按 --name value 成对提供");
}
values[argv[index].slice(2)] = argv[index + 1];
}
if (!values.spec || !values.output) {
fail("缺少 --spec 或 --output");
}
return values;
}
function color(value, label) {
if (typeof value !== "string" || !/^[0-9A-Fa-f]{6}$/.test(value)) {
throw new Error(`${label} 必须是不带 # 的 6 位十六进制颜色`);
}
return `#${value.toUpperCase()}`;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const specPath = path.resolve(args.spec);
const outputPath = path.resolve(args.output);
if (path.extname(outputPath).toLowerCase() !== ".png") {
fail("output 必须使用 .png 扩展名");
}
let spec;
try {
spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
} catch (error) {
fail(`读取 spec 失败:${error.message}`);
}
try {
const libraryPath = LIBRARIES[spec.library];
if (!libraryPath) {
throw new Error(`不支持的图标库:${spec.library}`);
}
if (typeof spec.name !== "string" || !/^[A-Za-z][A-Za-z0-9]*$/.test(spec.name)) {
throw new Error("name 必须是合法的 React Icons 导出名称");
}
const size = spec.size ?? 256;
if (!Number.isInteger(size) || size < 32 || size > 2048) {
throw new Error("size 必须是 32 到 2048 的整数");
}
const moduleExports = require(libraryPath);
const Icon = moduleExports[spec.name];
if (typeof Icon !== "function") {
throw new Error(`${spec.library} 中不存在图标 ${spec.name}`);
}
const foreground = color(spec.color || "111827", "color");
const markup = ReactDOMServer.renderToStaticMarkup(
React.createElement(Icon, {
color: foreground,
size,
title: typeof spec.title === "string" ? spec.title : undefined,
}),
);
let pipeline = sharp(Buffer.from(markup))
.resize(size, size, { fit: "contain" });
if (spec.background) {
pipeline = pipeline.flatten({ background: color(spec.background, "background") });
}
await pipeline.png().toFile(outputPath);
const metadata = await sharp(outputPath).metadata();
process.stdout.write(
`${JSON.stringify({
library: spec.library,
name: spec.name,
width: metadata.width,
height: metadata.height,
})}\n`,
);
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
}
main();