fix: use PSD text metrics for template rendering
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-03-27 22:00:05 +08:00
parent eb6a0bf7dd
commit 449a631db4
3 changed files with 78 additions and 9 deletions

View File

@ -147,9 +147,22 @@ function buildInitialTemplateLayers(params: {
fontId: matchedFont?.font.id,
fontFamily: matchedFont?.font.family,
fontLabel: matchedFont?.font.name,
fontSize: Math.max(16, Math.min(layer.height, Math.round(layer.height * 0.78))),
lineHeight: 1.2,
letterSpacing: 0,
fontSize: Math.max(
10,
Math.round(
Number(
layer.detectedFontSize ||
Math.max(16, Math.min(layer.height, Math.round(layer.height * 0.78)))
)
)
),
lineHeight:
Number.isFinite(Number(layer.detectedLineHeight)) && Number(layer.detectedLineHeight) > 0
? Number(layer.detectedLineHeight)
: 1.2,
letterSpacing: Number.isFinite(Number(layer.detectedLetterSpacing))
? Number(layer.detectedLetterSpacing)
: 0,
fontWeight: 500,
textColor: "#111111",
textAlign: "left",

View File

@ -73,6 +73,9 @@ export interface ParsedPsdTemplateLayer {
defaultText?: string;
detectedFontName?: string;
detectedFontNames?: string[];
detectedFontSize?: number;
detectedLineHeight?: number;
detectedLetterSpacing?: number;
}
export interface ParsedPsdTemplatePayload {
@ -203,6 +206,38 @@ function extractDetectedFontNames(textProperties?: PsdTextPropertiesLike) {
return normalizeDetectedFontNames(candidates);
}
function extractTextStyleMetrics(textProperties?: PsdTextPropertiesLike) {
const styleRuns = textProperties?.EngineDict?.StyleRun?.RunArray || [];
const firstValidRun = styleRuns.find((styleRun) => {
const style = styleRun?.StyleSheet?.StyleSheetData;
return Number.isFinite(Number(style?.FontSize));
});
const style = firstValidRun?.StyleSheet?.StyleSheetData;
const fontSizeValue = Number(style?.FontSize);
const leadingValue = Number(style?.Leading);
const trackingValue = Number(style?.Tracking);
const detectedFontSize =
Number.isFinite(fontSizeValue) && fontSizeValue > 0 ? fontSizeValue : undefined;
const detectedLineHeight =
detectedFontSize && Number.isFinite(leadingValue) && leadingValue > 0
? Number((leadingValue / detectedFontSize).toFixed(3))
: style?.AutoLeading
? 1.2
: undefined;
const detectedLetterSpacing =
detectedFontSize && Number.isFinite(trackingValue)
? Number(((trackingValue / 1000) * detectedFontSize).toFixed(3))
: undefined;
return {
detectedFontSize,
detectedLineHeight,
detectedLetterSpacing,
};
}
function collectPsdLayerEntries(
nodes: PsdLayerLike[],
ancestors: string[] = [],
@ -246,6 +281,13 @@ function collectPsdLayerEntries(
const detectedFontNames = defaultText
? extractDetectedFontNames(node.textProperties)
: [];
const textStyleMetrics = defaultText
? extractTextStyleMetrics(node.textProperties)
: {
detectedFontSize: undefined,
detectedLineHeight: undefined,
detectedLetterSpacing: undefined,
};
result.push({
node,
@ -263,6 +305,9 @@ function collectPsdLayerEntries(
defaultText: defaultText || undefined,
detectedFontName: detectedFontNames[0] || undefined,
detectedFontNames: detectedFontNames.length > 0 ? detectedFontNames : undefined,
detectedFontSize: textStyleMetrics.detectedFontSize,
detectedLineHeight: textStyleMetrics.detectedLineHeight,
detectedLetterSpacing: textStyleMetrics.detectedLetterSpacing,
},
});
}

View File

@ -231,17 +231,27 @@ async function createTextOverlayBuffer(params: {
maxLines,
} = params;
const normalizedText = String(text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const explicitLineCount = Math.max(1, normalizedText.split("\n").length);
const availableHeight = Math.max(8, height - paddingY * 2);
const adjustedFontSize =
explicitLineCount > 1 && fontSize * Math.max(1, lineHeight) * explicitLineCount > availableHeight * 1.18
? Math.max(
10,
Math.floor((availableHeight / (explicitLineCount * Math.max(1, lineHeight))) * 100) / 100
)
: fontSize;
const availableWidth = Math.max(8, width - paddingX * 2);
const lineHeightPx = Math.max(fontSize, fontSize * lineHeight);
const lineHeightPx = Math.max(adjustedFontSize, adjustedFontSize * lineHeight);
const heightLimitedMaxLines = Math.max(
1,
Math.floor((height - paddingY * 2 + fontSize * 0.2) / lineHeightPx)
Math.floor((height - paddingY * 2 + adjustedFontSize * 0.2) / lineHeightPx)
);
const resolvedMaxLines = Math.max(1, Math.min(maxLines, heightLimitedMaxLines));
const lines = wrapTextByWidth({
text,
text: normalizedText,
maxWidth: availableWidth,
fontSize,
fontSize: adjustedFontSize,
letterSpacing,
maxLines: resolvedMaxLines,
});
@ -266,7 +276,8 @@ async function createTextOverlayBuffer(params: {
const tspans = lines
.map((line, index) => {
const y = paddingY + fontSize + index * lineHeightPx;
return `<tspan x="${x}" y="${y}">${escapeXml(line)}</tspan>`;
const baselineY = paddingY + adjustedFontSize + index * lineHeightPx;
return `<tspan x="${x}" y="${baselineY}">${escapeXml(line)}</tspan>`;
})
.join("");
@ -275,7 +286,7 @@ async function createTextOverlayBuffer(params: {
<defs>
<style>
${fontFaceCss}
.text { font-family: "${escapedFamily}", sans-serif; font-size: ${fontSize}px; font-weight: ${fontWeight}; fill: ${textColor}; letter-spacing: ${letterSpacing}px; }
.text { font-family: "${escapedFamily}", sans-serif; font-size: ${adjustedFontSize}px; font-weight: ${fontWeight}; fill: ${textColor}; letter-spacing: ${letterSpacing}px; }
</style>
<clipPath id="${clipId}">
<rect x="0" y="0" width="${width}" height="${height}" />