fix: improve psd text font recovery
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-10 20:19:21 +08:00
parent aa885a3d48
commit 9b745e5591
7 changed files with 180 additions and 34 deletions

View File

@ -91,6 +91,7 @@ function buildInitialTemplateLayers(params: {
detectedFontNames: layer.detectedFontNames || [],
fonts,
});
const autoBoundFont = matchedFont?.confidence === "high" ? matchedFont.font : undefined;
const isEditable = shouldAutoEditableTextLayer(layer);
return {
@ -102,17 +103,17 @@ function buildInitialTemplateLayers(params: {
placeholder: layer.defaultText || layer.layerName,
detectedFontName: layer.detectedFontName,
detectedFontNames: layer.detectedFontNames,
fontId: matchedFont?.font.id,
fontId: autoBoundFont?.id,
fontFamily:
matchedFont?.font.family ||
autoBoundFont?.family ||
layer.detectedFontNames?.[0] ||
layer.detectedFontName,
fontLabel:
matchedFont?.font.name ||
autoBoundFont?.name ||
layer.detectedFontNames?.[0] ||
layer.detectedFontName,
fontFileUrl: matchedFont?.font.fileUrl,
fontContentType: matchedFont?.font.contentType,
fontFileUrl: autoBoundFont?.fileUrl,
fontContentType: autoBoundFont?.contentType,
fontSize: Math.max(
10,
Math.round(
@ -130,7 +131,7 @@ function buildInitialTemplateLayers(params: {
? Number(layer.detectedLetterSpacing)
: 0,
fontWeight: 500,
textColor: "#111111",
textColor: layer.detectedTextColor || "#111111",
textAlign: "left",
paddingX: 0,
paddingY: 0,

View File

@ -169,7 +169,7 @@ export async function PUT(
],
fonts,
});
if (matchedFont) {
if (matchedFont?.confidence === "high") {
layer.fontId = matchedFont.font.id;
layer.fontFamily = matchedFont.font.family;
layer.fontLabel = matchedFont.font.name;

View File

@ -27,6 +27,7 @@ import {
normalizeAgentPsdTemplateCategoryTagConfig,
} from "@/lib/agents/psd-template-category-tag-config";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair";
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
export const runtime = "nodejs";
@ -92,6 +93,7 @@ export async function GET(request: NextRequest) {
)
.filter((template) => (requestedMode ? template.mode === requestedMode : true))
.filter((template) => getEditableAgentPsdTemplateLayers(template).length > 0)
.map((template) => repairAgentPsdTemplateFontBindings(template, fonts))
.map((template) => ({
...template,
layers: template.layers.map((layer) => {

View File

@ -32,6 +32,31 @@ export function normalizeDetectedFontNames(value: unknown) {
);
}
function getPsdFontIdentityTokens(font: Pick<AgentPsdFont, "name" | "family">) {
return Array.from(
new Set(
[font.name, font.family]
.map((item) => normalizeFontToken(String(item || "")))
.filter(Boolean)
)
);
}
export function doesPsdFontMatchDetectedNames(params: {
detectedFontNames: string[];
font: Pick<AgentPsdFont, "name" | "family">;
}) {
const detectedFontNames = normalizeDetectedFontNames(params.detectedFontNames);
if (detectedFontNames.length === 0) {
return true;
}
const fontTokens = new Set(getPsdFontIdentityTokens(params.font));
return detectedFontNames.some((detectedFontName) =>
fontTokens.has(normalizeFontToken(detectedFontName))
);
}
export function findBestMatchingPsdFont(params: {
detectedFontNames: string[];
fonts: AgentPsdFont[];

View File

@ -15,6 +15,11 @@ type PsdFontSetEntry = {
Name?: string;
};
type PsdTextStyleColorValue = {
Values?: number[];
Type?: number;
};
type PsdTextStyleRun = {
StyleSheet?: {
StyleSheetData?: {
@ -23,6 +28,7 @@ type PsdTextStyleRun = {
Leading?: number;
AutoLeading?: number | boolean;
Tracking?: number;
FillColor?: PsdTextStyleColorValue;
};
};
};
@ -87,6 +93,7 @@ export interface ParsedPsdTemplateLayer {
detectedFontSize?: number;
detectedLineHeight?: number;
detectedLetterSpacing?: number;
detectedTextColor?: string;
}
export interface ParsedPsdTemplatePayload {
@ -277,6 +284,30 @@ function extractTextTransformScale(additionalProperties?: Record<string, unknown
};
}
function extractHexColorFromTextStyle(value?: PsdTextStyleColorValue) {
const channels = Array.isArray(value?.Values) ? value.Values : [];
if (channels.length < 4) {
return undefined;
}
const red = Number(channels[1]);
const green = Number(channels[2]);
const blue = Number(channels[3]);
if (![red, green, blue].every((channel) => Number.isFinite(channel))) {
return undefined;
}
return normalizeHexColor(
`#${Math.round(Math.max(0, Math.min(1, red)) * 255)
.toString(16)
.padStart(2, "0")}${Math.round(Math.max(0, Math.min(1, green)) * 255)
.toString(16)
.padStart(2, "0")}${Math.round(Math.max(0, Math.min(1, blue)) * 255)
.toString(16)
.padStart(2, "0")}`
);
}
function extractTextStyleMetrics(
textProperties?: PsdTextPropertiesLike,
additionalProperties?: Record<string, unknown>
@ -286,8 +317,12 @@ function extractTextStyleMetrics(
const style = styleRun?.StyleSheet?.StyleSheetData;
return Number.isFinite(Number(style?.FontSize));
});
const firstColorRun = styleRuns.find((styleRun) =>
Boolean(extractHexColorFromTextStyle(styleRun?.StyleSheet?.StyleSheetData?.FillColor))
);
const style = firstValidRun?.StyleSheet?.StyleSheetData;
const colorStyle = firstColorRun?.StyleSheet?.StyleSheetData;
const fontSizeValue = Number(style?.FontSize);
const leadingValue = Number(style?.Leading);
const trackingValue = Number(style?.Tracking);
@ -312,6 +347,7 @@ function extractTextStyleMetrics(
detectedFontSize,
detectedLineHeight,
detectedLetterSpacing,
detectedTextColor: extractHexColorFromTextStyle(colorStyle?.FillColor),
};
}
@ -364,6 +400,7 @@ function collectPsdLayerEntries(
detectedFontSize: undefined,
detectedLineHeight: undefined,
detectedLetterSpacing: undefined,
detectedTextColor: undefined,
};
result.push({
@ -385,6 +422,7 @@ function collectPsdLayerEntries(
detectedFontSize: textStyleMetrics.detectedFontSize,
detectedLineHeight: textStyleMetrics.detectedLineHeight,
detectedLetterSpacing: textStyleMetrics.detectedLetterSpacing,
detectedTextColor: textStyleMetrics.detectedTextColor,
},
});
}

View File

@ -1,6 +1,10 @@
import "server-only";
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
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,
@ -111,6 +115,83 @@ function toArrayBuffer(buffer: Buffer) {
) 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 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 (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
) {
@ -142,6 +223,12 @@ export async function repairAgentPsdTemplateAssetsIfNeeded(
...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) {
@ -152,31 +239,14 @@ export async function repairAgentPsdTemplateAssetsIfNeeded(
}
}
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 fontRepairedTemplate = repairAgentPsdTemplateFontBindings(
{
...template,
layers: nextLayers,
},
fonts
);
nextLayers = fontRepairedTemplate.layers;
const hasFontRepair = nextLayers.some(
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])

View File

@ -105,6 +105,13 @@ export async function resolveAgentPsdTemplateTextStyles(template: AgentPsdTempla
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,
@ -114,7 +121,10 @@ export async function resolveAgentPsdTemplateTextStyles(template: AgentPsdTempla
sourceEntry.layer.height
);
if (analysis?.dominantColor && needsColorRepair) {
if (
analysis?.dominantColor &&
normalizeHexColor(nextLayer.textColor) === "#111111"
) {
nextLayer = {
...nextLayer,
textColor: analysis.dominantColor,