fix: clip PSD overlays to canvas bounds
This commit is contained in:
parent
ed7534a1ff
commit
e23912d48f
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user