diff --git a/lib/agents/psd-template-config.ts b/lib/agents/psd-template-config.ts index d1fb00c15..a199bc8db 100644 --- a/lib/agents/psd-template-config.ts +++ b/lib/agents/psd-template-config.ts @@ -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, diff --git a/lib/agents/psd-template-parser.ts b/lib/agents/psd-template-parser.ts index 013ff359c..379750425 100644 --- a/lib/agents/psd-template-parser.ts +++ b/lib/agents/psd-template-parser.ts @@ -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, diff --git a/lib/agents/psd-template-renderer.ts b/lib/agents/psd-template-renderer.ts index 9724978f8..7acd0b5d8 100644 --- a/lib/agents/psd-template-renderer.ts +++ b/lib/agents/psd-template-renderer.ts @@ -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(); 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); } }