129 lines
3.7 KiB
JavaScript
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;
|
|
});
|