fix: harden psd template rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-01 22:36:45 +08:00
parent 494a3c6d48
commit fb6b2170bd
9 changed files with 383 additions and 256 deletions

View File

@ -5,6 +5,7 @@ import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import {
AGENT_PSD_TEMPLATES_KEY,
AGENT_PSD_TEMPLATE_ASSET_VERSION,
type AgentPsdTemplate,
type AgentPsdTemplateLayerSlot,
type AgentPsdTemplateMutationResponse,
@ -356,6 +357,7 @@ export async function POST(
height: parsedTemplate.height,
layerCount: parsedTemplate.layers.length,
layers: preparedAssets.layers,
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
status: nextStatus,
parseStatus: "completed",
parseProgress: 100,

View File

@ -5,6 +5,7 @@ import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
import {
AGENT_PSD_TEMPLATES_KEY,
AGENT_PSD_TEMPLATE_ASSET_VERSION,
getEditableAgentPsdTemplateLayers,
isAgentPsdTextLayerSlot,
normalizeAgentPsdTemplateStatus,
@ -12,10 +13,10 @@ import {
type AgentPsdTemplateMutationResponse,
} from "@/lib/agents/psd-template-config";
import {
parsePsdTemplateFileFromUrl,
preparePsdTemplateRenderAssets,
} from "@/lib/agents/psd-template-parser";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
import { uploadFile } from "@/lib/storage/service";
import {
readAgentPsdTemplates,
@ -40,105 +41,6 @@ function jsonError(status: number, error: string) {
return NextResponse.json(payload, { status });
}
async function repairTemplateLayerPreviewsIfNeeded(template: NonNullable<Awaited<ReturnType<typeof readAgentPsdTemplates>>[number]>) {
if (template.parseStatus !== "completed") {
return template;
}
const needsRepair = template.layers.some(
(layer) =>
!String(layer.previewUrl || "").trim() || !String(layer.sourceUrl || "").trim()
);
const fonts = await readAgentPsdFonts().catch(() => []);
const needsFontDetection = template.layers.some(
(layer) =>
isAgentPsdTextLayerSlot(layer) &&
(!String(layer.detectedFontName || "").trim() ||
!Array.isArray(layer.detectedFontNames) ||
layer.detectedFontNames.length === 0)
);
let nextLayers = template.layers;
if (needsFontDetection) {
try {
const parsedTemplate = await parsePsdTemplateFileFromUrl(template.fileUrl);
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,
};
});
} catch (error) {
console.warn("[PSD Templates][Admin] failed to parse PSD font metadata:", {
templateId: template.id,
error,
});
}
}
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 hasFontRepair = nextLayers.some(
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
);
if (!needsRepair && !hasFontRepair) {
return template;
}
try {
const preparedAssets = await preparePsdTemplateRenderAssets({
templateId: template.id,
fileUrl: template.fileUrl,
layers: nextLayers,
});
return {
...template,
layers: preparedAssets.layers,
updatedAt: new Date().toISOString(),
};
} catch (error) {
console.warn("[PSD Templates][Admin] failed to repair missing layer previews:", {
templateId: template.id,
error,
});
return template;
}
}
export async function GET(
request: NextRequest,
context: { params: Promise<{ id: string }> }
@ -157,7 +59,7 @@ export async function GET(
return jsonError(404, "未找到 PSD 模板");
}
const repairedTemplate = await repairTemplateLayerPreviewsIfNeeded(template);
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
if (repairedTemplate !== template) {
await upsertSystemSetting({
key: AGENT_PSD_TEMPLATES_KEY,
@ -310,6 +212,7 @@ export async function PUT(
renderBasePath: uploadedRenderBase.pathname,
layers: nextLayers,
layerCount: Math.max(currentTemplate.layerCount, nextLayers.length),
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
updatedAt: new Date().toISOString(),
};

View File

@ -2,11 +2,10 @@ import { randomUUID } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { upsertSystemSetting } from "@/lib/db/queries";
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
import { uploadFile } from "@/lib/storage/service";
import {
AGENT_PSD_TEMPLATES_KEY,
AGENT_PSD_TEMPLATE_ASSET_VERSION,
normalizeAgentPsdTemplateStatus,
upsertAgentPsdTemplate,
type AgentPsdTemplate,
@ -14,10 +13,7 @@ import {
type AgentPsdTemplateListResponse,
type AgentPsdTemplateMutationResponse,
} from "@/lib/agents/psd-template-config";
import {
parsePsdTemplateFileFromUrl,
preparePsdTemplateRenderAssets,
} from "@/lib/agents/psd-template-parser";
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
export const runtime = "nodejs";
@ -61,107 +57,6 @@ function toArrayBuffer(buffer: Buffer) {
) as ArrayBuffer;
}
async function repairTemplateLayerPreviewsIfNeeded(params: {
template: AgentPsdTemplate;
updatedBy: string;
}) {
const { template } = params;
if (template.parseStatus !== "completed") {
return template;
}
const needsRepair = template.layers.some(
(layer) =>
!String(layer.previewUrl || "").trim() || !String(layer.sourceUrl || "").trim()
);
const fonts = await readAgentPsdFonts().catch(() => []);
const needsFontDetection = template.layers.some(
(layer) =>
layer.slotType === "text" &&
(!String(layer.detectedFontName || "").trim() ||
!Array.isArray(layer.detectedFontNames) ||
layer.detectedFontNames.length === 0)
);
let nextLayers = template.layers;
if (needsFontDetection) {
try {
const parsedTemplate = await parsePsdTemplateFileFromUrl(template.fileUrl);
const parsedTextLayerMap = new Map(
parsedTemplate.layers
.filter((layer) => layer.slotType === "text")
.map((layer) => [layer.layerId, layer])
);
nextLayers = nextLayers.map((layer) => {
if (layer.slotType !== "text") return layer;
const parsedLayer = parsedTextLayerMap.get(layer.layerId);
if (!parsedLayer) return layer;
return {
...layer,
detectedFontName: parsedLayer.detectedFontName || layer.detectedFontName,
detectedFontNames: parsedLayer.detectedFontNames || layer.detectedFontNames,
};
});
} catch (error) {
console.warn("[PSD Templates][Admin] failed to parse PSD font metadata:", {
templateId: template.id,
error,
});
}
}
nextLayers = nextLayers.map((layer) => {
if (layer.slotType !== "text") return layer;
if (layer.fontId) return layer;
const detectedFontNames = [
...((layer.detectedFontNames || []).filter(Boolean)),
...(layer.detectedFontName ? [layer.detectedFontName] : []),
];
const matchedFont = findBestMatchingPsdFont({
detectedFontNames,
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 hasFontRepair = nextLayers.some(
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
);
if (!needsRepair && !hasFontRepair) {
return template;
}
try {
const preparedAssets = await preparePsdTemplateRenderAssets({
templateId: template.id,
fileUrl: template.fileUrl,
layers: nextLayers,
});
return {
...template,
layers: preparedAssets.layers,
updatedAt: new Date().toISOString(),
} satisfies AgentPsdTemplate;
} catch (error) {
console.warn("[PSD Templates][Admin] failed to repair missing layer previews:", {
templateId: template.id,
error,
});
return template;
}
}
export async function GET() {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session?.user?.id) {
@ -174,10 +69,7 @@ export async function GET() {
let hasRepairedTemplate = false;
for (const template of templates) {
const repairedTemplate = await repairTemplateLayerPreviewsIfNeeded({
template,
updatedBy: session.user.id,
});
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
repairedTemplates.push(repairedTemplate);
if (repairedTemplate !== template) {
hasRepairedTemplate = true;
@ -289,6 +181,7 @@ export async function POST(request: NextRequest) {
height: 1,
layerCount: 0,
layers: [] as AgentPsdTemplateLayerSlot[],
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
createdAt: now,
updatedAt: now,
uploadedById: session.user.id,

View File

@ -1,7 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { upsertSystemSetting } from "@/lib/db/queries";
import {
AGENT_PSD_TEMPLATES_KEY,
getEditableAgentPsdTemplateLayers,
isAgentPsdImageLayerSlot,
isAgentPsdTemplateReady,
@ -10,6 +12,7 @@ import {
} from "@/lib/agents/psd-template-config";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement";
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
import { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
@ -56,19 +59,36 @@ export async function POST(request: NextRequest) {
try {
const templates = await readAgentPsdTemplates();
const template = templates.find(
const templateIndex = templates.findIndex(
(entry) =>
entry.id === body.templateId &&
entry.status === "active" &&
isAgentPsdTemplateReady(entry)
);
const template = templateIndex >= 0 ? templates[templateIndex] : undefined;
if (!template) {
return jsonError(404, "PSD 模板不存在或未启用");
}
const resolvedTemplate = await resolveAgentPsdTemplateTextStyles(template).catch(
() => template
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
if (repairedTemplate !== template) {
await upsertSystemSetting({
key: AGENT_PSD_TEMPLATES_KEY,
value: templates.map((entry, index) =>
index === templateIndex ? repairedTemplate : entry
),
updatedBy: session.user.id,
}).catch((error) => {
console.warn("[PSD Templates] failed to persist repaired template before render:", {
templateId: template.id,
error,
});
});
}
const resolvedTemplate = await resolveAgentPsdTemplateTextStyles(repairedTemplate).catch(
() => repairedTemplate
);
const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate);
if (editableLayers.length === 0) {
@ -139,6 +159,10 @@ export async function POST(request: NextRequest) {
} satisfies AgentPsdTemplateRenderResponse);
} catch (error) {
console.error("[PSD Templates] render failed:", error);
return jsonError(502, "PSD 模板渲染失败");
const message =
error instanceof Error && String(error.message || "").trim()
? error.message
: "PSD 模板渲染失败";
return jsonError(502, message);
}
}

View File

@ -1,12 +1,15 @@
import { NextRequest, NextResponse } from "next/server";
import { upsertSystemSetting } from "@/lib/db/queries";
import { auth } from "@/app/(auth)/auth";
import {
AGENT_PSD_TEMPLATES_KEY,
getEditableAgentPsdTemplateLayers,
isAgentPsdTextLayerSlot,
isAgentPsdTemplateReady,
type AgentPsdTemplateListResponse,
} from "@/lib/agents/psd-template-config";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
@ -30,12 +33,49 @@ export async function GET(request: NextRequest) {
try {
const requestedBrand = String(request.nextUrl.searchParams.get("brand") || "").trim();
const allTemplates = await readAgentPsdTemplates();
const repairedTemplateMap = new Map<
string,
Awaited<ReturnType<typeof readAgentPsdTemplates>>[number]
>();
for (const template of allTemplates) {
if (
template.status !== "active" ||
!isAgentPsdTemplateReady(template) ||
(requestedBrand ? template.brandName !== requestedBrand : false) ||
getEditableAgentPsdTemplateLayers(template).length === 0
) {
continue;
}
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
if (repairedTemplate !== template) {
repairedTemplateMap.set(template.id, repairedTemplate);
}
}
const sourceTemplates =
repairedTemplateMap.size > 0
? allTemplates.map((template) => repairedTemplateMap.get(template.id) || template)
: allTemplates;
if (repairedTemplateMap.size > 0) {
await upsertSystemSetting({
key: AGENT_PSD_TEMPLATES_KEY,
value: sourceTemplates,
updatedBy: session.user.id,
}).catch((error) => {
console.warn("[PSD Templates] failed to persist repaired assets:", error);
});
}
const fonts = (await readAgentPsdFonts().catch(
() => [] as Awaited<ReturnType<typeof readAgentPsdFonts>>
)) as Awaited<ReturnType<typeof readAgentPsdFonts>>;
const fontMap = new Map(fonts.map((font) => [font.id, font]));
const templates = await Promise.all(
(await readAgentPsdTemplates())
sourceTemplates
.filter((template) => template.status === "active")
.filter(isAgentPsdTemplateReady)
.filter((template) =>

View File

@ -1,4 +1,5 @@
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 3;
export type AgentPsdTemplateStatus = "active" | "disabled";
export type AgentPsdTemplateParseStatus =
@ -88,6 +89,7 @@ export interface AgentPsdTemplate {
height: number;
layerCount: number;
layers: AgentPsdTemplateLayerSlot[];
assetVersion?: number;
createdAt: string;
updatedAt: string;
uploadedById?: string;
@ -399,6 +401,7 @@ export function parseAgentPsdTemplates(value: unknown) {
height,
layerCount,
layers,
assetVersion: normalizePositiveInteger(record.assetVersion),
createdAt,
updatedAt,
uploadedById: normalizeText(record.uploadedById) || undefined,

View File

@ -502,6 +502,7 @@ export async function preparePsdTemplateRenderAssets(params: {
templateId: string;
fileUrl: string;
layers: AgentPsdTemplateLayerSlot[];
forceRefreshLayerAssets?: boolean;
}) {
const psdBuffer = await fetchRemoteBuffer(params.fileUrl);
const psd = await parsePsdBuffer(psdBuffer);
@ -520,9 +521,9 @@ export async function preparePsdTemplateRenderAssets(params: {
}
try {
const rgba = await entry.node.composite(false, true);
const rgba = await entry.node.composite();
if (!targetLayer.previewUrl) {
if (params.forceRefreshLayerAssets || !targetLayer.previewUrl) {
const thumbnailBuffer = await createLayerThumbnailBuffer({
rgba,
width: entry.layer.width,
@ -545,7 +546,7 @@ export async function preparePsdTemplateRenderAssets(params: {
}
}
if (!String(targetLayer.sourceUrl || "").trim()) {
if (params.forceRefreshLayerAssets || !String(targetLayer.sourceUrl || "").trim()) {
const sourceBuffer = await rgbaToPngBuffer(rgba, entry.layer.width, entry.layer.height);
const uploadedSource = await uploadFile(
`agent-psd-layer-source-${params.templateId}-${sanitizeFileSegment(targetLayer.layerId, targetLayer.id)}.png`,
@ -606,7 +607,11 @@ export async function preparePsdTemplateRenderAssets(params: {
}
}
if (!isAgentPsdImageLayerSlot(targetLayer) || !targetLayer.isEditable || targetLayer.maskUrl) {
if (
!isAgentPsdImageLayerSlot(targetLayer) ||
!targetLayer.isEditable ||
(!params.forceRefreshLayerAssets && targetLayer.maskUrl)
) {
continue;
}

View File

@ -305,12 +305,13 @@ export async function renderAgentPsdTemplate(params: {
}) {
const { template, imageAssignments, imagePlacements, textAssignments, fonts } = params;
const sharp = await getSharp();
const source = await loadPsdTemplateSourceFromUrl(template.fileUrl);
const canvasWidth = Math.max(1, Math.round(Number(source.width || template.width || 1)));
const canvasHeight = Math.max(1, Math.round(Number(source.height || template.height || 1)));
const canvasWidth = Math.max(1, Math.round(Number(template.width || 1)));
const canvasHeight = Math.max(1, Math.round(Number(template.height || 1)));
const remoteBufferCache = new Map<string, Buffer>();
const fontMap = new Map(fonts.map((font) => [font.id, font]));
const sourceLayerMap = new Map(source.layers.map((entry) => [entry.layer.layerId, entry]));
let sourceLayerMapPromise:
| Promise<Map<string, Awaited<ReturnType<typeof loadPsdTemplateSourceFromUrl>>["layers"][number]>>
| null = null;
async function readCachedBuffer(url: string) {
const normalizedUrl = String(url || "").trim();
@ -325,9 +326,44 @@ export async function renderAgentPsdTemplate(params: {
return remoteBufferCache.get(normalizedUrl)!;
}
async function getSourceLayerMap() {
if (!sourceLayerMapPromise) {
sourceLayerMapPromise = loadPsdTemplateSourceFromUrl(template.fileUrl).then(
(source) => new Map(source.layers.map((entry) => [entry.layer.layerId, entry]))
);
}
return sourceLayerMapPromise;
}
const compositeInputs: Array<{ input: Buffer; left: number; top: number }> = [];
async function appendBufferOverlay(params: {
input: Buffer;
left: number;
top: number;
width: number;
height: number;
}) {
const fittedOverlay = await fitOverlayBufferIntoCanvas({
input: params.input,
left: params.left,
top: params.top,
width: params.width,
height: params.height,
canvasWidth,
canvasHeight,
});
if (!fittedOverlay) {
return false;
}
compositeInputs.push(fittedOverlay);
return true;
}
async function appendOriginalLayer(layerId: string) {
const sourceLayerMap = await getSourceLayerMap();
const sourceLayer = sourceLayerMap.get(layerId);
if (!sourceLayer) return;
const rgba = await sourceLayer.composite();
@ -336,29 +372,81 @@ export async function renderAgentPsdTemplate(params: {
sourceLayer.layer.width,
sourceLayer.layer.height
);
const fittedOverlay = await fitOverlayBufferIntoCanvas({
await appendBufferOverlay({
input: overlayBuffer,
left: sourceLayer.layer.x,
top: sourceLayer.layer.y,
width: sourceLayer.layer.width,
height: sourceLayer.layer.height,
canvasWidth,
canvasHeight,
});
if (!fittedOverlay) return;
compositeInputs.push(fittedOverlay);
}
async function appendStoredLayer(layer: AgentPsdTemplate["layers"][number]) {
const sourceUrl = String(layer.sourceUrl || "").trim();
if (!sourceUrl) {
await appendOriginalLayer(layer.layerId);
return;
}
try {
const overlayBuffer = await readCachedBuffer(sourceUrl);
const didAppend = await appendBufferOverlay({
input: overlayBuffer,
left: layer.x,
top: layer.y,
width: layer.width,
height: layer.height,
});
if (didAppend) {
return;
}
} catch (error) {
console.warn("[PSD Template] failed to reuse stored layer asset during render:", {
templateId: template.id,
layerId: layer.layerId,
error,
});
}
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) {
await appendOriginalLayer(layer.layerId);
if (!hasBaseOverlay) {
await appendOriginalLayer(layer.layerId);
}
continue;
}
if (isAgentPsdImageLayerSlot(layer)) {
const assignmentUrl = String(imageAssignments[layer.id] || "").trim();
if (!assignmentUrl) {
await appendOriginalLayer(layer.layerId);
await appendStoredLayer(layer);
continue;
}
@ -390,30 +478,34 @@ export async function renderAgentPsdTemplate(params: {
.toBuffer();
if (layer.maskUrl) {
const maskBuffer = await readCachedBuffer(layer.maskUrl);
overlayBuffer = await sharp(overlayBuffer)
.composite([
{
input: maskBuffer,
blend: "dest-in",
},
])
.png()
.toBuffer();
try {
const maskBuffer = await readCachedBuffer(layer.maskUrl);
overlayBuffer = await sharp(overlayBuffer)
.composite([
{
input: maskBuffer,
blend: "dest-in",
},
])
.png()
.toBuffer();
} catch (error) {
console.warn("[PSD Template] failed to apply layer mask during render:", {
templateId: template.id,
layerId: layer.layerId,
error,
});
}
}
const fittedOverlay = await fitOverlayBufferIntoCanvas({
const didAppend = await appendBufferOverlay({
input: overlayBuffer,
left: layer.x + placementFrame.left,
top: layer.y + placementFrame.top,
width: placementFrame.width,
height: placementFrame.height,
canvasWidth,
canvasHeight,
});
if (!fittedOverlay) continue;
compositeInputs.push(fittedOverlay);
if (!didAppend) continue;
continue;
}
@ -421,12 +513,22 @@ export async function renderAgentPsdTemplate(params: {
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
const defaultText = String(layer.defaultText || "").trim();
if (!text || text === defaultText) {
await appendOriginalLayer(layer.layerId);
await appendStoredLayer(layer);
continue;
}
const font = layer.fontId ? fontMap.get(layer.fontId) : undefined;
const fontBuffer = font?.fileUrl ? await readCachedBuffer(font.fileUrl) : undefined;
const fontBuffer = font?.fileUrl
? await readCachedBuffer(font.fileUrl).catch((error) => {
console.warn("[PSD Template] failed to load render font, falling back to family stack:", {
templateId: template.id,
layerId: layer.layerId,
fontId: font.id,
error,
});
return undefined;
})
: undefined;
const runtimeFont = font && fontBuffer
? await ensurePsdFontAvailableForRender({
font,
@ -463,18 +565,14 @@ export async function renderAgentPsdTemplate(params: {
if (!overlayBuffer) continue;
const fittedOverlay = await fitOverlayBufferIntoCanvas({
const didAppend = await appendBufferOverlay({
input: overlayBuffer,
left: layer.x,
top: layer.y,
width: layer.width,
height: layer.height,
canvasWidth,
canvasHeight,
});
if (!fittedOverlay) continue;
compositeInputs.push(fittedOverlay);
if (!didAppend) continue;
}
}

View File

@ -0,0 +1,159 @@
import "server-only";
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import {
AGENT_PSD_TEMPLATE_ASSET_VERSION,
isAgentPsdImageLayerSlot,
isAgentPsdTextLayerSlot,
type AgentPsdTemplate,
} from "@/lib/agents/psd-template-config";
import {
parsePsdTemplateFileFromUrl,
preparePsdTemplateRenderAssets,
} from "@/lib/agents/psd-template-parser";
import { uploadFile } from "@/lib/storage/service";
function hasMissingLayerAssets(template: AgentPsdTemplate) {
return template.layers.some(
(layer) =>
!String(layer.previewUrl || "").trim() ||
!String(layer.sourceUrl || "").trim() ||
(isAgentPsdImageLayerSlot(layer) &&
layer.isEditable &&
!String(layer.maskUrl || "").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(String(template.renderBasePath || "").trim()) || (
Boolean(renderBaseUrl) &&
renderBaseUrl !== previewUrl
);
}
function toArrayBuffer(buffer: Buffer) {
return buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
) as ArrayBuffer;
}
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 = template.layers;
if (needsFontDetection) {
try {
const parsedTemplate = await parsePsdTemplateFileFromUrl(template.fileUrl);
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,
};
});
} catch (error) {
console.warn("[PSD Template] failed to repair parsed text metadata:", {
templateId: template.id,
error,
});
}
}
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 hasFontRepair = nextLayers.some(
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
);
if (!needsRepair && !hasFontRepair && !needsRenderBaseRepair) {
return template;
}
try {
const preparedAssets = await preparePsdTemplateRenderAssets({
templateId: template.id,
fileUrl: template.fileUrl,
layers: nextLayers,
forceRefreshLayerAssets: needsAssetVersionRefresh,
});
const uploadedRenderBase =
needsRenderBaseRepair
? await uploadFile(
`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;
}
}