import "server-only"; import type { AgentPsdFont } from "@/lib/agents/psd-font-config"; import { doesPsdFontMatchDetectedNames, findBestMatchingPsdFont, } from "@/lib/agents/psd-font-matching"; import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; import { AGENT_PSD_TEMPLATE_ASSET_VERSION, isAgentPsdImageLayerSlot, isAgentPsdLayerSuperLocked, isAgentPsdTemplateLayerUserEditable, isAgentPsdTextLayerSlot, type AgentPsdTemplate, } from "@/lib/agents/psd-template-config"; import { isPsdAdjustmentLikeImageLayer, pickPrimaryPsdImageLayerId, } from "@/lib/agents/psd-template-image-slot-selection"; import { parsePsdTemplateFileFromSource, preparePsdTemplateRenderAssets, } from "@/lib/agents/psd-template-parser"; import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache"; import { createStorageUploadSession } from "@/lib/storage/service"; function hasMissingLayerAssets(template: AgentPsdTemplate) { const hasRenderBase = hasDedicatedRenderBase(template); return template.layers.some( (layer) => { if ( isAgentPsdTemplateLayerUserEditable(layer) && !String(layer.previewUrl || "").trim() ) { return true; } if (hasRenderBase) { return ( isAgentPsdImageLayerSlot(layer) && isAgentPsdTemplateLayerUserEditable(layer) && !String(layer.sourceUrl || "").trim() ); } return !String(layer.sourceUrl || "").trim(); } ); } function needsTextMetadataRepair(template: AgentPsdTemplate) { return template.layers.some( (layer) => isAgentPsdTextLayerSlot(layer) && (!String(layer.detectedFontName || "").trim() || !Array.isArray(layer.detectedFontNames) || layer.detectedFontNames.length === 0) ); } function hasDedicatedRenderBase(template: AgentPsdTemplate) { const renderBaseUrl = String(template.renderBaseUrl || "").trim(); const previewUrl = String(template.previewUrl || "").trim(); return Boolean(renderBaseUrl) && renderBaseUrl !== previewUrl; } function repairEditableImageLayerSelection(template: AgentPsdTemplate) { const imageLayers = template.layers.filter(isAgentPsdImageLayerSlot); const unlockedImageLayers = imageLayers.filter( (layer) => !isAgentPsdLayerSuperLocked(layer) ); if (unlockedImageLayers.length === 0) { return template.layers; } const editableImageLayers = unlockedImageLayers.filter( isAgentPsdTemplateLayerUserEditable ); const defaultImageLayer = unlockedImageLayers.find( (layer) => isAgentPsdTemplateLayerUserEditable(layer) && layer.isDefaultResultSource ); const shouldAutoRepairSelection = editableImageLayers.length === 0 || !defaultImageLayer || (editableImageLayers.length === 1 && isPsdAdjustmentLikeImageLayer(editableImageLayers[0])); if (!shouldAutoRepairSelection) { return template.layers; } const nextPrimaryLayerId = pickPrimaryPsdImageLayerId({ imageLayers: unlockedImageLayers, canvasWidth: template.width, canvasHeight: template.height, }); if (!nextPrimaryLayerId) { return template.layers; } const currentPrimaryLayerId = defaultImageLayer?.id || editableImageLayers[0]?.id || ""; if (currentPrimaryLayerId === nextPrimaryLayerId) { return template.layers; } return template.layers.map((layer) => { if (!isAgentPsdImageLayerSlot(layer)) { return layer; } if (layer.id === nextPrimaryLayerId) { return { ...layer, isEditable: true, isDefaultResultSource: true, allowUserUploadOverride: true, }; } if (layer.id === currentPrimaryLayerId) { return { ...layer, isEditable: false, isDefaultResultSource: false, allowUserUploadOverride: false, }; } return layer; }); } function toArrayBuffer(buffer: Buffer) { return buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; } export function repairAgentPsdTemplateFontBindings( template: AgentPsdTemplate, fonts: AgentPsdFont[] ) { if (fonts.length === 0) { return template; } const fontMap = new Map(fonts.map((font) => [font.id, font])); let didChange = false; const nextLayers = template.layers.map((layer) => { if (!isAgentPsdTextLayerSlot(layer) || layer.styleSource === "custom") { return layer; } const detectedFontNames = [ ...((layer.detectedFontNames || []).filter(Boolean)), ...(layer.detectedFontName ? [layer.detectedFontName] : []), ]; const preferredDetectedFontName = detectedFontNames.find((entry) => String(entry || "").trim()) || ""; const currentFont = layer.fontId ? fontMap.get(layer.fontId) : undefined; const currentFontMatches = currentFont ? doesPsdFontMatchDetectedNames({ detectedFontNames, font: currentFont, }) : false; const matchedFont = findBestMatchingPsdFont({ detectedFontNames, fonts, }); if ( matchedFont?.confidence === "high" && (!currentFontMatches || !layer.fontId) ) { didChange = true; return { ...layer, fontId: matchedFont.font.id, fontFamily: matchedFont.font.family, fontLabel: matchedFont.font.name, fontFileUrl: matchedFont.font.fileUrl, fontContentType: matchedFont.font.contentType, }; } if ( !matchedFont && !currentFontMatches && ( layer.fontId || layer.fontFileUrl || layer.fontContentType || (preferredDetectedFontName && ( layer.fontFamily !== preferredDetectedFontName || layer.fontLabel !== preferredDetectedFontName )) ) ) { didChange = true; return { ...layer, fontId: undefined, fontFamily: preferredDetectedFontName || undefined, fontLabel: preferredDetectedFontName || undefined, fontFileUrl: undefined, fontContentType: undefined, }; } if (currentFont && ( layer.fontFamily !== currentFont.family || layer.fontLabel !== currentFont.name || layer.fontFileUrl !== currentFont.fileUrl || layer.fontContentType !== currentFont.contentType )) { didChange = true; return { ...layer, fontFamily: currentFont.family, fontLabel: currentFont.name, fontFileUrl: currentFont.fileUrl, fontContentType: currentFont.contentType, }; } return layer; }); if (!didChange) { return template; } return { ...template, layers: nextLayers, updatedAt: new Date().toISOString(), } satisfies AgentPsdTemplate; } export async function repairAgentPsdTemplateAssetsIfNeeded( template: AgentPsdTemplate ) { if (template.parseStatus !== "completed") { return template; } const needsAssetVersionRefresh = Number(template.assetVersion || 0) !== AGENT_PSD_TEMPLATE_ASSET_VERSION; const needsRepair = needsAssetVersionRefresh || hasMissingLayerAssets(template); const needsRenderBaseRepair = !hasDedicatedRenderBase(template); const fonts = await readAgentPsdFonts().catch(() => []); const needsFontDetection = needsTextMetadataRepair(template); let nextLayers = repairEditableImageLayerSelection(template); if (needsFontDetection) { try { const localFilePath = resolveAgentPsdLocalCachePath({ templateId: template.id, fileName: template.fileName, }); const parsedTemplate = await parsePsdTemplateFileFromSource({ fileUrl: template.fileUrl, filePath: localFilePath, }); const parsedTextLayerMap = new Map( parsedTemplate.layers .filter((layer) => layer.slotType === "text") .map((layer) => [layer.layerId, layer]) ); nextLayers = nextLayers.map((layer) => { if (!isAgentPsdTextLayerSlot(layer)) return layer; const parsedLayer = parsedTextLayerMap.get(layer.layerId); if (!parsedLayer) return layer; return { ...layer, detectedFontName: parsedLayer.detectedFontName || layer.detectedFontName, detectedFontNames: parsedLayer.detectedFontNames || layer.detectedFontNames, textColor: layer.styleSource !== "custom" && (!String(layer.textColor || "").trim() || layer.textColor === "#111111") && parsedLayer.detectedTextColor ? parsedLayer.detectedTextColor : layer.textColor, }; }); } catch (error) { console.warn("[PSD Template] failed to repair parsed text metadata:", { templateId: template.id, error, }); } } const fontRepairedTemplate = repairAgentPsdTemplateFontBindings( { ...template, layers: nextLayers, }, fonts ); nextLayers = fontRepairedTemplate.layers; const hasFontRepair = nextLayers.some( (layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index]) ); if (!needsRepair && !hasFontRepair && !needsRenderBaseRepair) { return template; } try { const uploadAsset = await createStorageUploadSession(); const preparedAssets = await preparePsdTemplateRenderAssets({ templateId: template.id, fileUrl: template.fileUrl, filePath: resolveAgentPsdLocalCachePath({ templateId: template.id, fileName: template.fileName, }), layers: nextLayers, forceRefreshLayerAssets: needsAssetVersionRefresh, uploadAsset, }); const uploadedRenderBase = needsRenderBaseRepair ? await uploadAsset( `agent-psd-base-${template.id}-${Date.now()}.png`, toArrayBuffer(preparedAssets.renderBaseBuffer) ) : null; return { ...template, renderBaseUrl: uploadedRenderBase?.url || template.renderBaseUrl, renderBasePath: uploadedRenderBase?.pathname || template.renderBasePath, layers: preparedAssets.layers, assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION, updatedAt: new Date().toISOString(), } satisfies AgentPsdTemplate; } catch (error) { console.warn("[PSD Template] failed to repair template assets:", { templateId: template.id, error, }); return template; } }