fix: precompile PSD template render segments
This commit is contained in:
parent
720652b68a
commit
0a001fc5f4
@ -18,6 +18,7 @@ import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template
|
||||
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
|
||||
import {
|
||||
preparePsdTemplateRenderAssets,
|
||||
uploadPreparedPsdTemplateRenderSegments,
|
||||
} from "@/lib/agents/psd-template-parser";
|
||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||
@ -222,6 +223,11 @@ export async function PUT(
|
||||
preparedAssets.renderBaseBuffer.byteOffset + preparedAssets.renderBaseBuffer.byteLength
|
||||
) as ArrayBuffer
|
||||
);
|
||||
const uploadedRenderSegments = await uploadPreparedPsdTemplateRenderSegments({
|
||||
templateId: currentTemplate.id,
|
||||
segments: preparedAssets.renderSegments,
|
||||
uploadAsset,
|
||||
});
|
||||
|
||||
const updatedTemplate = {
|
||||
...currentTemplate,
|
||||
@ -234,6 +240,7 @@ export async function PUT(
|
||||
status: nextStatus,
|
||||
renderBaseUrl: uploadedRenderBase.url,
|
||||
renderBasePath: uploadedRenderBase.pathname,
|
||||
renderSegments: uploadedRenderSegments,
|
||||
layers: nextLayers,
|
||||
layerCount: nextLayers.length,
|
||||
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||
|
||||
@ -10,7 +10,10 @@ import {
|
||||
type AgentPsdTemplateLayerSlot,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
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 { createStorageUploadSession } from "@/lib/storage/service";
|
||||
|
||||
@ -181,7 +184,7 @@ async function backfillTemplateAssetsOnce(params: {
|
||||
...currentTemplate,
|
||||
parseStatus: "processing",
|
||||
parseProgress: Math.max(88, Number(currentTemplate.parseProgress || 0)),
|
||||
parseStage: "正在后台生成模板底图",
|
||||
parseStage: "正在后台生成模板底图和快速导出分层",
|
||||
parseError: undefined,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}),
|
||||
@ -191,6 +194,11 @@ async function backfillTemplateAssetsOnce(params: {
|
||||
`agent-psd-base-${template.id}-${Date.now()}.png`,
|
||||
toArrayBuffer(preparedAssets.renderBaseBuffer)
|
||||
);
|
||||
const uploadedRenderSegments = await uploadPreparedPsdTemplateRenderSegments({
|
||||
templateId: template.id,
|
||||
segments: preparedAssets.renderSegments,
|
||||
uploadAsset,
|
||||
});
|
||||
|
||||
await updateTemplateById({
|
||||
templateId: template.id,
|
||||
@ -205,6 +213,7 @@ async function backfillTemplateAssetsOnce(params: {
|
||||
...currentTemplate,
|
||||
renderBaseUrl: uploadedRenderBase.url,
|
||||
renderBasePath: uploadedRenderBase.pathname,
|
||||
renderSegments: uploadedRenderSegments,
|
||||
layers: mergedLayers,
|
||||
layerCount: mergedLayers.length,
|
||||
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||
|
||||
@ -4,7 +4,7 @@ import type { AgentPsdTemplateCategoryTagConfig } from "@/lib/agents/psd-templat
|
||||
import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config";
|
||||
|
||||
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 AgentPsdTemplateParseStatus =
|
||||
@ -83,6 +83,13 @@ export type AgentPsdTemplateLayerSlot =
|
||||
| AgentPsdTemplateImageLayerSlot
|
||||
| AgentPsdTemplateTextLayerSlot;
|
||||
|
||||
export interface AgentPsdTemplateRenderSegment {
|
||||
id: string;
|
||||
beforeLayerId?: string;
|
||||
url: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface AgentPsdTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -103,6 +110,7 @@ export interface AgentPsdTemplate {
|
||||
previewPath?: string;
|
||||
renderBaseUrl?: string;
|
||||
renderBasePath?: string;
|
||||
renderSegments?: AgentPsdTemplateRenderSegment[];
|
||||
width: number;
|
||||
height: 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) {
|
||||
if (!Array.isArray(value)) return [] as AgentPsdTemplate[];
|
||||
|
||||
@ -512,6 +550,7 @@ export function parseAgentPsdTemplates(value: unknown) {
|
||||
renderBaseUrl:
|
||||
normalizeOptionalUrl(record.renderBaseUrl) || normalizeOptionalUrl(record.previewUrl),
|
||||
renderBasePath: normalizeText(record.renderBasePath) || undefined,
|
||||
renderSegments: parseAgentPsdTemplateRenderSegments(record.renderSegments),
|
||||
width,
|
||||
height,
|
||||
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(
|
||||
template: Pick<AgentPsdTemplate, "renderBaseUrl" | "renderBasePath" | "previewUrl">
|
||||
) {
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
isAgentPsdTextLayerSlot,
|
||||
type AgentPsdTemplateImageContentBounds,
|
||||
type AgentPsdTemplateLayerSlot,
|
||||
type AgentPsdTemplateRenderSegment,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
|
||||
const DEFAULT_PSD_TEMPLATE_ASSET_CONCURRENCY = 4;
|
||||
@ -119,6 +120,12 @@ export interface PsdTemplateSourceLayerEntry {
|
||||
composite: () => Promise<Uint8ClampedArray>;
|
||||
}
|
||||
|
||||
export interface PreparedPsdTemplateRenderSegment {
|
||||
id: string;
|
||||
beforeLayerId?: string;
|
||||
buffer: Buffer;
|
||||
}
|
||||
|
||||
function resolveVisibleContentBounds(params: {
|
||||
rgba: Uint8ClampedArray;
|
||||
width: number;
|
||||
@ -666,6 +673,35 @@ function toArrayBuffer(buffer: Buffer) {
|
||||
) 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>(
|
||||
items: T[],
|
||||
concurrency: number,
|
||||
@ -727,8 +763,13 @@ export async function preparePsdTemplateRenderAssets(params: {
|
||||
fileBuffer: params.fileBuffer,
|
||||
});
|
||||
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 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(
|
||||
params.layers
|
||||
.filter(isAgentPsdTemplateLayerUserEditable)
|
||||
@ -900,19 +941,28 @@ export async function preparePsdTemplateRenderAssets(params: {
|
||||
nextLayers[index] = layer;
|
||||
});
|
||||
|
||||
const baseEntries = [...layerEntries]
|
||||
const drawableEntries = [...layerEntries]
|
||||
.reverse()
|
||||
.filter(
|
||||
(entry) =>
|
||||
!editableLayerIdSet.has(entry.layer.layerId) &&
|
||||
entry.node &&
|
||||
typeof entry.node.composite === "function"
|
||||
);
|
||||
.filter((entry) => entry.node && typeof entry.node.composite === "function");
|
||||
|
||||
const baseCompositeResults = await mapWithConcurrency(
|
||||
baseEntries,
|
||||
const layerCompositeResults = await mapWithConcurrency(
|
||||
drawableEntries,
|
||||
assetConcurrency,
|
||||
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 {
|
||||
const rgba = await entry.node.composite!();
|
||||
const overlayBuffer = await rgbaToPngBuffer(
|
||||
@ -926,8 +976,8 @@ export async function preparePsdTemplateRenderAssets(params: {
|
||||
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))),
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
});
|
||||
if (!fittedOverlay) {
|
||||
return null;
|
||||
@ -936,6 +986,8 @@ export async function preparePsdTemplateRenderAssets(params: {
|
||||
return {
|
||||
index,
|
||||
input: fittedOverlay,
|
||||
beforeLayerId: "",
|
||||
editableBoundary: false,
|
||||
};
|
||||
} catch (error) {
|
||||
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(
|
||||
(
|
||||
result
|
||||
): result is {
|
||||
index: 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);
|
||||
|
||||
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)
|
||||
.composite(baseCompositeInputs)
|
||||
.png()
|
||||
.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 {
|
||||
layers: nextLayers,
|
||||
renderBaseBuffer,
|
||||
renderSegments,
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ import "server-only";
|
||||
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import { fetchRemoteAssetBuffer } from "@/lib/agents/psd-remote-asset";
|
||||
import { loadPsdTemplateSource } from "@/lib/agents/psd-template-parser";
|
||||
import {
|
||||
hasAgentPsdTemplateStoredLayerSource,
|
||||
isAgentPsdImageLayerSlot,
|
||||
isAgentPsdLayerSuperLocked,
|
||||
isAgentPsdTemplateLayerUserEditable,
|
||||
isAgentPsdTextLayerSlot,
|
||||
resolveAgentPsdTextLayerEffectiveAlign,
|
||||
@ -181,8 +183,7 @@ function getDefaultSourceImageLayer(
|
||||
) {
|
||||
const editableImageLayers = layers.filter(
|
||||
(layer): layer is AgentPsdTemplateImageLayerSlot =>
|
||||
isAgentPsdTemplateLayerUserEditable(layer) &&
|
||||
isAgentPsdImageLayerSlot(layer)
|
||||
layer.isEditable && isAgentPsdImageLayerSlot(layer)
|
||||
);
|
||||
|
||||
return (
|
||||
@ -462,18 +463,68 @@ export async function renderAgentPsdTemplate(params: {
|
||||
const hasDedicatedRenderBase =
|
||||
Boolean(renderBaseUrl) &&
|
||||
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 =
|
||||
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(
|
||||
(layer) =>
|
||||
!isAgentPsdTemplateLayerUserEditable(layer) &&
|
||||
shouldRenderAsStaticLayer(layer) &&
|
||||
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 =
|
||||
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) {
|
||||
const layer = layerMap.get(layerId);
|
||||
if (layer && isAgentPsdLayerSuperLocked(layer)) {
|
||||
return normalizeTextPositionOffset();
|
||||
}
|
||||
return normalizeTextPositionOffset(layerPositionOffsets[layerId]);
|
||||
}
|
||||
|
||||
@ -516,17 +567,22 @@ export async function renderAgentPsdTemplate(params: {
|
||||
if (shouldUseRenderBaseFallback) {
|
||||
pushAsset(renderBaseUrl, "PSD 模板底图");
|
||||
}
|
||||
if (shouldUseRenderSegments) {
|
||||
for (const segment of renderSegments) {
|
||||
pushAsset(segment.url, "PSD 模板快速导出分层");
|
||||
}
|
||||
}
|
||||
|
||||
for (const layer of orderedTemplateLayers) {
|
||||
if (hiddenLayerIdSet.has(layer.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const storedLayerSourceUrl = getStoredLayerSourceUrl(layer);
|
||||
if (!isAgentPsdTemplateLayerUserEditable(layer)) {
|
||||
if (shouldUseRenderBaseFallback) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const storedLayerSourceUrl = getStoredLayerSourceUrl(layer);
|
||||
if (shouldRenderAsStaticLayer(layer)) {
|
||||
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
|
||||
continue;
|
||||
}
|
||||
pushAsset(
|
||||
storedLayerSourceUrl,
|
||||
"PSD 图层素材",
|
||||
@ -601,6 +657,111 @@ export async function renderAgentPsdTemplate(params: {
|
||||
|
||||
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: {
|
||||
input: Buffer;
|
||||
@ -631,10 +792,19 @@ export async function renderAgentPsdTemplate(params: {
|
||||
options: { optional?: boolean } = {}
|
||||
) {
|
||||
const sourceUrl = getStoredLayerSourceUrl(layer);
|
||||
if (
|
||||
(!hasAgentPsdTemplateStoredLayerSource(layer) && !sourceUrl) ||
|
||||
!sourceUrl
|
||||
) {
|
||||
if ((!hasAgentPsdTemplateStoredLayerSource(layer) && !sourceUrl) || !sourceUrl) {
|
||||
const hydratedBuffer = await readPsdSourceLayerBuffer(layer);
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
@ -645,7 +815,13 @@ export async function renderAgentPsdTemplate(params: {
|
||||
|
||||
const layerPositionOffset = getLayerPositionOffset(layer.id);
|
||||
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({
|
||||
input: overlayBuffer,
|
||||
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 (
|
||||
shouldUseRenderBaseFallback &&
|
||||
(!defaultSourceImageLayerId || hiddenLayerIdSet.has(defaultSourceImageLayerId))
|
||||
@ -701,12 +913,16 @@ export async function renderAgentPsdTemplate(params: {
|
||||
}
|
||||
|
||||
for (const layer of [...orderedTemplateLayers].reverse()) {
|
||||
if (shouldUseRenderSegments && isAgentPsdTemplateLayerUserEditable(layer)) {
|
||||
await appendRenderSegmentBeforeLayer(layer.id);
|
||||
}
|
||||
|
||||
if (hiddenLayerIdSet.has(layer.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isAgentPsdTemplateLayerUserEditable(layer)) {
|
||||
if (shouldUseRenderBaseFallback) {
|
||||
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
|
||||
continue;
|
||||
}
|
||||
await appendStoredLayer(layer, { optional: shouldUseRenderBaseFallback });
|
||||
@ -1001,6 +1217,8 @@ export async function renderAgentPsdTemplate(params: {
|
||||
}
|
||||
}
|
||||
|
||||
await appendTopRenderSegments();
|
||||
|
||||
const transparentCanvas = await sharp({
|
||||
create: {
|
||||
width: canvasWidth,
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||
import {
|
||||
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||
hasAgentPsdTemplateRenderSegments,
|
||||
isAgentPsdImageLayerSlot,
|
||||
isAgentPsdLayerSuperLocked,
|
||||
isAgentPsdTemplateLayerUserEditable,
|
||||
@ -21,6 +22,7 @@ import {
|
||||
import {
|
||||
parsePsdTemplateFileFromSource,
|
||||
preparePsdTemplateRenderAssets,
|
||||
uploadPreparedPsdTemplateRenderSegments,
|
||||
} from "@/lib/agents/psd-template-parser";
|
||||
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
|
||||
import { createStorageUploadSession } from "@/lib/storage/service";
|
||||
@ -66,6 +68,10 @@ function hasDedicatedRenderBase(template: AgentPsdTemplate) {
|
||||
return Boolean(renderBaseUrl) && renderBaseUrl !== previewUrl;
|
||||
}
|
||||
|
||||
function hasStaticTemplateLayers(template: AgentPsdTemplate) {
|
||||
return template.layers.some((layer) => !isAgentPsdTemplateLayerUserEditable(layer));
|
||||
}
|
||||
|
||||
function repairEditableImageLayerSelection(template: AgentPsdTemplate) {
|
||||
const imageLayers = template.layers.filter(isAgentPsdImageLayerSlot);
|
||||
const unlockedImageLayers = imageLayers.filter(
|
||||
@ -255,6 +261,8 @@ export async function repairAgentPsdTemplateAssetsIfNeeded(
|
||||
Number(template.assetVersion || 0) !== AGENT_PSD_TEMPLATE_ASSET_VERSION;
|
||||
const needsRepair = needsAssetVersionRefresh || hasMissingLayerAssets(template);
|
||||
const needsRenderBaseRepair = !hasDedicatedRenderBase(template);
|
||||
const needsRenderSegmentRepair =
|
||||
hasStaticTemplateLayers(template) && !hasAgentPsdTemplateRenderSegments(template);
|
||||
const fonts = await readAgentPsdFonts().catch(() => []);
|
||||
const needsFontDetection = needsTextMetadataRepair(template);
|
||||
|
||||
@ -311,7 +319,12 @@ export async function repairAgentPsdTemplateAssetsIfNeeded(
|
||||
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
|
||||
);
|
||||
|
||||
if (!needsRepair && !hasFontRepair && !needsRenderBaseRepair) {
|
||||
if (
|
||||
!needsRepair &&
|
||||
!hasFontRepair &&
|
||||
!needsRenderBaseRepair &&
|
||||
!needsRenderSegmentRepair
|
||||
) {
|
||||
return template;
|
||||
}
|
||||
|
||||
@ -335,11 +348,20 @@ export async function repairAgentPsdTemplateAssetsIfNeeded(
|
||||
toArrayBuffer(preparedAssets.renderBaseBuffer)
|
||||
)
|
||||
: null;
|
||||
const uploadedRenderSegments =
|
||||
needsRenderSegmentRepair || needsAssetVersionRefresh
|
||||
? await uploadPreparedPsdTemplateRenderSegments({
|
||||
templateId: template.id,
|
||||
segments: preparedAssets.renderSegments,
|
||||
uploadAsset,
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
...template,
|
||||
renderBaseUrl: uploadedRenderBase?.url || template.renderBaseUrl,
|
||||
renderBasePath: uploadedRenderBase?.pathname || template.renderBasePath,
|
||||
renderSegments: uploadedRenderSegments || template.renderSegments,
|
||||
layers: preparedAssets.layers,
|
||||
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
@ -26,6 +26,7 @@ export async function tryDeleteAgentPsdTemplateAssets(template: AgentPsdTemplate
|
||||
template.filePath,
|
||||
template.previewPath,
|
||||
template.renderBasePath,
|
||||
...(template.renderSegments || []).map((segment) => segment.path),
|
||||
...template.layers.map((layer) => layer.previewPath),
|
||||
...template.layers
|
||||
.filter(isAgentPsdImageLayerSlot)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user