fix: preserve psd text and layer exports
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-01 22:50:24 +08:00
parent fb6b2170bd
commit bc432ffb44
6 changed files with 113 additions and 38 deletions

View File

@ -147,8 +147,14 @@ function buildInitialTemplateLayers(params: {
detectedFontName: layer.detectedFontName,
detectedFontNames: layer.detectedFontNames,
fontId: matchedFont?.font.id,
fontFamily: matchedFont?.font.family,
fontLabel: matchedFont?.font.name,
fontFamily:
matchedFont?.font.family ||
layer.detectedFontNames?.[0] ||
layer.detectedFontName,
fontLabel:
matchedFont?.font.name ||
layer.detectedFontNames?.[0] ||
layer.detectedFontName,
fontFileUrl: matchedFont?.font.fileUrl,
fontContentType: matchedFont?.font.contentType,
fontSize: Math.max(

View File

@ -133,18 +133,58 @@ function buildPreviewFontFaces(template: AgentPsdTemplate | null) {
if (!isAgentPsdTextLayerSlot(layer) || !layer.fontFamily || !layer.fontFileUrl) {
continue;
}
if (rules.has(layer.fontFamily)) {
continue;
}
rules.set(
layer.fontFamily,
`@font-face { font-family: "${layer.fontFamily}"; src: url("${layer.fontFileUrl}") format("${layer.fontContentType?.includes("woff2") ? "woff2" : layer.fontContentType?.includes("woff") ? "woff" : layer.fontContentType?.includes("otf") ? "opentype" : "truetype"}"); font-display: swap; }`
const familyAliases = Array.from(
new Set(
[
layer.fontFamily,
...(layer.detectedFontNames || []),
layer.detectedFontName,
]
.map((family) => String(family || "").trim())
.filter(Boolean)
)
);
for (const familyAlias of familyAliases) {
if (rules.has(familyAlias)) {
continue;
}
rules.set(
familyAlias,
`@font-face { font-family: "${familyAlias}"; src: url("${layer.fontFileUrl}") format("${layer.fontContentType?.includes("woff2") ? "woff2" : layer.fontContentType?.includes("woff") ? "woff" : layer.fontContentType?.includes("otf") ? "opentype" : "truetype"}"); font-display: swap; }`
);
}
}
return Array.from(rules.values()).join("\n");
}
function buildTextLayerFontFamily(layer: AgentPsdTemplateTextLayerSlot) {
const families = Array.from(
new Set(
[
layer.fontFamily,
...(layer.detectedFontNames || []),
layer.detectedFontName,
"PingFang SC",
"Microsoft YaHei",
"Noto Sans SC",
"Source Han Sans SC",
"sans-serif",
]
.map((family) => String(family || "").trim())
.filter(Boolean)
)
);
return families
.map((family) =>
family === "sans-serif" ? family : `"${family.replace(/"/g, '\\"')}"`
)
.join(", ");
}
function getPreferredSelectedLayerId(template: AgentPsdTemplate | null) {
if (!template || template.layers.length === 0) {
return "";
@ -205,12 +245,13 @@ function getTextLayerContentStyle(
paddingX: layer.paddingX,
paddingY: layer.paddingY,
maxLines: layer.maxLines,
autoFit: false,
});
return {
width: "100%",
height: "100%",
fontFamily: layer.fontFamily || "sans-serif",
fontFamily: buildTextLayerFontFamily(layer),
fontSize: resolvedLayout.fontSize,
lineHeight: layer.lineHeight,
letterSpacing: `${layer.letterSpacing}px`,

View File

@ -601,6 +601,17 @@ export async function preparePsdTemplateRenderAssets(params: {
};
}
if (
currentLayer.paddingY <= 0 &&
Number.isFinite(Number(analysis.suggestedPaddingY)) &&
Number(analysis.suggestedPaddingY) > 0
) {
nextLayer = {
...nextLayer,
paddingY: Math.max(0, Number(analysis.suggestedPaddingY)),
};
}
if (nextLayer !== currentLayer) {
nextLayers[layerIndex] = nextLayer;
}

View File

@ -233,6 +233,7 @@ async function createTextOverlayBuffer(params: {
paddingX,
paddingY,
maxLines,
autoFit: false,
});
const adjustedFontSize = resolvedLayout.fontSize;
const lineHeightPx = resolvedLayout.lineHeightPx;
@ -411,35 +412,9 @@ export async function renderAgentPsdTemplate(params: {
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()) {
if (!layer.isEditable) {
if (!hasBaseOverlay) {
await appendOriginalLayer(layer.layerId);
}
await appendStoredLayer(layer);
continue;
}
@ -542,14 +517,22 @@ export async function renderAgentPsdTemplate(params: {
text,
fontBuffer,
font,
fontFamily: layer.fontFamily || font?.family || "sans-serif",
fontFamily:
layer.fontFamily ||
layer.detectedFontNames?.[0] ||
layer.detectedFontName ||
font?.family ||
"sans-serif",
renderFontFamilies: runtimeFont
? [
runtimeFont.fontPostscriptName,
runtimeFont.fontFullName,
runtimeFont.fontFamily,
].filter((value): value is string => Boolean(String(value || "").trim()))
: undefined,
: [
...(layer.detectedFontNames || []),
...(layer.detectedFontName ? [layer.detectedFontName] : []),
].filter((value): value is string => Boolean(String(value || "").trim())),
renderFontFamily: runtimeFont?.fontFamily,
useExactFontFace: Boolean(runtimeFont?.fontPostscriptName || runtimeFont?.fontFullName),
fontSize: layer.fontSize,

View File

@ -142,6 +142,17 @@ export async function resolveAgentPsdTemplateTextStyles(template: AgentPsdTempla
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) {

View File

@ -91,6 +91,7 @@ export function resolveTextLayout(params: {
paddingX: number;
paddingY: number;
maxLines: number;
autoFit?: boolean;
}) {
const {
text,
@ -102,6 +103,7 @@ export function resolveTextLayout(params: {
paddingX,
paddingY,
maxLines,
autoFit = true,
} = params;
const normalizedText = String(text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
@ -114,6 +116,23 @@ export function resolveTextLayout(params: {
let bestLines: string[] = [];
let bestLineHeightPx = Math.max(fittedFontSize, fittedFontSize * resolvedLineHeight);
if (!autoFit) {
bestLines = wrapTextByWidth({
text: normalizedText,
maxWidth: availableWidth,
fontSize: fittedFontSize,
letterSpacing,
maxLines: resolvedMaxLines,
}).slice(0, resolvedMaxLines);
bestLineHeightPx = Math.max(fittedFontSize, fittedFontSize * resolvedLineHeight);
return {
fontSize: Number(fittedFontSize.toFixed(2)),
lineHeightPx: Number(bestLineHeightPx.toFixed(2)),
lines: bestLines,
};
}
for (let index = 0; index < 24; index += 1) {
const lineHeightPx = Math.max(fittedFontSize, fittedFontSize * resolvedLineHeight);
const heightLimitedMaxLines = Math.max(
@ -216,11 +235,15 @@ export function analyzeTextLayerPixels(params: {
: inferredTextAlign === "center"
? Math.max(0, Math.round(Math.min(leftGap, rightGap)))
: Math.max(0, Math.round(leftGap));
const topGap = minY;
const bottomGap = Math.max(0, height - maxY - 1);
const suggestedPaddingY = Math.max(0, Math.round(Math.min(topGap, bottomGap)));
return {
dominantColor,
inferredTextAlign,
suggestedPaddingX,
suggestedPaddingY,
visibleBounds: {
minX,
minY,