190 lines
5.5 KiB
TypeScript
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;
|
|
}
|