fix: precompile PSD template render segments

This commit is contained in:
Your Name 2026-06-01 17:52:25 +08:00
parent 720652b68a
commit 0a001fc5f4
7 changed files with 442 additions and 36 deletions

View File

@ -18,6 +18,7 @@ import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache"; import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
import { import {
preparePsdTemplateRenderAssets, preparePsdTemplateRenderAssets,
uploadPreparedPsdTemplateRenderSegments,
} from "@/lib/agents/psd-template-parser"; } from "@/lib/agents/psd-template-parser";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair"; import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
@ -222,6 +223,11 @@ export async function PUT(
preparedAssets.renderBaseBuffer.byteOffset + preparedAssets.renderBaseBuffer.byteLength preparedAssets.renderBaseBuffer.byteOffset + preparedAssets.renderBaseBuffer.byteLength
) as ArrayBuffer ) as ArrayBuffer
); );
const uploadedRenderSegments = await uploadPreparedPsdTemplateRenderSegments({
templateId: currentTemplate.id,
segments: preparedAssets.renderSegments,
uploadAsset,
});
const updatedTemplate = { const updatedTemplate = {
...currentTemplate, ...currentTemplate,
@ -234,6 +240,7 @@ export async function PUT(
status: nextStatus, status: nextStatus,
renderBaseUrl: uploadedRenderBase.url, renderBaseUrl: uploadedRenderBase.url,
renderBasePath: uploadedRenderBase.pathname, renderBasePath: uploadedRenderBase.pathname,
renderSegments: uploadedRenderSegments,
layers: nextLayers, layers: nextLayers,
layerCount: nextLayers.length, layerCount: nextLayers.length,
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION, assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,

View File

@ -10,7 +10,10 @@ import {
type AgentPsdTemplateLayerSlot, type AgentPsdTemplateLayerSlot,
} from "@/lib/agents/psd-template-config"; } from "@/lib/agents/psd-template-config";
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache"; import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
import { preparePsdTemplateRenderAssets } from "@/lib/agents/psd-template-parser"; import {
preparePsdTemplateRenderAssets,
uploadPreparedPsdTemplateRenderSegments,
} from "@/lib/agents/psd-template-parser";
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
import { createStorageUploadSession } from "@/lib/storage/service"; import { createStorageUploadSession } from "@/lib/storage/service";
@ -181,7 +184,7 @@ async function backfillTemplateAssetsOnce(params: {
...currentTemplate, ...currentTemplate,
parseStatus: "processing", parseStatus: "processing",
parseProgress: Math.max(88, Number(currentTemplate.parseProgress || 0)), parseProgress: Math.max(88, Number(currentTemplate.parseProgress || 0)),
parseStage: "正在后台生成模板底图", parseStage: "正在后台生成模板底图和快速导出分层",
parseError: undefined, parseError: undefined,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}), }),
@ -191,6 +194,11 @@ async function backfillTemplateAssetsOnce(params: {
`agent-psd-base-${template.id}-${Date.now()}.png`, `agent-psd-base-${template.id}-${Date.now()}.png`,
toArrayBuffer(preparedAssets.renderBaseBuffer) toArrayBuffer(preparedAssets.renderBaseBuffer)
); );
const uploadedRenderSegments = await uploadPreparedPsdTemplateRenderSegments({
templateId: template.id,
segments: preparedAssets.renderSegments,
uploadAsset,
});
await updateTemplateById({ await updateTemplateById({
templateId: template.id, templateId: template.id,
@ -205,6 +213,7 @@ async function backfillTemplateAssetsOnce(params: {
...currentTemplate, ...currentTemplate,
renderBaseUrl: uploadedRenderBase.url, renderBaseUrl: uploadedRenderBase.url,
renderBasePath: uploadedRenderBase.pathname, renderBasePath: uploadedRenderBase.pathname,
renderSegments: uploadedRenderSegments,
layers: mergedLayers, layers: mergedLayers,
layerCount: mergedLayers.length, layerCount: mergedLayers.length,
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION, assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,

View File

@ -4,7 +4,7 @@ import type { AgentPsdTemplateCategoryTagConfig } from "@/lib/agents/psd-templat
import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config"; import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config";
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates"; export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 6; export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 7;
export type AgentPsdTemplateStatus = "active" | "disabled"; export type AgentPsdTemplateStatus = "active" | "disabled";
export type AgentPsdTemplateParseStatus = export type AgentPsdTemplateParseStatus =
@ -83,6 +83,13 @@ export type AgentPsdTemplateLayerSlot =
| AgentPsdTemplateImageLayerSlot | AgentPsdTemplateImageLayerSlot
| AgentPsdTemplateTextLayerSlot; | AgentPsdTemplateTextLayerSlot;
export interface AgentPsdTemplateRenderSegment {
id: string;
beforeLayerId?: string;
url: string;
path?: string;
}
export interface AgentPsdTemplate { export interface AgentPsdTemplate {
id: string; id: string;
name: string; name: string;
@ -103,6 +110,7 @@ export interface AgentPsdTemplate {
previewPath?: string; previewPath?: string;
renderBaseUrl?: string; renderBaseUrl?: string;
renderBasePath?: string; renderBasePath?: string;
renderSegments?: AgentPsdTemplateRenderSegment[];
width: number; width: number;
height: number; height: number;
layerCount: number; layerCount: number;
@ -443,6 +451,36 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
}); });
} }
function parseAgentPsdTemplateRenderSegments(value: unknown) {
if (!Array.isArray(value)) return [] as AgentPsdTemplateRenderSegment[];
const segments: AgentPsdTemplateRenderSegment[] = [];
const seenIds = new Set<string>();
for (const entry of value) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
continue;
}
const record = entry as Record<string, unknown>;
const id = normalizeText(record.id);
const url = normalizeUrl(record.url);
if (!id || !url || seenIds.has(id)) {
continue;
}
seenIds.add(id);
segments.push({
id,
beforeLayerId: normalizeText(record.beforeLayerId) || undefined,
url,
path: normalizeText(record.path) || undefined,
});
}
return segments;
}
export function parseAgentPsdTemplates(value: unknown) { export function parseAgentPsdTemplates(value: unknown) {
if (!Array.isArray(value)) return [] as AgentPsdTemplate[]; if (!Array.isArray(value)) return [] as AgentPsdTemplate[];
@ -512,6 +550,7 @@ export function parseAgentPsdTemplates(value: unknown) {
renderBaseUrl: renderBaseUrl:
normalizeOptionalUrl(record.renderBaseUrl) || normalizeOptionalUrl(record.previewUrl), normalizeOptionalUrl(record.renderBaseUrl) || normalizeOptionalUrl(record.previewUrl),
renderBasePath: normalizeText(record.renderBasePath) || undefined, renderBasePath: normalizeText(record.renderBasePath) || undefined,
renderSegments: parseAgentPsdTemplateRenderSegments(record.renderSegments),
width, width,
height, height,
layerCount, layerCount,
@ -547,6 +586,14 @@ export function hasAgentPsdTemplateStoredLayerSource(
); );
} }
export function hasAgentPsdTemplateRenderSegments(
template: Pick<AgentPsdTemplate, "renderSegments">
) {
return Boolean(
template.renderSegments?.some((segment) => String(segment.url || "").trim())
);
}
export function hasAgentPsdTemplateDedicatedRenderBase( export function hasAgentPsdTemplateDedicatedRenderBase(
template: Pick<AgentPsdTemplate, "renderBaseUrl" | "renderBasePath" | "previewUrl"> template: Pick<AgentPsdTemplate, "renderBaseUrl" | "renderBasePath" | "previewUrl">
) { ) {

View File

@ -11,6 +11,7 @@ import {
isAgentPsdTextLayerSlot, isAgentPsdTextLayerSlot,
type AgentPsdTemplateImageContentBounds, type AgentPsdTemplateImageContentBounds,
type AgentPsdTemplateLayerSlot, type AgentPsdTemplateLayerSlot,
type AgentPsdTemplateRenderSegment,
} from "@/lib/agents/psd-template-config"; } from "@/lib/agents/psd-template-config";
const DEFAULT_PSD_TEMPLATE_ASSET_CONCURRENCY = 4; const DEFAULT_PSD_TEMPLATE_ASSET_CONCURRENCY = 4;
@ -119,6 +120,12 @@ export interface PsdTemplateSourceLayerEntry {
composite: () => Promise<Uint8ClampedArray>; composite: () => Promise<Uint8ClampedArray>;
} }
export interface PreparedPsdTemplateRenderSegment {
id: string;
beforeLayerId?: string;
buffer: Buffer;
}
function resolveVisibleContentBounds(params: { function resolveVisibleContentBounds(params: {
rgba: Uint8ClampedArray; rgba: Uint8ClampedArray;
width: number; width: number;
@ -666,6 +673,35 @@ function toArrayBuffer(buffer: Buffer) {
) as ArrayBuffer; ) as ArrayBuffer;
} }
export async function uploadPreparedPsdTemplateRenderSegments(params: {
templateId: string;
segments: PreparedPsdTemplateRenderSegment[];
uploadAsset: StorageUploadFunction;
}) {
const uploadedSegments = await mapWithConcurrency(
params.segments,
DEFAULT_PSD_TEMPLATE_ASSET_CONCURRENCY,
async (segment) => {
const uploaded = await params.uploadAsset(
`agent-psd-segment-${params.templateId}-${sanitizeFileSegment(
segment.id,
"segment"
)}-${Date.now()}.png`,
toArrayBuffer(segment.buffer)
);
return {
id: segment.id,
beforeLayerId: segment.beforeLayerId,
url: uploaded.url,
path: uploaded.pathname,
} satisfies AgentPsdTemplateRenderSegment;
}
);
return uploadedSegments;
}
async function mapWithConcurrency<T, R>( async function mapWithConcurrency<T, R>(
items: T[], items: T[],
concurrency: number, concurrency: number,
@ -727,8 +763,13 @@ export async function preparePsdTemplateRenderAssets(params: {
fileBuffer: params.fileBuffer, fileBuffer: params.fileBuffer,
}); });
const psd = await parsePsdBuffer(psdBuffer); const psd = await parsePsdBuffer(psdBuffer);
const canvasWidth = Math.max(1, Math.round(Number(psd.width || 0)));
const canvasHeight = Math.max(1, Math.round(Number(psd.height || 0)));
const layerEntries = collectPsdLayerEntries(Array.isArray(psd.children) ? psd.children : []); const layerEntries = collectPsdLayerEntries(Array.isArray(psd.children) ? psd.children : []);
const nodeMap = new Map(layerEntries.map((entry) => [entry.layer.layerId, entry])); const nodeMap = new Map(layerEntries.map((entry) => [entry.layer.layerId, entry]));
const layerSlotByPsdLayerId = new Map(
params.layers.map((layer) => [layer.layerId, layer])
);
const editableLayerIdSet = new Set( const editableLayerIdSet = new Set(
params.layers params.layers
.filter(isAgentPsdTemplateLayerUserEditable) .filter(isAgentPsdTemplateLayerUserEditable)
@ -900,19 +941,28 @@ export async function preparePsdTemplateRenderAssets(params: {
nextLayers[index] = layer; nextLayers[index] = layer;
}); });
const baseEntries = [...layerEntries] const drawableEntries = [...layerEntries]
.reverse() .reverse()
.filter( .filter((entry) => entry.node && typeof entry.node.composite === "function");
(entry) =>
!editableLayerIdSet.has(entry.layer.layerId) &&
entry.node &&
typeof entry.node.composite === "function"
);
const baseCompositeResults = await mapWithConcurrency( const layerCompositeResults = await mapWithConcurrency(
baseEntries, drawableEntries,
assetConcurrency, assetConcurrency,
async (entry, index) => { async (entry, index) => {
const layerSlot = layerSlotByPsdLayerId.get(entry.layer.layerId);
const isEditableBoundary = layerSlot
? isAgentPsdTemplateLayerUserEditable(layerSlot)
: editableLayerIdSet.has(entry.layer.layerId);
if (isEditableBoundary) {
return {
index,
input: null,
beforeLayerId: layerSlot?.id || "",
editableBoundary: true,
};
}
try { try {
const rgba = await entry.node.composite!(); const rgba = await entry.node.composite!();
const overlayBuffer = await rgbaToPngBuffer( const overlayBuffer = await rgbaToPngBuffer(
@ -926,8 +976,8 @@ export async function preparePsdTemplateRenderAssets(params: {
top: entry.layer.y, top: entry.layer.y,
width: entry.layer.width, width: entry.layer.width,
height: entry.layer.height, height: entry.layer.height,
canvasWidth: Math.max(1, Math.round(Number(psd.width || 0))), canvasWidth,
canvasHeight: Math.max(1, Math.round(Number(psd.height || 0))), canvasHeight,
}); });
if (!fittedOverlay) { if (!fittedOverlay) {
return null; return null;
@ -936,6 +986,8 @@ export async function preparePsdTemplateRenderAssets(params: {
return { return {
index, index,
input: fittedOverlay, input: fittedOverlay,
beforeLayerId: "",
editableBoundary: false,
}; };
} catch (error) { } catch (error) {
console.warn("[PSD Template] Failed to compose base layer:", { console.warn("[PSD Template] Failed to compose base layer:", {
@ -947,27 +999,77 @@ export async function preparePsdTemplateRenderAssets(params: {
} }
); );
const baseCompositeInputs = baseCompositeResults const sortedLayerCompositeResults = layerCompositeResults
.filter(
(
result
): result is {
index: number;
input: { input: Buffer; left: number; top: number } | null;
beforeLayerId: string;
editableBoundary: boolean;
} => Boolean(result)
)
.sort((left, right) => left.index - right.index);
const baseCompositeInputs = sortedLayerCompositeResults
.filter( .filter(
( (
result result
): result is { ): result is {
index: number; index: number;
input: { input: Buffer; left: number; top: number }; input: { input: Buffer; left: number; top: number };
} => Boolean(result) beforeLayerId: string;
editableBoundary: false;
} => Boolean(result.input) && !result.editableBoundary
) )
.sort((left, right) => left.index - right.index)
.map((result) => result.input); .map((result) => result.input);
const sharp = await getSharp(); const sharp = await getSharp();
const transparentBaseBuffer = await createTransparentCanvasBuffer(psd.width, psd.height); const transparentBaseBuffer = await createTransparentCanvasBuffer(
canvasWidth,
canvasHeight
);
const composeSegmentBuffer = (
inputs: Array<{ input: Buffer; left: number; top: number }>
) => sharp(transparentBaseBuffer).composite(inputs).png().toBuffer();
const renderBaseBuffer = await sharp(transparentBaseBuffer) const renderBaseBuffer = await sharp(transparentBaseBuffer)
.composite(baseCompositeInputs) .composite(baseCompositeInputs)
.png() .png()
.toBuffer(); .toBuffer();
const renderSegments: PreparedPsdTemplateRenderSegment[] = [];
let pendingSegmentInputs: Array<{ input: Buffer; left: number; top: number }> = [];
for (const result of sortedLayerCompositeResults) {
if (result.editableBoundary) {
if (pendingSegmentInputs.length > 0) {
renderSegments.push({
id: `before-${sanitizeFileSegment(
result.beforeLayerId,
`layer-${result.index}`
)}`,
beforeLayerId: result.beforeLayerId,
buffer: await composeSegmentBuffer(pendingSegmentInputs),
});
pendingSegmentInputs = [];
}
continue;
}
if (result.input) {
pendingSegmentInputs.push(result.input);
}
}
if (pendingSegmentInputs.length > 0) {
renderSegments.push({
id: "above-editable-layers",
buffer: await composeSegmentBuffer(pendingSegmentInputs),
});
}
return { return {
layers: nextLayers, layers: nextLayers,
renderBaseBuffer, renderBaseBuffer,
renderSegments,
}; };
} }

View File

@ -2,9 +2,11 @@ import "server-only";
import { uploadFile } from "@/lib/storage/service"; import { uploadFile } from "@/lib/storage/service";
import { fetchRemoteAssetBuffer } from "@/lib/agents/psd-remote-asset"; import { fetchRemoteAssetBuffer } from "@/lib/agents/psd-remote-asset";
import { loadPsdTemplateSource } from "@/lib/agents/psd-template-parser";
import { import {
hasAgentPsdTemplateStoredLayerSource, hasAgentPsdTemplateStoredLayerSource,
isAgentPsdImageLayerSlot, isAgentPsdImageLayerSlot,
isAgentPsdLayerSuperLocked,
isAgentPsdTemplateLayerUserEditable, isAgentPsdTemplateLayerUserEditable,
isAgentPsdTextLayerSlot, isAgentPsdTextLayerSlot,
resolveAgentPsdTextLayerEffectiveAlign, resolveAgentPsdTextLayerEffectiveAlign,
@ -181,8 +183,7 @@ function getDefaultSourceImageLayer(
) { ) {
const editableImageLayers = layers.filter( const editableImageLayers = layers.filter(
(layer): layer is AgentPsdTemplateImageLayerSlot => (layer): layer is AgentPsdTemplateImageLayerSlot =>
isAgentPsdTemplateLayerUserEditable(layer) && layer.isEditable && isAgentPsdImageLayerSlot(layer)
isAgentPsdImageLayerSlot(layer)
); );
return ( return (
@ -462,18 +463,68 @@ export async function renderAgentPsdTemplate(params: {
const hasDedicatedRenderBase = const hasDedicatedRenderBase =
Boolean(renderBaseUrl) && Boolean(renderBaseUrl) &&
renderBaseUrl !== String(template.previewUrl || "").trim(); renderBaseUrl !== String(template.previewUrl || "").trim();
const templateLayerOrderIds = template.layers.map((layer) => layer.id);
const isUsingTemplateLayerOrder =
orderedTemplateLayers.length === templateLayerOrderIds.length &&
orderedTemplateLayers.every(
(layer, index) => layer.id === templateLayerOrderIds[index]
);
const defaultSourceImageLayerId = const defaultSourceImageLayerId =
getDefaultSourceImageLayer(orderedTemplateLayers)?.id || ""; getDefaultSourceImageLayer(orderedTemplateLayers)?.id || "";
const hasImageAssignment = (layer: AgentPsdTemplate["layers"][number]) =>
isAgentPsdImageLayerSlot(layer) &&
Boolean(String(imageAssignments[layer.id] || "").trim());
const shouldRenderAsStaticLayer = (
layer: AgentPsdTemplate["layers"][number]
) => !isAgentPsdTemplateLayerUserEditable(layer) && !hasImageAssignment(layer);
const hasHiddenStaticLayer = orderedTemplateLayers.some( const hasHiddenStaticLayer = orderedTemplateLayers.some(
(layer) => (layer) =>
!isAgentPsdTemplateLayerUserEditable(layer) && shouldRenderAsStaticLayer(layer) &&
hiddenLayerIdSet.has(layer.id) hiddenLayerIdSet.has(layer.id)
); );
const hasVisibleAssignedImageLayer = orderedTemplateLayers.some(
(layer) =>
!hiddenLayerIdSet.has(layer.id) && hasImageAssignment(layer)
);
const hasStaticLayerPositionOffset = orderedTemplateLayers.some((layer) => {
if (
isAgentPsdTemplateLayerUserEditable(layer) ||
isAgentPsdLayerSuperLocked(layer)
) {
return false;
}
const offset = normalizeTextPositionOffset(layerPositionOffsets[layer.id]);
return Math.abs(offset.offsetX) > 0.01 || Math.abs(offset.offsetY) > 0.01;
});
const renderSegments = (template.renderSegments || []).filter((segment) =>
Boolean(String(segment.url || "").trim())
);
const shouldUseRenderSegments =
hasVisibleAssignedImageLayer &&
renderSegments.length > 0 &&
isUsingTemplateLayerOrder &&
!hasHiddenStaticLayer &&
!hasStaticLayerPositionOffset;
const shouldUseRenderBaseFallback = const shouldUseRenderBaseFallback =
hasDedicatedRenderBase && hasDedicatedRenderBase &&
!hasHiddenStaticLayer; !hasHiddenStaticLayer &&
!hasVisibleAssignedImageLayer;
const shouldHydrateLayerSourcesFromPsd =
hasVisibleAssignedImageLayer && !shouldUseRenderSegments;
const renderSegmentByBeforeLayerId = new Map(
renderSegments
.filter((segment) => String(segment.beforeLayerId || "").trim())
.map((segment) => [String(segment.beforeLayerId || "").trim(), segment])
);
const topRenderSegments = renderSegments.filter(
(segment) => !String(segment.beforeLayerId || "").trim()
);
function getLayerPositionOffset(layerId: string) { function getLayerPositionOffset(layerId: string) {
const layer = layerMap.get(layerId);
if (layer && isAgentPsdLayerSuperLocked(layer)) {
return normalizeTextPositionOffset();
}
return normalizeTextPositionOffset(layerPositionOffsets[layerId]); return normalizeTextPositionOffset(layerPositionOffsets[layerId]);
} }
@ -516,17 +567,22 @@ export async function renderAgentPsdTemplate(params: {
if (shouldUseRenderBaseFallback) { if (shouldUseRenderBaseFallback) {
pushAsset(renderBaseUrl, "PSD 模板底图"); pushAsset(renderBaseUrl, "PSD 模板底图");
} }
if (shouldUseRenderSegments) {
for (const segment of renderSegments) {
pushAsset(segment.url, "PSD 模板快速导出分层");
}
}
for (const layer of orderedTemplateLayers) { for (const layer of orderedTemplateLayers) {
if (hiddenLayerIdSet.has(layer.id)) { if (hiddenLayerIdSet.has(layer.id)) {
continue; continue;
} }
const storedLayerSourceUrl = getStoredLayerSourceUrl(layer); const storedLayerSourceUrl = getStoredLayerSourceUrl(layer);
if (!isAgentPsdTemplateLayerUserEditable(layer)) { if (shouldRenderAsStaticLayer(layer)) {
if (shouldUseRenderBaseFallback) { if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
continue; continue;
} }
pushAsset( pushAsset(
storedLayerSourceUrl, storedLayerSourceUrl,
"PSD 图层素材", "PSD 图层素材",
@ -601,6 +657,111 @@ export async function renderAgentPsdTemplate(params: {
const compositeInputs: Array<{ input: Buffer; left: number; top: number }> = const compositeInputs: Array<{ input: Buffer; left: number; top: number }> =
[]; [];
let psdSourceLayerEntryMapPromise: Promise<
Map<
string,
{
layer: { width: number; height: number };
composite: () => Promise<Uint8ClampedArray>;
}
>
> | null = null;
const psdSourceLayerBufferPromiseMap = new Map<string, Promise<Buffer | null>>();
async function getPsdSourceLayerEntryMap() {
if (!shouldHydrateLayerSourcesFromPsd) {
return null;
}
if (!psdSourceLayerEntryMapPromise) {
psdSourceLayerEntryMapPromise = loadPsdTemplateSource({
fileUrl: template.fileUrl,
filePath: template.filePath,
}).then((source) => {
const entryMap = new Map<
string,
{
layer: { width: number; height: number };
composite: () => Promise<Uint8ClampedArray>;
}
>();
for (const entry of source.layers) {
entryMap.set(entry.layer.layerId, {
layer: {
width: entry.layer.width,
height: entry.layer.height,
},
composite: entry.composite,
});
}
return entryMap;
});
}
return psdSourceLayerEntryMapPromise;
}
async function readPsdSourceLayerBuffer(
layer: AgentPsdTemplate["layers"][number]
) {
if (!shouldHydrateLayerSourcesFromPsd) {
return null;
}
if (!psdSourceLayerBufferPromiseMap.has(layer.layerId)) {
psdSourceLayerBufferPromiseMap.set(
layer.layerId,
(async () => {
try {
const entryMap = await getPsdSourceLayerEntryMap();
const entry = entryMap?.get(layer.layerId);
if (!entry) {
return null;
}
const rgba = await entry.composite();
return sharp(Buffer.from(rgba), {
raw: {
width: Math.max(1, Math.round(entry.layer.width || layer.width)),
height: Math.max(1, Math.round(entry.layer.height || layer.height)),
channels: 4,
},
})
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
} catch (error) {
console.warn("[PSD Template] failed to hydrate layer from PSD source:", {
templateId: template.id,
layerId: layer.layerId,
error,
});
return null;
}
})()
);
}
return psdSourceLayerBufferPromiseMap.get(layer.layerId)!;
}
async function readReusableLayerBuffer(
layer: AgentPsdTemplate["layers"][number]
) {
const sourceUrl = getStoredLayerSourceUrl(layer);
if (sourceUrl) {
try {
return await readCachedBuffer(sourceUrl, "PSD 图层素材");
} catch (error) {
const hydratedBuffer = await readPsdSourceLayerBuffer(layer);
if (hydratedBuffer) {
return hydratedBuffer;
}
throw error;
}
}
return readPsdSourceLayerBuffer(layer);
}
async function appendBufferOverlay(params: { async function appendBufferOverlay(params: {
input: Buffer; input: Buffer;
@ -631,10 +792,19 @@ export async function renderAgentPsdTemplate(params: {
options: { optional?: boolean } = {} options: { optional?: boolean } = {}
) { ) {
const sourceUrl = getStoredLayerSourceUrl(layer); const sourceUrl = getStoredLayerSourceUrl(layer);
if ( if ((!hasAgentPsdTemplateStoredLayerSource(layer) && !sourceUrl) || !sourceUrl) {
(!hasAgentPsdTemplateStoredLayerSource(layer) && !sourceUrl) || const hydratedBuffer = await readPsdSourceLayerBuffer(layer);
!sourceUrl if (hydratedBuffer) {
) { const layerPositionOffset = getLayerPositionOffset(layer.id);
return appendBufferOverlay({
input: hydratedBuffer,
left: layer.x + layerPositionOffset.offsetX,
top: layer.y + layerPositionOffset.offsetY,
width: layer.width,
height: layer.height,
});
}
if (options.optional) { if (options.optional) {
return false; return false;
} }
@ -645,7 +815,13 @@ export async function renderAgentPsdTemplate(params: {
const layerPositionOffset = getLayerPositionOffset(layer.id); const layerPositionOffset = getLayerPositionOffset(layer.id);
try { try {
const overlayBuffer = await readCachedBuffer(sourceUrl, "PSD 图层素材"); const overlayBuffer = await readReusableLayerBuffer(layer);
if (!overlayBuffer) {
if (options.optional) {
return false;
}
throw new Error("Missing PSD layer asset");
}
const didAppend = await appendBufferOverlay({ const didAppend = await appendBufferOverlay({
input: overlayBuffer, input: overlayBuffer,
left: layer.x + layerPositionOffset.offsetX, left: layer.x + layerPositionOffset.offsetX,
@ -693,6 +869,42 @@ export async function renderAgentPsdTemplate(params: {
}); });
} }
const appendedRenderSegmentIds = new Set<string>();
async function appendRenderSegmentOverlay(
segment: (typeof renderSegments)[number] | undefined
) {
if (
!shouldUseRenderSegments ||
!segment ||
appendedRenderSegmentIds.has(segment.id)
) {
return;
}
appendedRenderSegmentIds.add(segment.id);
const segmentBuffer = await readCachedBuffer(
segment.url,
"PSD 模板快速导出分层"
);
await appendBufferOverlay({
input: segmentBuffer,
left: 0,
top: 0,
width: canvasWidth,
height: canvasHeight,
});
}
async function appendRenderSegmentBeforeLayer(layerId: string) {
await appendRenderSegmentOverlay(renderSegmentByBeforeLayerId.get(layerId));
}
async function appendTopRenderSegments() {
for (const segment of topRenderSegments) {
await appendRenderSegmentOverlay(segment);
}
}
if ( if (
shouldUseRenderBaseFallback && shouldUseRenderBaseFallback &&
(!defaultSourceImageLayerId || hiddenLayerIdSet.has(defaultSourceImageLayerId)) (!defaultSourceImageLayerId || hiddenLayerIdSet.has(defaultSourceImageLayerId))
@ -701,12 +913,16 @@ export async function renderAgentPsdTemplate(params: {
} }
for (const layer of [...orderedTemplateLayers].reverse()) { for (const layer of [...orderedTemplateLayers].reverse()) {
if (shouldUseRenderSegments && isAgentPsdTemplateLayerUserEditable(layer)) {
await appendRenderSegmentBeforeLayer(layer.id);
}
if (hiddenLayerIdSet.has(layer.id)) { if (hiddenLayerIdSet.has(layer.id)) {
continue; continue;
} }
if (!isAgentPsdTemplateLayerUserEditable(layer)) { if (!isAgentPsdTemplateLayerUserEditable(layer)) {
if (shouldUseRenderBaseFallback) { if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
continue; continue;
} }
await appendStoredLayer(layer, { optional: shouldUseRenderBaseFallback }); await appendStoredLayer(layer, { optional: shouldUseRenderBaseFallback });
@ -1001,6 +1217,8 @@ export async function renderAgentPsdTemplate(params: {
} }
} }
await appendTopRenderSegments();
const transparentCanvas = await sharp({ const transparentCanvas = await sharp({
create: { create: {
width: canvasWidth, width: canvasWidth,

View File

@ -8,6 +8,7 @@ import {
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings"; import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import { import {
AGENT_PSD_TEMPLATE_ASSET_VERSION, AGENT_PSD_TEMPLATE_ASSET_VERSION,
hasAgentPsdTemplateRenderSegments,
isAgentPsdImageLayerSlot, isAgentPsdImageLayerSlot,
isAgentPsdLayerSuperLocked, isAgentPsdLayerSuperLocked,
isAgentPsdTemplateLayerUserEditable, isAgentPsdTemplateLayerUserEditable,
@ -21,6 +22,7 @@ import {
import { import {
parsePsdTemplateFileFromSource, parsePsdTemplateFileFromSource,
preparePsdTemplateRenderAssets, preparePsdTemplateRenderAssets,
uploadPreparedPsdTemplateRenderSegments,
} from "@/lib/agents/psd-template-parser"; } from "@/lib/agents/psd-template-parser";
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache"; import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
import { createStorageUploadSession } from "@/lib/storage/service"; import { createStorageUploadSession } from "@/lib/storage/service";
@ -66,6 +68,10 @@ function hasDedicatedRenderBase(template: AgentPsdTemplate) {
return Boolean(renderBaseUrl) && renderBaseUrl !== previewUrl; return Boolean(renderBaseUrl) && renderBaseUrl !== previewUrl;
} }
function hasStaticTemplateLayers(template: AgentPsdTemplate) {
return template.layers.some((layer) => !isAgentPsdTemplateLayerUserEditable(layer));
}
function repairEditableImageLayerSelection(template: AgentPsdTemplate) { function repairEditableImageLayerSelection(template: AgentPsdTemplate) {
const imageLayers = template.layers.filter(isAgentPsdImageLayerSlot); const imageLayers = template.layers.filter(isAgentPsdImageLayerSlot);
const unlockedImageLayers = imageLayers.filter( const unlockedImageLayers = imageLayers.filter(
@ -255,6 +261,8 @@ export async function repairAgentPsdTemplateAssetsIfNeeded(
Number(template.assetVersion || 0) !== AGENT_PSD_TEMPLATE_ASSET_VERSION; Number(template.assetVersion || 0) !== AGENT_PSD_TEMPLATE_ASSET_VERSION;
const needsRepair = needsAssetVersionRefresh || hasMissingLayerAssets(template); const needsRepair = needsAssetVersionRefresh || hasMissingLayerAssets(template);
const needsRenderBaseRepair = !hasDedicatedRenderBase(template); const needsRenderBaseRepair = !hasDedicatedRenderBase(template);
const needsRenderSegmentRepair =
hasStaticTemplateLayers(template) && !hasAgentPsdTemplateRenderSegments(template);
const fonts = await readAgentPsdFonts().catch(() => []); const fonts = await readAgentPsdFonts().catch(() => []);
const needsFontDetection = needsTextMetadataRepair(template); const needsFontDetection = needsTextMetadataRepair(template);
@ -311,7 +319,12 @@ export async function repairAgentPsdTemplateAssetsIfNeeded(
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index]) (layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
); );
if (!needsRepair && !hasFontRepair && !needsRenderBaseRepair) { if (
!needsRepair &&
!hasFontRepair &&
!needsRenderBaseRepair &&
!needsRenderSegmentRepair
) {
return template; return template;
} }
@ -335,11 +348,20 @@ export async function repairAgentPsdTemplateAssetsIfNeeded(
toArrayBuffer(preparedAssets.renderBaseBuffer) toArrayBuffer(preparedAssets.renderBaseBuffer)
) )
: null; : null;
const uploadedRenderSegments =
needsRenderSegmentRepair || needsAssetVersionRefresh
? await uploadPreparedPsdTemplateRenderSegments({
templateId: template.id,
segments: preparedAssets.renderSegments,
uploadAsset,
})
: null;
return { return {
...template, ...template,
renderBaseUrl: uploadedRenderBase?.url || template.renderBaseUrl, renderBaseUrl: uploadedRenderBase?.url || template.renderBaseUrl,
renderBasePath: uploadedRenderBase?.pathname || template.renderBasePath, renderBasePath: uploadedRenderBase?.pathname || template.renderBasePath,
renderSegments: uploadedRenderSegments || template.renderSegments,
layers: preparedAssets.layers, layers: preparedAssets.layers,
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION, assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),

View File

@ -26,6 +26,7 @@ export async function tryDeleteAgentPsdTemplateAssets(template: AgentPsdTemplate
template.filePath, template.filePath,
template.previewPath, template.previewPath,
template.renderBasePath, template.renderBasePath,
...(template.renderSegments || []).map((segment) => segment.path),
...template.layers.map((layer) => layer.previewPath), ...template.layers.map((layer) => layer.previewPath),
...template.layers ...template.layers
.filter(isAgentPsdImageLayerSlot) .filter(isAgentPsdImageLayerSlot)