ai-chatbot/lib/agents/psd-template-style-resolver.ts
Your Name db46934025
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: stabilize PSD template parsing workflow
2026-04-10 21:03:59 +08:00

190 lines
5.5 KiB
TypeScript

import "server-only";
import { analyzeTextLayerPixels, normalizeHexColor } from "@/lib/agents/psd-text-layout";
import {
isAgentPsdTextLayerSlot,
type AgentPsdTemplate,
type AgentPsdTemplateTextLayerSlot,
} from "@/lib/agents/psd-template-config";
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
import { loadPsdTemplateSource } from "@/lib/agents/psd-template-parser";
function getEstimatedFontSize(height: number) {
return Math.max(16, Math.min(height, Math.round(height * 0.78)));
}
function shouldRepairAutoTextLayer(layer: AgentPsdTemplateTextLayerSlot) {
return layer.styleSource !== "custom";
}
function shouldPreferDetectedFontMetrics(
layer: AgentPsdTemplateTextLayerSlot,
detectedFontSize?: number
) {
if (!Number.isFinite(detectedFontSize) || Number(detectedFontSize) <= 0) {
return false;
}
const safeDetectedFontSize = Number(detectedFontSize);
const fallbackEstimatedFontSize = getEstimatedFontSize(layer.height);
const currentFontSize = Math.max(10, Number(layer.fontSize || 10));
const relativeDiff =
Math.abs(currentFontSize - safeDetectedFontSize) / Math.max(1, safeDetectedFontSize);
return (
shouldRepairAutoTextLayer(layer) &&
(Math.abs(currentFontSize - fallbackEstimatedFontSize) <= 2 || relativeDiff > 0.16)
);
}
async function getPixelAnalysis(
cache: Map<string, Promise<ReturnType<typeof analyzeTextLayerPixels>>>,
layerId: string,
loadRgba: () => Promise<Uint8ClampedArray>,
width: number,
height: number
) {
if (!cache.has(layerId)) {
cache.set(
layerId,
loadRgba()
.then((rgba) => analyzeTextLayerPixels({ rgba, width, height }))
.catch(() => null)
);
}
return cache.get(layerId)!;
}
export async function resolveAgentPsdTemplateTextStyles(template: AgentPsdTemplate) {
const autoTextLayers = template.layers.filter(
(layer): layer is AgentPsdTemplateTextLayerSlot =>
isAgentPsdTextLayerSlot(layer) && shouldRepairAutoTextLayer(layer)
);
if (autoTextLayers.length === 0) {
return template;
}
const source = await loadPsdTemplateSource({
fileUrl: template.fileUrl,
filePath: resolveAgentPsdLocalCachePath({
templateId: template.id,
fileName: template.fileName,
}),
});
const sourceLayerMap = new Map(source.layers.map((entry) => [entry.layer.layerId, entry]));
const pixelAnalysisCache = new Map<
string,
Promise<ReturnType<typeof analyzeTextLayerPixels>>
>();
let didChange = false;
const nextLayers = await Promise.all(
template.layers.map(async (layer) => {
if (!isAgentPsdTextLayerSlot(layer) || !shouldRepairAutoTextLayer(layer)) {
return layer;
}
const sourceEntry = sourceLayerMap.get(layer.layerId);
if (!sourceEntry) {
return layer;
}
let nextLayer = layer;
if (shouldPreferDetectedFontMetrics(layer, sourceEntry.layer.detectedFontSize)) {
nextLayer = {
...nextLayer,
fontSize: Math.max(10, Math.round(Number(sourceEntry.layer.detectedFontSize || nextLayer.fontSize))),
lineHeight:
Number.isFinite(Number(sourceEntry.layer.detectedLineHeight)) &&
Number(sourceEntry.layer.detectedLineHeight) > 0
? Number(sourceEntry.layer.detectedLineHeight)
: nextLayer.lineHeight,
letterSpacing: Number.isFinite(Number(sourceEntry.layer.detectedLetterSpacing))
? Number(sourceEntry.layer.detectedLetterSpacing)
: nextLayer.letterSpacing,
};
}
const needsColorRepair = normalizeHexColor(nextLayer.textColor) === "#111111";
const needsAlignRepair = nextLayer.textAlign === "left";
if (needsColorRepair && sourceEntry.layer.detectedTextColor) {
nextLayer = {
...nextLayer,
textColor: sourceEntry.layer.detectedTextColor,
};
}
if (needsColorRepair || needsAlignRepair) {
const analysis = await getPixelAnalysis(
pixelAnalysisCache,
layer.layerId,
() => sourceEntry.composite(),
sourceEntry.layer.width,
sourceEntry.layer.height
);
if (
analysis?.dominantColor &&
normalizeHexColor(nextLayer.textColor) === "#111111"
) {
nextLayer = {
...nextLayer,
textColor: analysis.dominantColor,
};
}
if (
analysis?.inferredTextAlign &&
needsAlignRepair &&
analysis.inferredTextAlign !== nextLayer.textAlign
) {
nextLayer = {
...nextLayer,
textAlign: analysis.inferredTextAlign,
};
}
if (
Number(nextLayer.paddingX || 0) <= 0 &&
Number.isFinite(Number(analysis?.suggestedPaddingX)) &&
Number(analysis?.suggestedPaddingX) > 0
) {
nextLayer = {
...nextLayer,
paddingX: Math.max(0, Number(analysis?.suggestedPaddingX)),
};
}
if (
Number(nextLayer.paddingY || 0) <= 0 &&
Number.isFinite(Number(analysis?.suggestedPaddingY)) &&
Number(analysis?.suggestedPaddingY) > 0
) {
nextLayer = {
...nextLayer,
paddingY: Math.max(0, Number(analysis?.suggestedPaddingY)),
};
}
}
if (nextLayer !== layer) {
didChange = true;
}
return nextLayer;
})
);
return didChange
? {
...template,
layers: nextLayers,
}
: template;
}