Files
kickkingsadmin/workers/generate-id.cjs
T
2026-09-14 13:23:14 +00:00

129 lines
3.7 KiB
JavaScript

"use strict";
const { parentPort, workerData } = require("node:worker_threads");
const { writeFile } = require("node:fs/promises");
const { createCanvas, loadImage, GlobalFonts } = require("@napi-rs/canvas");
function parseHexColor(hex) {
const raw = String(hex || "").trim();
const m = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.exec(raw);
if (!m) return { r: 0, g: 0, b: 0, a: 1 };
const h = m[1];
if (h.length === 3) {
return {
r: parseInt(h[0] + h[0], 16),
g: parseInt(h[1] + h[1], 16),
b: parseInt(h[2] + h[2], 16),
a: 1,
};
}
if (h.length === 6) {
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
a: 1,
};
}
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
a: parseInt(h.slice(6, 8), 16) / 255,
};
}
function hexToRgba(hex, opacity) {
const c = parseHexColor(hex);
const a = Math.max(0, Math.min(1, c.a * opacity));
return `rgba(${c.r}, ${c.g}, ${c.b}, ${a})`;
}
function fontString(style) {
const italic = style.italic ? "italic " : "";
const weight = style.bold ? 700 : 400;
return `${italic}${weight} ${style.fontSize}px "${style.fontFamily}"`;
}
function applyShadow(ctx, style) {
if (!style.shadowEnabled) {
clearShadow(ctx);
return;
}
ctx.shadowColor = hexToRgba(style.shadowColor, style.shadowOpacity);
ctx.shadowBlur = style.shadowBlur;
ctx.shadowOffsetX = style.shadowOffsetX;
ctx.shadowOffsetY = style.shadowOffsetY;
}
function clearShadow(ctx) {
ctx.shadowColor = "rgba(0,0,0,0)";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
function drawIdText(ctx, text, canvasWidth, canvasHeight, style) {
ctx.font = fontString(style);
ctx.textAlign = style.align;
ctx.textBaseline = "alphabetic";
const metrics = ctx.measureText(text);
const ascent =
Number.isFinite(metrics.actualBoundingBoxAscent) &&
metrics.actualBoundingBoxAscent > 0
? metrics.actualBoundingBoxAscent
: style.fontSize * 0.8;
const descent =
Number.isFinite(metrics.actualBoundingBoxDescent) &&
metrics.actualBoundingBoxDescent > 0
? metrics.actualBoundingBoxDescent
: style.fontSize * 0.2;
const x = (style.xPercent / 100) * canvasWidth;
const y = (style.yPercent / 100) * canvasHeight;
const baselineY = y + (ascent - descent) / 2;
applyShadow(ctx, style);
if (style.outlineEnabled && style.outlineWidth > 0) {
ctx.lineJoin = "round";
ctx.miterLimit = 2;
ctx.lineWidth = style.outlineWidth;
ctx.strokeStyle = hexToRgba(style.outlineColor, style.outlineOpacity);
ctx.strokeText(text, x, baselineY);
}
clearShadow(ctx);
if (!style.outlineEnabled && style.shadowEnabled) {
applyShadow(ctx, style);
}
ctx.fillStyle = style.color;
ctx.fillText(text, x, baselineY);
clearShadow(ctx);
}
async function run() {
const { imagePath, fonts, style, tasks } = workerData;
for (const font of fonts || []) {
GlobalFonts.registerFromPath(font.path, font.family);
}
const img = await loadImage(imagePath);
const canvas = createCanvas(img.width, img.height);
const ctx = canvas.getContext("2d");
for (const task of tasks) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
drawIdText(ctx, task.text, canvas.width, canvas.height, style);
const png = await canvas.encode("png");
await writeFile(task.outPath, png);
parentPort.postMessage({ type: "progress" });
}
}
run().catch((err) => {
const message = err instanceof Error ? err.message : String(err);
try {
parentPort.postMessage({ type: "error", message });
} catch {
// ignore
}
process.exitCode = 1;
});