"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; });