diff --git a/app/api/admin/psd-templates/[id]/parse/route.ts b/app/api/admin/psd-templates/[id]/parse/route.ts index 04ff661b2..a8cb61480 100644 --- a/app/api/admin/psd-templates/[id]/parse/route.ts +++ b/app/api/admin/psd-templates/[id]/parse/route.ts @@ -147,8 +147,14 @@ function buildInitialTemplateLayers(params: { detectedFontName: layer.detectedFontName, detectedFontNames: layer.detectedFontNames, fontId: matchedFont?.font.id, - fontFamily: matchedFont?.font.family, - fontLabel: matchedFont?.font.name, + fontFamily: + matchedFont?.font.family || + layer.detectedFontNames?.[0] || + layer.detectedFontName, + fontLabel: + matchedFont?.font.name || + layer.detectedFontNames?.[0] || + layer.detectedFontName, fontFileUrl: matchedFont?.font.fileUrl, fontContentType: matchedFont?.font.contentType, fontSize: Math.max( diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index cbd4a1bba..ec40d7f9e 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -133,18 +133,58 @@ function buildPreviewFontFaces(template: AgentPsdTemplate | null) { if (!isAgentPsdTextLayerSlot(layer) || !layer.fontFamily || !layer.fontFileUrl) { continue; } - if (rules.has(layer.fontFamily)) { - continue; - } - rules.set( - layer.fontFamily, - `@font-face { font-family: "${layer.fontFamily}"; src: url("${layer.fontFileUrl}") format("${layer.fontContentType?.includes("woff2") ? "woff2" : layer.fontContentType?.includes("woff") ? "woff" : layer.fontContentType?.includes("otf") ? "opentype" : "truetype"}"); font-display: swap; }` + const familyAliases = Array.from( + new Set( + [ + layer.fontFamily, + ...(layer.detectedFontNames || []), + layer.detectedFontName, + ] + .map((family) => String(family || "").trim()) + .filter(Boolean) + ) ); + + for (const familyAlias of familyAliases) { + if (rules.has(familyAlias)) { + continue; + } + + rules.set( + familyAlias, + `@font-face { font-family: "${familyAlias}"; src: url("${layer.fontFileUrl}") format("${layer.fontContentType?.includes("woff2") ? "woff2" : layer.fontContentType?.includes("woff") ? "woff" : layer.fontContentType?.includes("otf") ? "opentype" : "truetype"}"); font-display: swap; }` + ); + } } return Array.from(rules.values()).join("\n"); } +function buildTextLayerFontFamily(layer: AgentPsdTemplateTextLayerSlot) { + const families = Array.from( + new Set( + [ + layer.fontFamily, + ...(layer.detectedFontNames || []), + layer.detectedFontName, + "PingFang SC", + "Microsoft YaHei", + "Noto Sans SC", + "Source Han Sans SC", + "sans-serif", + ] + .map((family) => String(family || "").trim()) + .filter(Boolean) + ) + ); + + return families + .map((family) => + family === "sans-serif" ? family : `"${family.replace(/"/g, '\\"')}"` + ) + .join(", "); +} + function getPreferredSelectedLayerId(template: AgentPsdTemplate | null) { if (!template || template.layers.length === 0) { return ""; @@ -205,12 +245,13 @@ function getTextLayerContentStyle( paddingX: layer.paddingX, paddingY: layer.paddingY, maxLines: layer.maxLines, + autoFit: false, }); return { width: "100%", height: "100%", - fontFamily: layer.fontFamily || "sans-serif", + fontFamily: buildTextLayerFontFamily(layer), fontSize: resolvedLayout.fontSize, lineHeight: layer.lineHeight, letterSpacing: `${layer.letterSpacing}px`, diff --git a/lib/agents/psd-template-parser.ts b/lib/agents/psd-template-parser.ts index 35407d765..0bf9dbc6d 100644 --- a/lib/agents/psd-template-parser.ts +++ b/lib/agents/psd-template-parser.ts @@ -601,6 +601,17 @@ export async function preparePsdTemplateRenderAssets(params: { }; } + if ( + currentLayer.paddingY <= 0 && + Number.isFinite(Number(analysis.suggestedPaddingY)) && + Number(analysis.suggestedPaddingY) > 0 + ) { + nextLayer = { + ...nextLayer, + paddingY: Math.max(0, Number(analysis.suggestedPaddingY)), + }; + } + if (nextLayer !== currentLayer) { nextLayers[layerIndex] = nextLayer; } diff --git a/lib/agents/psd-template-renderer.ts b/lib/agents/psd-template-renderer.ts index 9da5528a4..0a7e60d14 100644 --- a/lib/agents/psd-template-renderer.ts +++ b/lib/agents/psd-template-renderer.ts @@ -233,6 +233,7 @@ async function createTextOverlayBuffer(params: { paddingX, paddingY, maxLines, + autoFit: false, }); const adjustedFontSize = resolvedLayout.fontSize; const lineHeightPx = resolvedLayout.lineHeightPx; @@ -411,35 +412,9 @@ export async function renderAgentPsdTemplate(params: { await appendOriginalLayer(layer.layerId); } - const hasDedicatedRenderBase = - Boolean(String(template.renderBasePath || "").trim()) || - (Boolean(String(template.renderBaseUrl || "").trim()) && - String(template.renderBaseUrl || "").trim() !== String(template.previewUrl || "").trim()); - let hasBaseOverlay = false; - - if (hasDedicatedRenderBase && template.renderBaseUrl) { - try { - const baseBuffer = await readCachedBuffer(template.renderBaseUrl); - hasBaseOverlay = await appendBufferOverlay({ - input: baseBuffer, - left: 0, - top: 0, - width: canvasWidth, - height: canvasHeight, - }); - } catch (error) { - console.warn("[PSD Template] failed to reuse render base during export:", { - templateId: template.id, - error, - }); - } - } - for (const layer of [...template.layers].reverse()) { if (!layer.isEditable) { - if (!hasBaseOverlay) { - await appendOriginalLayer(layer.layerId); - } + await appendStoredLayer(layer); continue; } @@ -542,14 +517,22 @@ export async function renderAgentPsdTemplate(params: { text, fontBuffer, font, - fontFamily: layer.fontFamily || font?.family || "sans-serif", + fontFamily: + layer.fontFamily || + layer.detectedFontNames?.[0] || + layer.detectedFontName || + font?.family || + "sans-serif", renderFontFamilies: runtimeFont ? [ runtimeFont.fontPostscriptName, runtimeFont.fontFullName, runtimeFont.fontFamily, ].filter((value): value is string => Boolean(String(value || "").trim())) - : undefined, + : [ + ...(layer.detectedFontNames || []), + ...(layer.detectedFontName ? [layer.detectedFontName] : []), + ].filter((value): value is string => Boolean(String(value || "").trim())), renderFontFamily: runtimeFont?.fontFamily, useExactFontFace: Boolean(runtimeFont?.fontPostscriptName || runtimeFont?.fontFullName), fontSize: layer.fontSize, diff --git a/lib/agents/psd-template-style-resolver.ts b/lib/agents/psd-template-style-resolver.ts index d92bc5b1c..73315656d 100644 --- a/lib/agents/psd-template-style-resolver.ts +++ b/lib/agents/psd-template-style-resolver.ts @@ -142,6 +142,17 @@ export async function resolveAgentPsdTemplateTextStyles(template: AgentPsdTempla 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) { diff --git a/lib/agents/psd-text-layout.ts b/lib/agents/psd-text-layout.ts index ab9cd49b1..81204a308 100644 --- a/lib/agents/psd-text-layout.ts +++ b/lib/agents/psd-text-layout.ts @@ -91,6 +91,7 @@ export function resolveTextLayout(params: { paddingX: number; paddingY: number; maxLines: number; + autoFit?: boolean; }) { const { text, @@ -102,6 +103,7 @@ export function resolveTextLayout(params: { paddingX, paddingY, maxLines, + autoFit = true, } = params; const normalizedText = String(text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); @@ -114,6 +116,23 @@ export function resolveTextLayout(params: { let bestLines: string[] = []; let bestLineHeightPx = Math.max(fittedFontSize, fittedFontSize * resolvedLineHeight); + if (!autoFit) { + bestLines = wrapTextByWidth({ + text: normalizedText, + maxWidth: availableWidth, + fontSize: fittedFontSize, + letterSpacing, + maxLines: resolvedMaxLines, + }).slice(0, resolvedMaxLines); + bestLineHeightPx = Math.max(fittedFontSize, fittedFontSize * resolvedLineHeight); + + return { + fontSize: Number(fittedFontSize.toFixed(2)), + lineHeightPx: Number(bestLineHeightPx.toFixed(2)), + lines: bestLines, + }; + } + for (let index = 0; index < 24; index += 1) { const lineHeightPx = Math.max(fittedFontSize, fittedFontSize * resolvedLineHeight); const heightLimitedMaxLines = Math.max( @@ -216,11 +235,15 @@ export function analyzeTextLayerPixels(params: { : inferredTextAlign === "center" ? Math.max(0, Math.round(Math.min(leftGap, rightGap))) : Math.max(0, Math.round(leftGap)); + const topGap = minY; + const bottomGap = Math.max(0, height - maxY - 1); + const suggestedPaddingY = Math.max(0, Math.round(Math.min(topGap, bottomGap))); return { dominantColor, inferredTextAlign, suggestedPaddingX, + suggestedPaddingY, visibleBounds: { minX, minY,