import "server-only"; import { execFile } from "node:child_process"; import { promises as fs } from "node:fs"; import { homedir } from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import type { AgentPsdFont } from "@/lib/agents/psd-font-config"; const execFileAsync = promisify(execFile); const ensuredFontCache = new Map< string, Promise<{ fontFamily: string; fontFullName?: string; fontPostscriptName?: string; fontStyle?: string; installedPath?: string; }> >(); function sanitizeFileSegment(value: string, fallback: string) { const normalized = String(value || "") .trim() .replace(/\s+/g, "-") .replace(/[^a-zA-Z0-9._-]/g, "-") .replace(/-+/g, "-") .replace(/^-+|-+$/g, "") .toLowerCase(); return normalized || fallback; } function getFontExtension(font: AgentPsdFont) { const fileName = String(font.fileName || "").trim().toLowerCase(); if (fileName.endsWith(".ttf")) return ".ttf"; if (fileName.endsWith(".otf")) return ".otf"; if (fileName.endsWith(".woff2")) return ".woff2"; if (fileName.endsWith(".woff")) return ".woff"; const contentType = String(font.contentType || "").trim().toLowerCase(); if (contentType.includes("woff2")) return ".woff2"; if (contentType.includes("woff")) return ".woff"; if (contentType.includes("otf")) return ".otf"; return ".ttf"; } function getRuntimeFontDirectory() { if (process.platform === "darwin") { return path.join(homedir(), "Library", "Fonts", "ai-chatbot-psd"); } if (process.platform === "linux") { return path.join(homedir(), ".local", "share", "fonts", "ai-chatbot-psd"); } return path.join(process.cwd(), ".next", "cache", "psd-fonts"); } function parseFontconfigToken(stdout: string) { const firstLine = String(stdout || "") .split("\n") .map((line) => line.trim()) .find(Boolean); if (!firstLine) { return ""; } return firstLine .split(",") .map((token) => token.trim()) .find(Boolean) || ""; } function parseFontScanMetadata(stdout: string) { const [fullName = "", family = "", style = "", postscriptName = ""] = String(stdout || "") .split("\n") .map((line) => line.trim()) .find(Boolean) ?.split("|") .map((item) => item.trim()) || []; return { fontFullName: fullName || undefined, fontFamily: family || fullName || "", fontStyle: style || undefined, fontPostscriptName: postscriptName || undefined, }; } async function ensureWritableFontFile(targetPath: string, fontBuffer: Buffer) { const existingFile = await fs.stat(targetPath).catch(() => null); if (existingFile && existingFile.size === fontBuffer.byteLength) { return; } await fs.writeFile(targetPath, fontBuffer); } async function detectInstalledFontFamily(targetPath: string, fallbackFamily: string) { try { const { stdout } = await execFileAsync( "fc-scan", ["--format=%{family}\n", targetPath], { timeout: 15_000 } ); return parseFontconfigToken(stdout) || fallbackFamily; } catch { return fallbackFamily; } } async function detectInstalledFontMetadata(targetPath: string, fallbackFamily: string) { try { const { stdout } = await execFileAsync( "fc-scan", ["--format=%{fullname[0]}|%{family[0]}|%{style[0]}|%{postscriptname}\n", targetPath], { timeout: 15_000 } ); const metadata = parseFontScanMetadata(stdout); return { ...metadata, fontFamily: metadata.fontFamily || fallbackFamily, }; } catch { return { fontFamily: await detectInstalledFontFamily(targetPath, fallbackFamily), fontFullName: undefined, fontStyle: undefined, fontPostscriptName: undefined, }; } } async function refreshFontCache(fontDirectory: string) { try { await execFileAsync("fc-cache", ["-f", fontDirectory], { timeout: 20_000 }); } catch { // ignore font cache refresh errors and let caller fall back to the original family } } export async function ensurePsdFontAvailableForRender(params: { font: AgentPsdFont; fontBuffer: Buffer; preferredFamily?: string; }) { const { font, fontBuffer, preferredFamily } = params; const cacheKey = `${font.id}:${font.updatedAt}:${font.fileUrl}`; if (!ensuredFontCache.has(cacheKey)) { ensuredFontCache.set( cacheKey, (async () => { const fallbackFamily = String(preferredFamily || font.family || font.name || "").trim() || "sans-serif"; const fontDirectory = getRuntimeFontDirectory(); await fs.mkdir(fontDirectory, { recursive: true }); const fileExtension = getFontExtension(font); const targetPath = path.join( fontDirectory, `${sanitizeFileSegment(font.id, "font")}-${sanitizeFileSegment( font.family || font.name || "custom-font", "custom-font" )}${fileExtension}` ); await ensureWritableFontFile(targetPath, fontBuffer); await refreshFontCache(fontDirectory); const metadata = await detectInstalledFontMetadata(targetPath, fallbackFamily); return { ...metadata, installedPath: targetPath, }; })() ); } return ensuredFontCache.get(cacheKey)!; }