fix: harden psd template rendering
This commit is contained in:
parent
494a3c6d48
commit
fb6b2170bd
@ -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,
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
159
lib/agents/psd-template-repair.ts
Normal file
159
lib/agents/psd-template-repair.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user