fix: preserve PSD layer order in 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 21:44:53 +08:00
parent 510b0c13a6
commit 9965ead31a
2 changed files with 94 additions and 10 deletions

View File

@ -87,6 +87,11 @@ interface PsdLayerNodeEntry {
layer: ParsedPsdTemplateLayer;
}
export interface PsdTemplateSourceLayerEntry {
layer: ParsedPsdTemplateLayer;
composite: () => Promise<Uint8ClampedArray>;
}
function normalizeLayerName(value: unknown, fallback: string) {
const text = String(value || "").trim();
return text || fallback;
@ -308,6 +313,23 @@ export async function parsePsdTemplateFileFromUrl(fileUrl: string) {
return parsePsdTemplateFile(buffer);
}
export async function loadPsdTemplateSourceFromUrl(fileUrl: string) {
const buffer = await fetchRemoteBuffer(fileUrl);
const psd = await parsePsdBuffer(buffer);
const layerEntries = collectPsdLayerEntries(Array.isArray(psd.children) ? psd.children : []);
return {
width: Math.max(1, Math.round(Number(psd.width || 0))),
height: Math.max(1, Math.round(Number(psd.height || 0))),
layers: layerEntries
.filter((entry) => entry.node && typeof entry.node.composite === "function")
.map((entry) => ({
layer: entry.layer,
composite: () => entry.node.composite!(),
})) satisfies PsdTemplateSourceLayerEntry[],
};
}
async function createTransparentCanvasBuffer(width: number, height: number) {
const sharp = await getSharp();
const canvasWidth = Math.max(1, Math.round(Number(width || 0)));
@ -491,7 +513,7 @@ export async function preparePsdTemplateRenderAssets(params: {
const baseCompositeInputs: Array<{ input: Buffer; left: number; top: number }> = [];
for (const entry of layerEntries) {
for (const entry of [...layerEntries].reverse()) {
if (editableLayerIdSet.has(entry.layer.layerId)) {
continue;
}

View File

@ -1,6 +1,7 @@
import "server-only";
import { uploadFile } from "@/lib/storage/service";
import { loadPsdTemplateSourceFromUrl } from "@/lib/agents/psd-template-parser";
import {
isAgentPsdImageLayerSlot,
isAgentPsdTextLayerSlot,
@ -26,6 +27,19 @@ async function fetchRemoteBuffer(url: string) {
return Buffer.from(await response.arrayBuffer());
}
async function rgbaToPngBuffer(rgba: Uint8ClampedArray, width: number, height: number) {
const sharp = await getSharp();
return sharp(Buffer.from(rgba), {
raw: {
width,
height,
channels: 4,
},
})
.png()
.toBuffer();
}
async function fitOverlayBufferIntoCanvas(params: {
input: Buffer;
left: number;
@ -285,12 +299,12 @@ 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 source = await loadPsdTemplateSourceFromUrl(template.fileUrl);
const canvasWidth = Math.max(1, Math.round(Number(source.width || template.width || 1)));
const canvasHeight = Math.max(1, Math.round(Number(source.height || template.height || 1)));
const remoteBufferCache = new Map<string, Buffer>();
const fontMap = new Map(fonts.map((font) => [font.id, font]));
const sourceLayerMap = new Map(source.layers.map((entry) => [entry.layer.layerId, entry]));
async function readCachedBuffer(url: string) {
const normalizedUrl = String(url || "").trim();
@ -307,12 +321,40 @@ export async function renderAgentPsdTemplate(params: {
const compositeInputs: Array<{ input: Buffer; left: number; top: number }> = [];
for (const layer of template.layers) {
if (!layer.isEditable) continue;
async function appendOriginalLayer(layerId: string) {
const sourceLayer = sourceLayerMap.get(layerId);
if (!sourceLayer) return;
const rgba = await sourceLayer.composite();
const overlayBuffer = await rgbaToPngBuffer(
rgba,
sourceLayer.layer.width,
sourceLayer.layer.height
);
const fittedOverlay = await fitOverlayBufferIntoCanvas({
input: overlayBuffer,
left: sourceLayer.layer.x,
top: sourceLayer.layer.y,
width: sourceLayer.layer.width,
height: sourceLayer.layer.height,
canvasWidth,
canvasHeight,
});
if (!fittedOverlay) return;
compositeInputs.push(fittedOverlay);
}
for (const layer of [...template.layers].reverse()) {
if (!layer.isEditable) {
await appendOriginalLayer(layer.layerId);
continue;
}
if (isAgentPsdImageLayerSlot(layer)) {
const assignmentUrl = String(imageAssignments[layer.id] || "").trim();
if (!assignmentUrl) continue;
if (!assignmentUrl) {
await appendOriginalLayer(layer.layerId);
continue;
}
const sourceBuffer = await readCachedBuffer(assignmentUrl);
const fit =
@ -368,7 +410,11 @@ export async function renderAgentPsdTemplate(params: {
if (isAgentPsdTextLayerSlot(layer)) {
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
if (!text) continue;
const defaultText = String(layer.defaultText || "").trim();
if (!text || text === defaultText) {
await appendOriginalLayer(layer.layerId);
continue;
}
const font = layer.fontId ? fontMap.get(layer.fontId) : undefined;
const fontBuffer = font?.fileUrl ? await readCachedBuffer(font.fileUrl) : undefined;
@ -407,7 +453,23 @@ export async function renderAgentPsdTemplate(params: {
}
}
const resultBuffer = await sharp(baseBuffer)
const transparentCanvas = await sharp({
create: {
width: canvasWidth,
height: canvasHeight,
channels: 4,
background: {
r: 0,
g: 0,
b: 0,
alpha: 0,
},
},
})
.png()
.toBuffer();
const resultBuffer = await sharp(transparentCanvas)
.composite(compositeInputs)
.png()
.toBuffer();