import "server-only"; import { analyzeTextLayerPixels, normalizeHexColor } from "@/lib/agents/psd-text-layout"; import { isAgentPsdTextLayerSlot, type AgentPsdTemplate, type AgentPsdTemplateTextLayerSlot, } from "@/lib/agents/psd-template-config"; import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache"; import { loadPsdTemplateSource } from "@/lib/agents/psd-template-parser"; function getEstimatedFontSize(height: number) { return Math.max(16, Math.min(height, Math.round(height * 0.78))); } function shouldRepairAutoTextLayer(layer: AgentPsdTemplateTextLayerSlot) { return layer.styleSource !== "custom"; } function shouldPreferDetectedFontMetrics( layer: AgentPsdTemplateTextLayerSlot, detectedFontSize?: number ) { if (!Number.isFinite(detectedFontSize) || Number(detectedFontSize) <= 0) { return false; } const safeDetectedFontSize = Number(detectedFontSize); const fallbackEstimatedFontSize = getEstimatedFontSize(layer.height); const currentFontSize = Math.max(10, Number(layer.fontSize || 10)); const relativeDiff = Math.abs(currentFontSize - safeDetectedFontSize) / Math.max(1, safeDetectedFontSize); return ( shouldRepairAutoTextLayer(layer) && (Math.abs(currentFontSize - fallbackEstimatedFontSize) <= 2 || relativeDiff > 0.16) ); } async function getPixelAnalysis( cache: Map>>, layerId: string, loadRgba: () => Promise, width: number, height: number ) { if (!cache.has(layerId)) { cache.set( layerId, loadRgba() .then((rgba) => analyzeTextLayerPixels({ rgba, width, height })) .catch(() => null) ); } return cache.get(layerId)!; } export async function resolveAgentPsdTemplateTextStyles(template: AgentPsdTemplate) { const autoTextLayers = template.layers.filter( (layer): layer is AgentPsdTemplateTextLayerSlot => isAgentPsdTextLayerSlot(layer) && shouldRepairAutoTextLayer(layer) ); if (autoTextLayers.length === 0) { return template; } const source = await loadPsdTemplateSource({ fileUrl: template.fileUrl, filePath: resolveAgentPsdLocalCachePath({ templateId: template.id, fileName: template.fileName, }), }); const sourceLayerMap = new Map(source.layers.map((entry) => [entry.layer.layerId, entry])); const pixelAnalysisCache = new Map< string, Promise> >(); let didChange = false; const nextLayers = await Promise.all( template.layers.map(async (layer) => { if (!isAgentPsdTextLayerSlot(layer) || !shouldRepairAutoTextLayer(layer)) { return layer; } const sourceEntry = sourceLayerMap.get(layer.layerId); if (!sourceEntry) { return layer; } let nextLayer = layer; if (shouldPreferDetectedFontMetrics(layer, sourceEntry.layer.detectedFontSize)) { nextLayer = { ...nextLayer, fontSize: Math.max(10, Math.round(Number(sourceEntry.layer.detectedFontSize || nextLayer.fontSize))), lineHeight: Number.isFinite(Number(sourceEntry.layer.detectedLineHeight)) && Number(sourceEntry.layer.detectedLineHeight) > 0 ? Number(sourceEntry.layer.detectedLineHeight) : nextLayer.lineHeight, letterSpacing: Number.isFinite(Number(sourceEntry.layer.detectedLetterSpacing)) ? Number(sourceEntry.layer.detectedLetterSpacing) : nextLayer.letterSpacing, }; } const needsColorRepair = normalizeHexColor(nextLayer.textColor) === "#111111"; const needsAlignRepair = nextLayer.textAlign === "left"; if (needsColorRepair && sourceEntry.layer.detectedTextColor) { nextLayer = { ...nextLayer, textColor: sourceEntry.layer.detectedTextColor, }; } if (needsColorRepair || needsAlignRepair) { const analysis = await getPixelAnalysis( pixelAnalysisCache, layer.layerId, () => sourceEntry.composite(), sourceEntry.layer.width, sourceEntry.layer.height ); if ( analysis?.dominantColor && normalizeHexColor(nextLayer.textColor) === "#111111" ) { nextLayer = { ...nextLayer, textColor: analysis.dominantColor, }; } if ( analysis?.inferredTextAlign && needsAlignRepair && analysis.inferredTextAlign !== nextLayer.textAlign ) { nextLayer = { ...nextLayer, textAlign: analysis.inferredTextAlign, }; } if ( Number(nextLayer.paddingX || 0) <= 0 && Number.isFinite(Number(analysis?.suggestedPaddingX)) && Number(analysis?.suggestedPaddingX) > 0 ) { nextLayer = { ...nextLayer, paddingX: Math.max(0, Number(analysis?.suggestedPaddingX)), }; } if ( Number(nextLayer.paddingY || 0) <= 0 && Number.isFinite(Number(analysis?.suggestedPaddingY)) && Number(analysis?.suggestedPaddingY) > 0 ) { nextLayer = { ...nextLayer, paddingY: Math.max(0, Number(analysis?.suggestedPaddingY)), }; } } if (nextLayer !== layer) { didChange = true; } return nextLayer; }) ); return didChange ? { ...template, layers: nextLayers, } : template; }