fix: preserve PSD layer order in template rendering
This commit is contained in:
parent
510b0c13a6
commit
9965ead31a
@ -87,6 +87,11 @@ interface PsdLayerNodeEntry {
|
|||||||
layer: ParsedPsdTemplateLayer;
|
layer: ParsedPsdTemplateLayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PsdTemplateSourceLayerEntry {
|
||||||
|
layer: ParsedPsdTemplateLayer;
|
||||||
|
composite: () => Promise<Uint8ClampedArray>;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeLayerName(value: unknown, fallback: string) {
|
function normalizeLayerName(value: unknown, fallback: string) {
|
||||||
const text = String(value || "").trim();
|
const text = String(value || "").trim();
|
||||||
return text || fallback;
|
return text || fallback;
|
||||||
@ -308,6 +313,23 @@ export async function parsePsdTemplateFileFromUrl(fileUrl: string) {
|
|||||||
return parsePsdTemplateFile(buffer);
|
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) {
|
async function createTransparentCanvasBuffer(width: number, height: number) {
|
||||||
const sharp = await getSharp();
|
const sharp = await getSharp();
|
||||||
const canvasWidth = Math.max(1, Math.round(Number(width || 0)));
|
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 }> = [];
|
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)) {
|
if (editableLayerIdSet.has(entry.layer.layerId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
|
import { loadPsdTemplateSourceFromUrl } from "@/lib/agents/psd-template-parser";
|
||||||
import {
|
import {
|
||||||
isAgentPsdImageLayerSlot,
|
isAgentPsdImageLayerSlot,
|
||||||
isAgentPsdTextLayerSlot,
|
isAgentPsdTextLayerSlot,
|
||||||
@ -26,6 +27,19 @@ async function fetchRemoteBuffer(url: string) {
|
|||||||
return Buffer.from(await response.arrayBuffer());
|
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: {
|
async function fitOverlayBufferIntoCanvas(params: {
|
||||||
input: Buffer;
|
input: Buffer;
|
||||||
left: number;
|
left: number;
|
||||||
@ -285,12 +299,12 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
}) {
|
}) {
|
||||||
const { template, imageAssignments, textAssignments, fonts } = params;
|
const { template, imageAssignments, textAssignments, fonts } = params;
|
||||||
const sharp = await getSharp();
|
const sharp = await getSharp();
|
||||||
const baseBuffer = await fetchRemoteBuffer(template.renderBaseUrl || template.previewUrl);
|
const source = await loadPsdTemplateSourceFromUrl(template.fileUrl);
|
||||||
const baseMetadata = await sharp(baseBuffer).metadata();
|
const canvasWidth = Math.max(1, Math.round(Number(source.width || template.width || 1)));
|
||||||
const canvasWidth = Math.max(1, Math.round(Number(baseMetadata.width || template.width || 1)));
|
const canvasHeight = Math.max(1, Math.round(Number(source.height || template.height || 1)));
|
||||||
const canvasHeight = Math.max(1, Math.round(Number(baseMetadata.height || template.height || 1)));
|
|
||||||
const remoteBufferCache = new Map<string, Buffer>();
|
const remoteBufferCache = new Map<string, Buffer>();
|
||||||
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
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) {
|
async function readCachedBuffer(url: string) {
|
||||||
const normalizedUrl = String(url || "").trim();
|
const normalizedUrl = String(url || "").trim();
|
||||||
@ -307,12 +321,40 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
|
|
||||||
const compositeInputs: Array<{ input: Buffer; left: number; top: number }> = [];
|
const compositeInputs: Array<{ input: Buffer; left: number; top: number }> = [];
|
||||||
|
|
||||||
for (const layer of template.layers) {
|
async function appendOriginalLayer(layerId: string) {
|
||||||
if (!layer.isEditable) continue;
|
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)) {
|
if (isAgentPsdImageLayerSlot(layer)) {
|
||||||
const assignmentUrl = String(imageAssignments[layer.id] || "").trim();
|
const assignmentUrl = String(imageAssignments[layer.id] || "").trim();
|
||||||
if (!assignmentUrl) continue;
|
if (!assignmentUrl) {
|
||||||
|
await appendOriginalLayer(layer.layerId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const sourceBuffer = await readCachedBuffer(assignmentUrl);
|
const sourceBuffer = await readCachedBuffer(assignmentUrl);
|
||||||
const fit =
|
const fit =
|
||||||
@ -368,7 +410,11 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
|
|
||||||
if (isAgentPsdTextLayerSlot(layer)) {
|
if (isAgentPsdTextLayerSlot(layer)) {
|
||||||
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
|
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 font = layer.fontId ? fontMap.get(layer.fontId) : undefined;
|
||||||
const fontBuffer = font?.fileUrl ? await readCachedBuffer(font.fileUrl) : 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)
|
.composite(compositeInputs)
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user