const { createCanvas, loadImage, GlobalFonts } = require("@napi-rs/canvas"); const { writeFile } = require("node:fs/promises"); const { parentPort, workerData } = require("node:worker_threads"); function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } function hexToRgba(hex, opacity) { let value = hex.replace("#", ""); if (value.length === 3) { value = value .split("") .map((char) => char + char) .join(""); } let alpha = clamp(opacity, 0, 1); if (value.length === 8) { alpha *= parseInt(value.slice(6, 8), 16) / 255; value = value.slice(0, 6); } const r = parseInt(value.slice(0, 2), 16); const g = parseInt(value.slice(2, 4), 16); const b = parseInt(value.slice(4, 6), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function buildCanvasFont(style) { const italic = style.italic ? "italic " : ""; const weight = style.bold ? "700 " : "400 "; return `${italic}${weight}${style.fontSize}px "${style.fontFamily}"`; } function drawIdText(ctx, text, imageWidth, imageHeight, style) { const x = (style.xPercent / 100) * imageWidth; const y = (style.yPercent / 100) * imageHeight; ctx.font = buildCanvasFont(style); ctx.textAlign = style.align; ctx.textBaseline = "alphabetic"; const metrics = ctx.measureText(text); const ascent = metrics.actualBoundingBoxAscent ?? style.fontSize * 0.8; const descent = metrics.actualBoundingBoxDescent ?? style.fontSize * 0.2; const baselineY = y + (ascent - descent) / 2; ctx.save(); ctx.fillStyle = style.color; if (style.shadowEnabled) { ctx.shadowColor = hexToRgba(style.shadowColor, style.shadowOpacity); ctx.shadowBlur = style.shadowBlur; ctx.shadowOffsetX = style.shadowOffsetX; ctx.shadowOffsetY = style.shadowOffsetY; } else { ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; } if (style.outlineEnabled && style.outlineWidth > 0) { ctx.strokeStyle = hexToRgba(style.outlineColor, style.outlineOpacity); ctx.lineWidth = style.outlineWidth; ctx.lineJoin = "round"; ctx.miterLimit = 2; ctx.strokeText(text, x, baselineY); ctx.shadowColor = "transparent"; } ctx.fillText(text, x, baselineY); ctx.restore(); } async function run() { const { imagePath, fonts, style, tasks } = workerData; for (const font of fonts) { GlobalFonts.registerFromPath(font.path, font.family); } const image = await loadImage(imagePath); const canvas = createCanvas(image.width, image.height); const ctx = canvas.getContext("2d"); for (const task of tasks) { ctx.drawImage(image, 0, 0); drawIdText(ctx, task.text, image.width, image.height, style); await writeFile(task.outPath, await canvas.encode("png")); parentPort?.postMessage({ type: "progress" }); } } run().catch((error) => { throw error; });