377 lines
11 KiB
TypeScript
377 lines
11 KiB
TypeScript
import "server-only";
|
|
|
|
import type { AgentPsdFont } from "@/lib/agents/psd-font-config";
|
|
import {
|
|
doesPsdFontMatchDetectedNames,
|
|
findBestMatchingPsdFont,
|
|
} from "@/lib/agents/psd-font-matching";
|
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
|
import {
|
|
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
|
hasAgentPsdTemplateRenderSegments,
|
|
isAgentPsdImageLayerSlot,
|
|
isAgentPsdLayerSuperLocked,
|
|
isAgentPsdTemplateLayerUserEditable,
|
|
isAgentPsdTextLayerSlot,
|
|
type AgentPsdTemplate,
|
|
} from "@/lib/agents/psd-template-config";
|
|
import {
|
|
isPsdAdjustmentLikeImageLayer,
|
|
pickPrimaryPsdImageLayerId,
|
|
} from "@/lib/agents/psd-template-image-slot-selection";
|
|
import {
|
|
parsePsdTemplateFileFromSource,
|
|
preparePsdTemplateRenderAssets,
|
|
uploadPreparedPsdTemplateRenderSegments,
|
|
} from "@/lib/agents/psd-template-parser";
|
|
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
|
|
import { createStorageUploadSession } from "@/lib/storage/service";
|
|
|
|
function hasMissingLayerAssets(template: AgentPsdTemplate) {
|
|
const hasRenderBase = hasDedicatedRenderBase(template);
|
|
|
|
return template.layers.some(
|
|
(layer) => {
|
|
if (
|
|
isAgentPsdTemplateLayerUserEditable(layer) &&
|
|
!String(layer.previewUrl || "").trim()
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (hasRenderBase) {
|
|
return (
|
|
isAgentPsdImageLayerSlot(layer) &&
|
|
isAgentPsdTemplateLayerUserEditable(layer) &&
|
|
!String(layer.sourceUrl || "").trim()
|
|
);
|
|
}
|
|
|
|
return !String(layer.sourceUrl || "").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(renderBaseUrl) && renderBaseUrl !== previewUrl;
|
|
}
|
|
|
|
function hasStaticTemplateLayers(template: AgentPsdTemplate) {
|
|
return template.layers.some((layer) => !isAgentPsdTemplateLayerUserEditable(layer));
|
|
}
|
|
|
|
function repairEditableImageLayerSelection(template: AgentPsdTemplate) {
|
|
const imageLayers = template.layers.filter(isAgentPsdImageLayerSlot);
|
|
const unlockedImageLayers = imageLayers.filter(
|
|
(layer) => !isAgentPsdLayerSuperLocked(layer)
|
|
);
|
|
if (unlockedImageLayers.length === 0) {
|
|
return template.layers;
|
|
}
|
|
|
|
const editableImageLayers = unlockedImageLayers.filter(
|
|
isAgentPsdTemplateLayerUserEditable
|
|
);
|
|
const defaultImageLayer = unlockedImageLayers.find(
|
|
(layer) =>
|
|
isAgentPsdTemplateLayerUserEditable(layer) && layer.isDefaultResultSource
|
|
);
|
|
const shouldAutoRepairSelection =
|
|
editableImageLayers.length === 0 ||
|
|
!defaultImageLayer ||
|
|
(editableImageLayers.length === 1 && isPsdAdjustmentLikeImageLayer(editableImageLayers[0]));
|
|
|
|
if (!shouldAutoRepairSelection) {
|
|
return template.layers;
|
|
}
|
|
|
|
const nextPrimaryLayerId = pickPrimaryPsdImageLayerId({
|
|
imageLayers: unlockedImageLayers,
|
|
canvasWidth: template.width,
|
|
canvasHeight: template.height,
|
|
});
|
|
|
|
if (!nextPrimaryLayerId) {
|
|
return template.layers;
|
|
}
|
|
|
|
const currentPrimaryLayerId = defaultImageLayer?.id || editableImageLayers[0]?.id || "";
|
|
if (currentPrimaryLayerId === nextPrimaryLayerId) {
|
|
return template.layers;
|
|
}
|
|
|
|
return template.layers.map((layer) => {
|
|
if (!isAgentPsdImageLayerSlot(layer)) {
|
|
return layer;
|
|
}
|
|
|
|
if (layer.id === nextPrimaryLayerId) {
|
|
return {
|
|
...layer,
|
|
isEditable: true,
|
|
isDefaultResultSource: true,
|
|
allowUserUploadOverride: true,
|
|
};
|
|
}
|
|
|
|
if (layer.id === currentPrimaryLayerId) {
|
|
return {
|
|
...layer,
|
|
isEditable: false,
|
|
isDefaultResultSource: false,
|
|
allowUserUploadOverride: false,
|
|
};
|
|
}
|
|
|
|
return layer;
|
|
});
|
|
}
|
|
|
|
function toArrayBuffer(buffer: Buffer) {
|
|
return buffer.buffer.slice(
|
|
buffer.byteOffset,
|
|
buffer.byteOffset + buffer.byteLength
|
|
) as ArrayBuffer;
|
|
}
|
|
|
|
export function repairAgentPsdTemplateFontBindings(
|
|
template: AgentPsdTemplate,
|
|
fonts: AgentPsdFont[]
|
|
) {
|
|
if (fonts.length === 0) {
|
|
return template;
|
|
}
|
|
|
|
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
|
let didChange = false;
|
|
|
|
const nextLayers = template.layers.map((layer) => {
|
|
if (!isAgentPsdTextLayerSlot(layer) || layer.styleSource === "custom") {
|
|
return layer;
|
|
}
|
|
|
|
const detectedFontNames = [
|
|
...((layer.detectedFontNames || []).filter(Boolean)),
|
|
...(layer.detectedFontName ? [layer.detectedFontName] : []),
|
|
];
|
|
const preferredDetectedFontName =
|
|
detectedFontNames.find((entry) => String(entry || "").trim()) || "";
|
|
const currentFont = layer.fontId ? fontMap.get(layer.fontId) : undefined;
|
|
const currentFontMatches = currentFont
|
|
? doesPsdFontMatchDetectedNames({
|
|
detectedFontNames,
|
|
font: currentFont,
|
|
})
|
|
: false;
|
|
const matchedFont = findBestMatchingPsdFont({
|
|
detectedFontNames,
|
|
fonts,
|
|
});
|
|
|
|
if (
|
|
matchedFont?.confidence === "high" &&
|
|
(!currentFontMatches || !layer.fontId)
|
|
) {
|
|
didChange = true;
|
|
return {
|
|
...layer,
|
|
fontId: matchedFont.font.id,
|
|
fontFamily: matchedFont.font.family,
|
|
fontLabel: matchedFont.font.name,
|
|
fontFileUrl: matchedFont.font.fileUrl,
|
|
fontContentType: matchedFont.font.contentType,
|
|
};
|
|
}
|
|
|
|
if (
|
|
!matchedFont &&
|
|
!currentFontMatches &&
|
|
(
|
|
layer.fontId ||
|
|
layer.fontFileUrl ||
|
|
layer.fontContentType ||
|
|
(preferredDetectedFontName &&
|
|
(
|
|
layer.fontFamily !== preferredDetectedFontName ||
|
|
layer.fontLabel !== preferredDetectedFontName
|
|
))
|
|
)
|
|
) {
|
|
didChange = true;
|
|
return {
|
|
...layer,
|
|
fontId: undefined,
|
|
fontFamily: preferredDetectedFontName || undefined,
|
|
fontLabel: preferredDetectedFontName || undefined,
|
|
fontFileUrl: undefined,
|
|
fontContentType: undefined,
|
|
};
|
|
}
|
|
|
|
if (currentFont && (
|
|
layer.fontFamily !== currentFont.family ||
|
|
layer.fontLabel !== currentFont.name ||
|
|
layer.fontFileUrl !== currentFont.fileUrl ||
|
|
layer.fontContentType !== currentFont.contentType
|
|
)) {
|
|
didChange = true;
|
|
return {
|
|
...layer,
|
|
fontFamily: currentFont.family,
|
|
fontLabel: currentFont.name,
|
|
fontFileUrl: currentFont.fileUrl,
|
|
fontContentType: currentFont.contentType,
|
|
};
|
|
}
|
|
|
|
return layer;
|
|
});
|
|
|
|
if (!didChange) {
|
|
return template;
|
|
}
|
|
|
|
return {
|
|
...template,
|
|
layers: nextLayers,
|
|
updatedAt: new Date().toISOString(),
|
|
} satisfies AgentPsdTemplate;
|
|
}
|
|
|
|
export async function repairAgentPsdTemplateAssetsIfNeeded(
|
|
template: AgentPsdTemplate
|
|
) {
|
|
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 needsRenderSegmentRepair =
|
|
hasStaticTemplateLayers(template) && !hasAgentPsdTemplateRenderSegments(template);
|
|
const fonts = await readAgentPsdFonts().catch(() => []);
|
|
const needsFontDetection = needsTextMetadataRepair(template);
|
|
|
|
let nextLayers = repairEditableImageLayerSelection(template);
|
|
if (needsFontDetection) {
|
|
try {
|
|
const localFilePath = resolveAgentPsdLocalCachePath({
|
|
templateId: template.id,
|
|
fileName: template.fileName,
|
|
});
|
|
const parsedTemplate = await parsePsdTemplateFileFromSource({
|
|
fileUrl: template.fileUrl,
|
|
filePath: localFilePath,
|
|
});
|
|
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,
|
|
textColor:
|
|
layer.styleSource !== "custom" &&
|
|
(!String(layer.textColor || "").trim() || layer.textColor === "#111111") &&
|
|
parsedLayer.detectedTextColor
|
|
? parsedLayer.detectedTextColor
|
|
: layer.textColor,
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.warn("[PSD Template] failed to repair parsed text metadata:", {
|
|
templateId: template.id,
|
|
error,
|
|
});
|
|
}
|
|
}
|
|
|
|
const fontRepairedTemplate = repairAgentPsdTemplateFontBindings(
|
|
{
|
|
...template,
|
|
layers: nextLayers,
|
|
},
|
|
fonts
|
|
);
|
|
nextLayers = fontRepairedTemplate.layers;
|
|
|
|
const hasFontRepair = nextLayers.some(
|
|
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
|
|
);
|
|
|
|
if (
|
|
!needsRepair &&
|
|
!hasFontRepair &&
|
|
!needsRenderBaseRepair &&
|
|
!needsRenderSegmentRepair
|
|
) {
|
|
return template;
|
|
}
|
|
|
|
try {
|
|
const uploadAsset = await createStorageUploadSession();
|
|
const preparedAssets = await preparePsdTemplateRenderAssets({
|
|
templateId: template.id,
|
|
fileUrl: template.fileUrl,
|
|
filePath: resolveAgentPsdLocalCachePath({
|
|
templateId: template.id,
|
|
fileName: template.fileName,
|
|
}),
|
|
layers: nextLayers,
|
|
forceRefreshLayerAssets: needsAssetVersionRefresh,
|
|
uploadAsset,
|
|
});
|
|
const uploadedRenderBase =
|
|
needsRenderBaseRepair
|
|
? await uploadAsset(
|
|
`agent-psd-base-${template.id}-${Date.now()}.png`,
|
|
toArrayBuffer(preparedAssets.renderBaseBuffer)
|
|
)
|
|
: null;
|
|
const uploadedRenderSegments =
|
|
needsRenderSegmentRepair || needsAssetVersionRefresh
|
|
? await uploadPreparedPsdTemplateRenderSegments({
|
|
templateId: template.id,
|
|
segments: preparedAssets.renderSegments,
|
|
uploadAsset,
|
|
})
|
|
: null;
|
|
|
|
return {
|
|
...template,
|
|
renderBaseUrl: uploadedRenderBase?.url || template.renderBaseUrl,
|
|
renderBasePath: uploadedRenderBase?.pathname || template.renderBasePath,
|
|
renderSegments: uploadedRenderSegments || template.renderSegments,
|
|
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;
|
|
}
|
|
}
|