fix: clip PSD overlays to canvas bounds
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 20:50:07 +08:00
parent ed7534a1ff
commit e23912d48f
3 changed files with 169 additions and 7 deletions

View File

@ -140,6 +140,12 @@ function normalizePositiveInteger(value: unknown) {
return Math.max(0, Math.round(numberValue));
}
function normalizeInteger(value: unknown) {
const numberValue = Number(value);
if (!Number.isFinite(numberValue)) return 0;
return Math.round(numberValue);
}
function normalizePositiveNumber(value: unknown, fallback: number) {
const numberValue = Number(value);
if (!Number.isFinite(numberValue) || numberValue <= 0) {
@ -228,8 +234,8 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
layerName,
layerPath,
depth: Math.max(0, normalizePositiveInteger(record.depth)),
x: normalizePositiveInteger(record.x),
y: normalizePositiveInteger(record.y),
x: normalizeInteger(record.x),
y: normalizeInteger(record.y),
width,
height,
isEditable,

View File

@ -250,8 +250,8 @@ function collectPsdLayerEntries(
layerName,
layerPath: [...ancestors, layerName].join(" / "),
depth,
x: Math.max(0, Math.round(Number(node.left || 0))),
y: Math.max(0, Math.round(Number(node.top || 0))),
x: Math.round(Number(node.left || 0)),
y: Math.round(Number(node.top || 0)),
width,
height,
slotType: defaultText ? "text" : "image",
@ -329,6 +329,71 @@ async function createTransparentCanvasBuffer(width: number, height: number) {
.toBuffer();
}
async function fitOverlayBufferIntoCanvas(params: {
input: Buffer;
left: number;
top: number;
width: number;
height: number;
canvasWidth: number;
canvasHeight: number;
}) {
const sharp = await getSharp();
const originalWidth = Math.max(1, Math.round(Number(params.width || 0)));
const originalHeight = Math.max(1, Math.round(Number(params.height || 0)));
let left = Math.round(Number(params.left || 0));
let top = Math.round(Number(params.top || 0));
let extractLeft = 0;
let extractTop = 0;
let targetWidth = originalWidth;
let targetHeight = originalHeight;
if (left < 0) {
extractLeft = Math.min(originalWidth, Math.abs(left));
targetWidth -= extractLeft;
left = 0;
}
if (top < 0) {
extractTop = Math.min(originalHeight, Math.abs(top));
targetHeight -= extractTop;
top = 0;
}
if (left >= params.canvasWidth || top >= params.canvasHeight) {
return null;
}
targetWidth = Math.min(targetWidth, params.canvasWidth - left);
targetHeight = Math.min(targetHeight, params.canvasHeight - top);
if (targetWidth <= 0 || targetHeight <= 0) {
return null;
}
const outputBuffer =
extractLeft > 0 ||
extractTop > 0 ||
targetWidth !== originalWidth ||
targetHeight !== originalHeight
? await sharp(params.input)
.extract({
left: extractLeft,
top: extractTop,
width: targetWidth,
height: targetHeight,
})
.png()
.toBuffer()
: params.input;
return {
input: outputBuffer,
left,
top,
};
}
export async function preparePsdTemplateRenderAssets(params: {
templateId: string;
fileUrl: string;
@ -438,11 +503,20 @@ export async function preparePsdTemplateRenderAssets(params: {
try {
const rgba = await entry.node.composite();
const overlayBuffer = await rgbaToPngBuffer(rgba, entry.layer.width, entry.layer.height);
baseCompositeInputs.push({
const fittedOverlay = await fitOverlayBufferIntoCanvas({
input: overlayBuffer,
left: entry.layer.x,
top: entry.layer.y,
width: entry.layer.width,
height: entry.layer.height,
canvasWidth: Math.max(1, Math.round(Number(psd.width || 0))),
canvasHeight: Math.max(1, Math.round(Number(psd.height || 0))),
});
if (!fittedOverlay) {
continue;
}
baseCompositeInputs.push(fittedOverlay);
} catch (error) {
console.warn("[PSD Template] Failed to compose base layer:", {
layerId: entry.layer.layerId,

View File

@ -26,6 +26,71 @@ async function fetchRemoteBuffer(url: string) {
return Buffer.from(await response.arrayBuffer());
}
async function fitOverlayBufferIntoCanvas(params: {
input: Buffer;
left: number;
top: number;
width: number;
height: number;
canvasWidth: number;
canvasHeight: number;
}) {
const sharp = await getSharp();
const originalWidth = Math.max(1, Math.round(Number(params.width || 0)));
const originalHeight = Math.max(1, Math.round(Number(params.height || 0)));
let left = Math.round(Number(params.left || 0));
let top = Math.round(Number(params.top || 0));
let extractLeft = 0;
let extractTop = 0;
let targetWidth = originalWidth;
let targetHeight = originalHeight;
if (left < 0) {
extractLeft = Math.min(originalWidth, Math.abs(left));
targetWidth -= extractLeft;
left = 0;
}
if (top < 0) {
extractTop = Math.min(originalHeight, Math.abs(top));
targetHeight -= extractTop;
top = 0;
}
if (left >= params.canvasWidth || top >= params.canvasHeight) {
return null;
}
targetWidth = Math.min(targetWidth, params.canvasWidth - left);
targetHeight = Math.min(targetHeight, params.canvasHeight - top);
if (targetWidth <= 0 || targetHeight <= 0) {
return null;
}
const outputBuffer =
extractLeft > 0 ||
extractTop > 0 ||
targetWidth !== originalWidth ||
targetHeight !== originalHeight
? await sharp(params.input)
.extract({
left: extractLeft,
top: extractTop,
width: targetWidth,
height: targetHeight,
})
.png()
.toBuffer()
: params.input;
return {
input: outputBuffer,
left,
top,
};
}
function sanitizeFileSegment(value: string, fallback: string) {
const normalized = value
.trim()
@ -221,6 +286,9 @@ export async function renderAgentPsdTemplate(params: {
const { template, imageAssignments, textAssignments, fonts } = params;
const sharp = await getSharp();
const baseBuffer = await fetchRemoteBuffer(template.renderBaseUrl || template.previewUrl);
const baseMetadata = await sharp(baseBuffer).metadata();
const canvasWidth = Math.max(1, Math.round(Number(baseMetadata.width || template.width || 1)));
const canvasHeight = Math.max(1, Math.round(Number(baseMetadata.height || template.height || 1)));
const remoteBufferCache = new Map<string, Buffer>();
const fontMap = new Map(fonts.map((font) => [font.id, font]));
@ -283,11 +351,18 @@ export async function renderAgentPsdTemplate(params: {
.toBuffer();
}
compositeInputs.push({
const fittedOverlay = await fitOverlayBufferIntoCanvas({
input: overlayBuffer,
left: layer.x,
top: layer.y,
width: layer.width,
height: layer.height,
canvasWidth,
canvasHeight,
});
if (!fittedOverlay) continue;
compositeInputs.push(fittedOverlay);
continue;
}
@ -317,11 +392,18 @@ export async function renderAgentPsdTemplate(params: {
if (!overlayBuffer) continue;
compositeInputs.push({
const fittedOverlay = await fitOverlayBufferIntoCanvas({
input: overlayBuffer,
left: layer.x,
top: layer.y,
width: layer.width,
height: layer.height,
canvasWidth,
canvasHeight,
});
if (!fittedOverlay) continue;
compositeInputs.push(fittedOverlay);
}
}