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 { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
import {
|
import {
|
||||||
AGENT_PSD_TEMPLATES_KEY,
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
|
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
type AgentPsdTemplate,
|
type AgentPsdTemplate,
|
||||||
type AgentPsdTemplateLayerSlot,
|
type AgentPsdTemplateLayerSlot,
|
||||||
type AgentPsdTemplateMutationResponse,
|
type AgentPsdTemplateMutationResponse,
|
||||||
@ -356,6 +357,7 @@ export async function POST(
|
|||||||
height: parsedTemplate.height,
|
height: parsedTemplate.height,
|
||||||
layerCount: parsedTemplate.layers.length,
|
layerCount: parsedTemplate.layers.length,
|
||||||
layers: preparedAssets.layers,
|
layers: preparedAssets.layers,
|
||||||
|
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
parseStatus: "completed",
|
parseStatus: "completed",
|
||||||
parseProgress: 100,
|
parseProgress: 100,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
|
|||||||
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
|
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
|
||||||
import {
|
import {
|
||||||
AGENT_PSD_TEMPLATES_KEY,
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
|
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
getEditableAgentPsdTemplateLayers,
|
getEditableAgentPsdTemplateLayers,
|
||||||
isAgentPsdTextLayerSlot,
|
isAgentPsdTextLayerSlot,
|
||||||
normalizeAgentPsdTemplateStatus,
|
normalizeAgentPsdTemplateStatus,
|
||||||
@ -12,10 +13,10 @@ import {
|
|||||||
type AgentPsdTemplateMutationResponse,
|
type AgentPsdTemplateMutationResponse,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
import {
|
import {
|
||||||
parsePsdTemplateFileFromUrl,
|
|
||||||
preparePsdTemplateRenderAssets,
|
preparePsdTemplateRenderAssets,
|
||||||
} from "@/lib/agents/psd-template-parser";
|
} from "@/lib/agents/psd-template-parser";
|
||||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
|
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
import {
|
import {
|
||||||
readAgentPsdTemplates,
|
readAgentPsdTemplates,
|
||||||
@ -40,105 +41,6 @@ function jsonError(status: number, error: string) {
|
|||||||
return NextResponse.json(payload, { status });
|
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(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
context: { params: Promise<{ id: string }> }
|
context: { params: Promise<{ id: string }> }
|
||||||
@ -157,7 +59,7 @@ export async function GET(
|
|||||||
return jsonError(404, "未找到 PSD 模板");
|
return jsonError(404, "未找到 PSD 模板");
|
||||||
}
|
}
|
||||||
|
|
||||||
const repairedTemplate = await repairTemplateLayerPreviewsIfNeeded(template);
|
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
|
||||||
if (repairedTemplate !== template) {
|
if (repairedTemplate !== template) {
|
||||||
await upsertSystemSetting({
|
await upsertSystemSetting({
|
||||||
key: AGENT_PSD_TEMPLATES_KEY,
|
key: AGENT_PSD_TEMPLATES_KEY,
|
||||||
@ -310,6 +212,7 @@ export async function PUT(
|
|||||||
renderBasePath: uploadedRenderBase.pathname,
|
renderBasePath: uploadedRenderBase.pathname,
|
||||||
layers: nextLayers,
|
layers: nextLayers,
|
||||||
layerCount: Math.max(currentTemplate.layerCount, nextLayers.length),
|
layerCount: Math.max(currentTemplate.layerCount, nextLayers.length),
|
||||||
|
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,11 +2,10 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { upsertSystemSetting } from "@/lib/db/queries";
|
import { upsertSystemSetting } from "@/lib/db/queries";
|
||||||
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
|
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 { uploadFile } from "@/lib/storage/service";
|
||||||
import {
|
import {
|
||||||
AGENT_PSD_TEMPLATES_KEY,
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
|
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
normalizeAgentPsdTemplateStatus,
|
normalizeAgentPsdTemplateStatus,
|
||||||
upsertAgentPsdTemplate,
|
upsertAgentPsdTemplate,
|
||||||
type AgentPsdTemplate,
|
type AgentPsdTemplate,
|
||||||
@ -14,10 +13,7 @@ import {
|
|||||||
type AgentPsdTemplateListResponse,
|
type AgentPsdTemplateListResponse,
|
||||||
type AgentPsdTemplateMutationResponse,
|
type AgentPsdTemplateMutationResponse,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
import {
|
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||||
parsePsdTemplateFileFromUrl,
|
|
||||||
preparePsdTemplateRenderAssets,
|
|
||||||
} from "@/lib/agents/psd-template-parser";
|
|
||||||
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
@ -61,107 +57,6 @@ function toArrayBuffer(buffer: Buffer) {
|
|||||||
) as ArrayBuffer;
|
) 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() {
|
export async function GET() {
|
||||||
const session = await requireAgentWorkflowAdmin().catch(() => null);
|
const session = await requireAgentWorkflowAdmin().catch(() => null);
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@ -174,10 +69,7 @@ export async function GET() {
|
|||||||
let hasRepairedTemplate = false;
|
let hasRepairedTemplate = false;
|
||||||
|
|
||||||
for (const template of templates) {
|
for (const template of templates) {
|
||||||
const repairedTemplate = await repairTemplateLayerPreviewsIfNeeded({
|
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
|
||||||
template,
|
|
||||||
updatedBy: session.user.id,
|
|
||||||
});
|
|
||||||
repairedTemplates.push(repairedTemplate);
|
repairedTemplates.push(repairedTemplate);
|
||||||
if (repairedTemplate !== template) {
|
if (repairedTemplate !== template) {
|
||||||
hasRepairedTemplate = true;
|
hasRepairedTemplate = true;
|
||||||
@ -289,6 +181,7 @@ export async function POST(request: NextRequest) {
|
|||||||
height: 1,
|
height: 1,
|
||||||
layerCount: 0,
|
layerCount: 0,
|
||||||
layers: [] as AgentPsdTemplateLayerSlot[],
|
layers: [] as AgentPsdTemplateLayerSlot[],
|
||||||
|
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
uploadedById: session.user.id,
|
uploadedById: session.user.id,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
import { upsertSystemSetting } from "@/lib/db/queries";
|
||||||
import {
|
import {
|
||||||
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
getEditableAgentPsdTemplateLayers,
|
getEditableAgentPsdTemplateLayers,
|
||||||
isAgentPsdImageLayerSlot,
|
isAgentPsdImageLayerSlot,
|
||||||
isAgentPsdTemplateReady,
|
isAgentPsdTemplateReady,
|
||||||
@ -10,6 +12,7 @@ import {
|
|||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
import { normalizeAgentPsdImagePlacement } from "@/lib/agents/psd-image-placement";
|
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 { renderAgentPsdTemplate } from "@/lib/agents/psd-template-renderer";
|
||||||
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
||||||
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
@ -56,19 +59,36 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const templates = await readAgentPsdTemplates();
|
const templates = await readAgentPsdTemplates();
|
||||||
const template = templates.find(
|
const templateIndex = templates.findIndex(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
entry.id === body.templateId &&
|
entry.id === body.templateId &&
|
||||||
entry.status === "active" &&
|
entry.status === "active" &&
|
||||||
isAgentPsdTemplateReady(entry)
|
isAgentPsdTemplateReady(entry)
|
||||||
);
|
);
|
||||||
|
const template = templateIndex >= 0 ? templates[templateIndex] : undefined;
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return jsonError(404, "PSD 模板不存在或未启用");
|
return jsonError(404, "PSD 模板不存在或未启用");
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedTemplate = await resolveAgentPsdTemplateTextStyles(template).catch(
|
const repairedTemplate = await repairAgentPsdTemplateAssetsIfNeeded(template);
|
||||||
() => 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);
|
const editableLayers = getEditableAgentPsdTemplateLayers(resolvedTemplate);
|
||||||
if (editableLayers.length === 0) {
|
if (editableLayers.length === 0) {
|
||||||
@ -139,6 +159,10 @@ export async function POST(request: NextRequest) {
|
|||||||
} satisfies AgentPsdTemplateRenderResponse);
|
} satisfies AgentPsdTemplateRenderResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[PSD Templates] render failed:", 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 { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { upsertSystemSetting } from "@/lib/db/queries";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import {
|
import {
|
||||||
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
getEditableAgentPsdTemplateLayers,
|
getEditableAgentPsdTemplateLayers,
|
||||||
isAgentPsdTextLayerSlot,
|
isAgentPsdTextLayerSlot,
|
||||||
isAgentPsdTemplateReady,
|
isAgentPsdTemplateReady,
|
||||||
type AgentPsdTemplateListResponse,
|
type AgentPsdTemplateListResponse,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
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 { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
||||||
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
|
|
||||||
@ -30,12 +33,49 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const requestedBrand = String(request.nextUrl.searchParams.get("brand") || "").trim();
|
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(
|
const fonts = (await readAgentPsdFonts().catch(
|
||||||
() => [] as Awaited<ReturnType<typeof readAgentPsdFonts>>
|
() => [] as Awaited<ReturnType<typeof readAgentPsdFonts>>
|
||||||
)) as Awaited<ReturnType<typeof readAgentPsdFonts>>;
|
)) as Awaited<ReturnType<typeof readAgentPsdFonts>>;
|
||||||
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
||||||
const templates = await Promise.all(
|
const templates = await Promise.all(
|
||||||
(await readAgentPsdTemplates())
|
sourceTemplates
|
||||||
.filter((template) => template.status === "active")
|
.filter((template) => template.status === "active")
|
||||||
.filter(isAgentPsdTemplateReady)
|
.filter(isAgentPsdTemplateReady)
|
||||||
.filter((template) =>
|
.filter((template) =>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
|
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
|
||||||
|
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 3;
|
||||||
|
|
||||||
export type AgentPsdTemplateStatus = "active" | "disabled";
|
export type AgentPsdTemplateStatus = "active" | "disabled";
|
||||||
export type AgentPsdTemplateParseStatus =
|
export type AgentPsdTemplateParseStatus =
|
||||||
@ -88,6 +89,7 @@ export interface AgentPsdTemplate {
|
|||||||
height: number;
|
height: number;
|
||||||
layerCount: number;
|
layerCount: number;
|
||||||
layers: AgentPsdTemplateLayerSlot[];
|
layers: AgentPsdTemplateLayerSlot[];
|
||||||
|
assetVersion?: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
uploadedById?: string;
|
uploadedById?: string;
|
||||||
@ -399,6 +401,7 @@ export function parseAgentPsdTemplates(value: unknown) {
|
|||||||
height,
|
height,
|
||||||
layerCount,
|
layerCount,
|
||||||
layers,
|
layers,
|
||||||
|
assetVersion: normalizePositiveInteger(record.assetVersion),
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
uploadedById: normalizeText(record.uploadedById) || undefined,
|
uploadedById: normalizeText(record.uploadedById) || undefined,
|
||||||
|
|||||||
@ -502,6 +502,7 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
templateId: string;
|
templateId: string;
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
layers: AgentPsdTemplateLayerSlot[];
|
layers: AgentPsdTemplateLayerSlot[];
|
||||||
|
forceRefreshLayerAssets?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const psdBuffer = await fetchRemoteBuffer(params.fileUrl);
|
const psdBuffer = await fetchRemoteBuffer(params.fileUrl);
|
||||||
const psd = await parsePsdBuffer(psdBuffer);
|
const psd = await parsePsdBuffer(psdBuffer);
|
||||||
@ -520,9 +521,9 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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({
|
const thumbnailBuffer = await createLayerThumbnailBuffer({
|
||||||
rgba,
|
rgba,
|
||||||
width: entry.layer.width,
|
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 sourceBuffer = await rgbaToPngBuffer(rgba, entry.layer.width, entry.layer.height);
|
||||||
const uploadedSource = await uploadFile(
|
const uploadedSource = await uploadFile(
|
||||||
`agent-psd-layer-source-${params.templateId}-${sanitizeFileSegment(targetLayer.layerId, targetLayer.id)}.png`,
|
`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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -305,12 +305,13 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
}) {
|
}) {
|
||||||
const { template, imageAssignments, imagePlacements, textAssignments, fonts } = params;
|
const { template, imageAssignments, imagePlacements, textAssignments, fonts } = params;
|
||||||
const sharp = await getSharp();
|
const sharp = await getSharp();
|
||||||
const source = await loadPsdTemplateSourceFromUrl(template.fileUrl);
|
const canvasWidth = Math.max(1, Math.round(Number(template.width || 1)));
|
||||||
const canvasWidth = Math.max(1, Math.round(Number(source.width || template.width || 1)));
|
const canvasHeight = Math.max(1, Math.round(Number(template.height || 1)));
|
||||||
const canvasHeight = Math.max(1, Math.round(Number(source.height || template.height || 1)));
|
|
||||||
const remoteBufferCache = new Map<string, Buffer>();
|
const remoteBufferCache = new Map<string, Buffer>();
|
||||||
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
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) {
|
async function readCachedBuffer(url: string) {
|
||||||
const normalizedUrl = String(url || "").trim();
|
const normalizedUrl = String(url || "").trim();
|
||||||
@ -325,9 +326,44 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
return remoteBufferCache.get(normalizedUrl)!;
|
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 }> = [];
|
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) {
|
async function appendOriginalLayer(layerId: string) {
|
||||||
|
const sourceLayerMap = await getSourceLayerMap();
|
||||||
const sourceLayer = sourceLayerMap.get(layerId);
|
const sourceLayer = sourceLayerMap.get(layerId);
|
||||||
if (!sourceLayer) return;
|
if (!sourceLayer) return;
|
||||||
const rgba = await sourceLayer.composite();
|
const rgba = await sourceLayer.composite();
|
||||||
@ -336,29 +372,81 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
sourceLayer.layer.width,
|
sourceLayer.layer.width,
|
||||||
sourceLayer.layer.height
|
sourceLayer.layer.height
|
||||||
);
|
);
|
||||||
const fittedOverlay = await fitOverlayBufferIntoCanvas({
|
await appendBufferOverlay({
|
||||||
input: overlayBuffer,
|
input: overlayBuffer,
|
||||||
left: sourceLayer.layer.x,
|
left: sourceLayer.layer.x,
|
||||||
top: sourceLayer.layer.y,
|
top: sourceLayer.layer.y,
|
||||||
width: sourceLayer.layer.width,
|
width: sourceLayer.layer.width,
|
||||||
height: sourceLayer.layer.height,
|
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()) {
|
for (const layer of [...template.layers].reverse()) {
|
||||||
if (!layer.isEditable) {
|
if (!layer.isEditable) {
|
||||||
await appendOriginalLayer(layer.layerId);
|
if (!hasBaseOverlay) {
|
||||||
|
await appendOriginalLayer(layer.layerId);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAgentPsdImageLayerSlot(layer)) {
|
if (isAgentPsdImageLayerSlot(layer)) {
|
||||||
const assignmentUrl = String(imageAssignments[layer.id] || "").trim();
|
const assignmentUrl = String(imageAssignments[layer.id] || "").trim();
|
||||||
if (!assignmentUrl) {
|
if (!assignmentUrl) {
|
||||||
await appendOriginalLayer(layer.layerId);
|
await appendStoredLayer(layer);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,30 +478,34 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
if (layer.maskUrl) {
|
if (layer.maskUrl) {
|
||||||
const maskBuffer = await readCachedBuffer(layer.maskUrl);
|
try {
|
||||||
overlayBuffer = await sharp(overlayBuffer)
|
const maskBuffer = await readCachedBuffer(layer.maskUrl);
|
||||||
.composite([
|
overlayBuffer = await sharp(overlayBuffer)
|
||||||
{
|
.composite([
|
||||||
input: maskBuffer,
|
{
|
||||||
blend: "dest-in",
|
input: maskBuffer,
|
||||||
},
|
blend: "dest-in",
|
||||||
])
|
},
|
||||||
.png()
|
])
|
||||||
.toBuffer();
|
.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,
|
input: overlayBuffer,
|
||||||
left: layer.x + placementFrame.left,
|
left: layer.x + placementFrame.left,
|
||||||
top: layer.y + placementFrame.top,
|
top: layer.y + placementFrame.top,
|
||||||
width: placementFrame.width,
|
width: placementFrame.width,
|
||||||
height: placementFrame.height,
|
height: placementFrame.height,
|
||||||
canvasWidth,
|
|
||||||
canvasHeight,
|
|
||||||
});
|
});
|
||||||
if (!fittedOverlay) continue;
|
if (!didAppend) continue;
|
||||||
|
|
||||||
compositeInputs.push(fittedOverlay);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,12 +513,22 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
|
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
|
||||||
const defaultText = String(layer.defaultText || "").trim();
|
const defaultText = String(layer.defaultText || "").trim();
|
||||||
if (!text || text === defaultText) {
|
if (!text || text === defaultText) {
|
||||||
await appendOriginalLayer(layer.layerId);
|
await appendStoredLayer(layer);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const font = layer.fontId ? fontMap.get(layer.fontId) : undefined;
|
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
|
const runtimeFont = font && fontBuffer
|
||||||
? await ensurePsdFontAvailableForRender({
|
? await ensurePsdFontAvailableForRender({
|
||||||
font,
|
font,
|
||||||
@ -463,18 +565,14 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
|
|
||||||
if (!overlayBuffer) continue;
|
if (!overlayBuffer) continue;
|
||||||
|
|
||||||
const fittedOverlay = await fitOverlayBufferIntoCanvas({
|
const didAppend = await appendBufferOverlay({
|
||||||
input: overlayBuffer,
|
input: overlayBuffer,
|
||||||
left: layer.x,
|
left: layer.x,
|
||||||
top: layer.y,
|
top: layer.y,
|
||||||
width: layer.width,
|
width: layer.width,
|
||||||
height: layer.height,
|
height: layer.height,
|
||||||
canvasWidth,
|
|
||||||
canvasHeight,
|
|
||||||
});
|
});
|
||||||
if (!fittedOverlay) continue;
|
if (!didAppend) continue;
|
||||||
|
|
||||||
compositeInputs.push(fittedOverlay);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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