diff --git a/app/api/admin/psd-templates/[id]/parse/route.ts b/app/api/admin/psd-templates/[id]/parse/route.ts index 1dffe11a7..fa18c2c3f 100644 --- a/app/api/admin/psd-templates/[id]/parse/route.ts +++ b/app/api/admin/psd-templates/[id]/parse/route.ts @@ -91,6 +91,7 @@ function buildInitialTemplateLayers(params: { detectedFontNames: layer.detectedFontNames || [], fonts, }); + const autoBoundFont = matchedFont?.confidence === "high" ? matchedFont.font : undefined; const isEditable = shouldAutoEditableTextLayer(layer); return { @@ -102,17 +103,17 @@ function buildInitialTemplateLayers(params: { placeholder: layer.defaultText || layer.layerName, detectedFontName: layer.detectedFontName, detectedFontNames: layer.detectedFontNames, - fontId: matchedFont?.font.id, + fontId: autoBoundFont?.id, fontFamily: - matchedFont?.font.family || + autoBoundFont?.family || layer.detectedFontNames?.[0] || layer.detectedFontName, fontLabel: - matchedFont?.font.name || + autoBoundFont?.name || layer.detectedFontNames?.[0] || layer.detectedFontName, - fontFileUrl: matchedFont?.font.fileUrl, - fontContentType: matchedFont?.font.contentType, + fontFileUrl: autoBoundFont?.fileUrl, + fontContentType: autoBoundFont?.contentType, fontSize: Math.max( 10, Math.round( @@ -130,7 +131,7 @@ function buildInitialTemplateLayers(params: { ? Number(layer.detectedLetterSpacing) : 0, fontWeight: 500, - textColor: "#111111", + textColor: layer.detectedTextColor || "#111111", textAlign: "left", paddingX: 0, paddingY: 0, diff --git a/app/api/admin/psd-templates/[id]/route.ts b/app/api/admin/psd-templates/[id]/route.ts index dc265acb8..d33e9bc0e 100644 --- a/app/api/admin/psd-templates/[id]/route.ts +++ b/app/api/admin/psd-templates/[id]/route.ts @@ -169,7 +169,7 @@ export async function PUT( ], fonts, }); - if (matchedFont) { + if (matchedFont?.confidence === "high") { layer.fontId = matchedFont.font.id; layer.fontFamily = matchedFont.font.family; layer.fontLabel = matchedFont.font.name; diff --git a/app/api/psd-templates/route.ts b/app/api/psd-templates/route.ts index c07e60116..cbff9cb18 100644 --- a/app/api/psd-templates/route.ts +++ b/app/api/psd-templates/route.ts @@ -27,6 +27,7 @@ import { normalizeAgentPsdTemplateCategoryTagConfig, } from "@/lib/agents/psd-template-category-tag-config"; import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; +import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; export const runtime = "nodejs"; @@ -92,6 +93,7 @@ export async function GET(request: NextRequest) { ) .filter((template) => (requestedMode ? template.mode === requestedMode : true)) .filter((template) => getEditableAgentPsdTemplateLayers(template).length > 0) + .map((template) => repairAgentPsdTemplateFontBindings(template, fonts)) .map((template) => ({ ...template, layers: template.layers.map((layer) => { diff --git a/lib/agents/psd-font-matching.ts b/lib/agents/psd-font-matching.ts index 10d461b98..e7e101773 100644 --- a/lib/agents/psd-font-matching.ts +++ b/lib/agents/psd-font-matching.ts @@ -32,6 +32,31 @@ export function normalizeDetectedFontNames(value: unknown) { ); } +function getPsdFontIdentityTokens(font: Pick) { + return Array.from( + new Set( + [font.name, font.family] + .map((item) => normalizeFontToken(String(item || ""))) + .filter(Boolean) + ) + ); +} + +export function doesPsdFontMatchDetectedNames(params: { + detectedFontNames: string[]; + font: Pick; +}) { + const detectedFontNames = normalizeDetectedFontNames(params.detectedFontNames); + if (detectedFontNames.length === 0) { + return true; + } + + const fontTokens = new Set(getPsdFontIdentityTokens(params.font)); + return detectedFontNames.some((detectedFontName) => + fontTokens.has(normalizeFontToken(detectedFontName)) + ); +} + export function findBestMatchingPsdFont(params: { detectedFontNames: string[]; fonts: AgentPsdFont[]; diff --git a/lib/agents/psd-template-parser.ts b/lib/agents/psd-template-parser.ts index 10f1d1a1d..f4ac330ed 100644 --- a/lib/agents/psd-template-parser.ts +++ b/lib/agents/psd-template-parser.ts @@ -15,6 +15,11 @@ type PsdFontSetEntry = { Name?: string; }; +type PsdTextStyleColorValue = { + Values?: number[]; + Type?: number; +}; + type PsdTextStyleRun = { StyleSheet?: { StyleSheetData?: { @@ -23,6 +28,7 @@ type PsdTextStyleRun = { Leading?: number; AutoLeading?: number | boolean; Tracking?: number; + FillColor?: PsdTextStyleColorValue; }; }; }; @@ -87,6 +93,7 @@ export interface ParsedPsdTemplateLayer { detectedFontSize?: number; detectedLineHeight?: number; detectedLetterSpacing?: number; + detectedTextColor?: string; } export interface ParsedPsdTemplatePayload { @@ -277,6 +284,30 @@ function extractTextTransformScale(additionalProperties?: Record Number.isFinite(channel))) { + return undefined; + } + + return normalizeHexColor( + `#${Math.round(Math.max(0, Math.min(1, red)) * 255) + .toString(16) + .padStart(2, "0")}${Math.round(Math.max(0, Math.min(1, green)) * 255) + .toString(16) + .padStart(2, "0")}${Math.round(Math.max(0, Math.min(1, blue)) * 255) + .toString(16) + .padStart(2, "0")}` + ); +} + function extractTextStyleMetrics( textProperties?: PsdTextPropertiesLike, additionalProperties?: Record @@ -286,8 +317,12 @@ function extractTextStyleMetrics( const style = styleRun?.StyleSheet?.StyleSheetData; return Number.isFinite(Number(style?.FontSize)); }); + const firstColorRun = styleRuns.find((styleRun) => + Boolean(extractHexColorFromTextStyle(styleRun?.StyleSheet?.StyleSheetData?.FillColor)) + ); const style = firstValidRun?.StyleSheet?.StyleSheetData; + const colorStyle = firstColorRun?.StyleSheet?.StyleSheetData; const fontSizeValue = Number(style?.FontSize); const leadingValue = Number(style?.Leading); const trackingValue = Number(style?.Tracking); @@ -312,6 +347,7 @@ function extractTextStyleMetrics( detectedFontSize, detectedLineHeight, detectedLetterSpacing, + detectedTextColor: extractHexColorFromTextStyle(colorStyle?.FillColor), }; } @@ -364,6 +400,7 @@ function collectPsdLayerEntries( detectedFontSize: undefined, detectedLineHeight: undefined, detectedLetterSpacing: undefined, + detectedTextColor: undefined, }; result.push({ @@ -385,6 +422,7 @@ function collectPsdLayerEntries( detectedFontSize: textStyleMetrics.detectedFontSize, detectedLineHeight: textStyleMetrics.detectedLineHeight, detectedLetterSpacing: textStyleMetrics.detectedLetterSpacing, + detectedTextColor: textStyleMetrics.detectedTextColor, }, }); } diff --git a/lib/agents/psd-template-repair.ts b/lib/agents/psd-template-repair.ts index 7f0d74797..98aa6f654 100644 --- a/lib/agents/psd-template-repair.ts +++ b/lib/agents/psd-template-repair.ts @@ -1,6 +1,10 @@ import "server-only"; -import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching"; +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, @@ -111,6 +115,83 @@ function toArrayBuffer(buffer: Buffer) { ) 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 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 (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 ) { @@ -142,6 +223,12 @@ export async function repairAgentPsdTemplateAssetsIfNeeded( ...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) { @@ -152,31 +239,14 @@ export async function repairAgentPsdTemplateAssetsIfNeeded( } } - nextLayers = nextLayers.map((layer) => { - if (!isAgentPsdTextLayerSlot(layer) || layer.fontId) { - return layer; - } - - const matchedFont = findBestMatchingPsdFont({ - detectedFontNames: [ - ...((layer.detectedFontNames || []).filter(Boolean)), - ...(layer.detectedFontName ? [layer.detectedFontName] : []), - ], - fonts, - }); - if (!matchedFont) { - return layer; - } - - return { - ...layer, - fontId: matchedFont.font.id, - fontFamily: matchedFont.font.family, - fontLabel: matchedFont.font.name, - fontFileUrl: matchedFont.font.fileUrl, - fontContentType: matchedFont.font.contentType, - }; - }); + const fontRepairedTemplate = repairAgentPsdTemplateFontBindings( + { + ...template, + layers: nextLayers, + }, + fonts + ); + nextLayers = fontRepairedTemplate.layers; const hasFontRepair = nextLayers.some( (layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index]) diff --git a/lib/agents/psd-template-style-resolver.ts b/lib/agents/psd-template-style-resolver.ts index 73315656d..c70fc49b2 100644 --- a/lib/agents/psd-template-style-resolver.ts +++ b/lib/agents/psd-template-style-resolver.ts @@ -105,6 +105,13 @@ export async function resolveAgentPsdTemplateTextStyles(template: AgentPsdTempla 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, @@ -114,7 +121,10 @@ export async function resolveAgentPsdTemplateTextStyles(template: AgentPsdTempla sourceEntry.layer.height ); - if (analysis?.dominantColor && needsColorRepair) { + if ( + analysis?.dominantColor && + normalizeHexColor(nextLayer.textColor) === "#111111" + ) { nextLayer = { ...nextLayer, textColor: analysis.dominantColor,