diff --git a/app/api/admin/psd-templates/[id]/route.ts b/app/api/admin/psd-templates/[id]/route.ts index 92baf4cf8..0c2003979 100644 --- a/app/api/admin/psd-templates/[id]/route.ts +++ b/app/api/admin/psd-templates/[id]/route.ts @@ -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, diff --git a/lib/agents/psd-template-asset-backfill.ts b/lib/agents/psd-template-asset-backfill.ts index aceb30a3e..19a4e903b 100644 --- a/lib/agents/psd-template-asset-backfill.ts +++ b/lib/agents/psd-template-asset-backfill.ts @@ -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, diff --git a/lib/agents/psd-template-config.ts b/lib/agents/psd-template-config.ts index 8f2d6949a..0b6921118 100644 --- a/lib/agents/psd-template-config.ts +++ b/lib/agents/psd-template-config.ts @@ -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(); + + for (const entry of value) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + continue; + } + + const record = entry as Record; + 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 +) { + return Boolean( + template.renderSegments?.some((segment) => String(segment.url || "").trim()) + ); +} + export function hasAgentPsdTemplateDedicatedRenderBase( template: Pick ) { diff --git a/lib/agents/psd-template-parser.ts b/lib/agents/psd-template-parser.ts index 22498f2ca..c62b6c870 100644 --- a/lib/agents/psd-template-parser.ts +++ b/lib/agents/psd-template-parser.ts @@ -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; } +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( 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, }; } diff --git a/lib/agents/psd-template-renderer.ts b/lib/agents/psd-template-renderer.ts index 750a53140..28d2e1fbf 100644 --- a/lib/agents/psd-template-renderer.ts +++ b/lib/agents/psd-template-renderer.ts @@ -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; + } + > + > | null = null; + const psdSourceLayerBufferPromiseMap = new Map>(); + + 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; + } + >(); + 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(); + 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, diff --git a/lib/agents/psd-template-repair.ts b/lib/agents/psd-template-repair.ts index 11a590181..bcd260523 100644 --- a/lib/agents/psd-template-repair.ts +++ b/lib/agents/psd-template-repair.ts @@ -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(), diff --git a/lib/agents/psd-template-settings.ts b/lib/agents/psd-template-settings.ts index f2866c0ea..3701a5198 100644 --- a/lib/agents/psd-template-settings.ts +++ b/lib/agents/psd-template-settings.ts @@ -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)