可编辑文字
{selectedTextLayer.layerName}
diff --git a/lib/agents/psd-image-placement.ts b/lib/agents/psd-image-placement.ts
index e8fda7984..61978e646 100644
--- a/lib/agents/psd-image-placement.ts
+++ b/lib/agents/psd-image-placement.ts
@@ -1,4 +1,7 @@
-import type { AgentPsdTemplateReplaceMode } from "@/lib/agents/psd-template-config";
+import type {
+ AgentPsdTemplateImageContentBounds,
+ AgentPsdTemplateReplaceMode,
+} from "@/lib/agents/psd-template-config";
export interface AgentPsdImagePlacement {
scale: number;
@@ -69,19 +72,59 @@ function resolveBaseFittedSize(params: {
};
}
+function normalizeContainerBounds(params: {
+ containerWidth: number;
+ containerHeight: number;
+ contentBounds?: AgentPsdTemplateImageContentBounds;
+}) {
+ const { containerWidth, containerHeight, contentBounds } = params;
+ if (!contentBounds) {
+ return {
+ left: 0,
+ top: 0,
+ width: containerWidth,
+ height: containerHeight,
+ };
+ }
+
+ const left = Math.max(0, Math.min(containerWidth - 1, Math.round(contentBounds.left)));
+ const top = Math.max(0, Math.min(containerHeight - 1, Math.round(contentBounds.top)));
+ const width = Math.max(
+ 1,
+ Math.min(Math.round(contentBounds.width), Math.max(1, containerWidth - left))
+ );
+ const height = Math.max(
+ 1,
+ Math.min(Math.round(contentBounds.height), Math.max(1, containerHeight - top))
+ );
+
+ return {
+ left,
+ top,
+ width,
+ height,
+ };
+}
+
export function resolveAgentPsdImagePlacementFrame(params: {
containerWidth: number;
containerHeight: number;
imageWidth: number;
imageHeight: number;
replaceMode: AgentPsdTemplateReplaceMode;
+ contentBounds?: AgentPsdTemplateImageContentBounds;
placement?: Partial
| null;
}) {
const { containerWidth, containerHeight, imageWidth, imageHeight, replaceMode } = params;
const placement = normalizeAgentPsdImagePlacement(params.placement);
- const baseFrame = resolveBaseFittedSize({
+ const contentBounds = normalizeContainerBounds({
containerWidth,
containerHeight,
+ contentBounds: params.contentBounds,
+ });
+ const baseFrame = resolveBaseFittedSize({
+ containerWidth: contentBounds.width,
+ containerHeight: contentBounds.height,
imageWidth,
imageHeight,
replaceMode,
@@ -89,8 +132,16 @@ export function resolveAgentPsdImagePlacementFrame(params: {
const width = baseFrame.width * placement.scale;
const height = baseFrame.height * placement.scale;
- const left = baseFrame.left - (width - baseFrame.width) / 2 + placement.offsetX;
- const top = baseFrame.top - (height - baseFrame.height) / 2 + placement.offsetY;
+ const left =
+ contentBounds.left +
+ baseFrame.left -
+ (width - baseFrame.width) / 2 +
+ placement.offsetX;
+ const top =
+ contentBounds.top +
+ baseFrame.top -
+ (height - baseFrame.height) / 2 +
+ placement.offsetY;
return {
width,
diff --git a/lib/agents/psd-template-config.ts b/lib/agents/psd-template-config.ts
index a312d7a83..a1db31098 100644
--- a/lib/agents/psd-template-config.ts
+++ b/lib/agents/psd-template-config.ts
@@ -2,7 +2,7 @@ import type { AgentPsdTextColorConfig } from "@/types/agent";
import type { AgentPsdTemplateSecondaryCategoryConfig } from "@/lib/agents/psd-template-secondary-category-config";
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
-export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 3;
+export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 4;
export type AgentPsdTemplateStatus = "active" | "disabled";
export type AgentPsdTemplateParseStatus =
@@ -15,6 +15,13 @@ export type AgentPsdTemplateSlotType = "image" | "text";
export type AgentPsdTextAlign = "left" | "center" | "right";
export type AgentPsdTextStyleSource = "auto" | "custom";
+export interface AgentPsdTemplateImageContentBounds {
+ left: number;
+ top: number;
+ width: number;
+ height: number;
+}
+
export interface AgentPsdTemplateBaseLayerSlot {
id: string;
layerId: string;
@@ -39,6 +46,7 @@ export interface AgentPsdTemplateImageLayerSlot
isDefaultResultSource: boolean;
allowUserUploadOverride: boolean;
replaceMode: AgentPsdTemplateReplaceMode;
+ contentBounds?: AgentPsdTemplateImageContentBounds;
maskUrl?: string;
maskPath?: string;
}
@@ -204,6 +212,49 @@ export function normalizeAgentPsdTemplateReplaceMode(
return "cover";
}
+function normalizeAgentPsdTemplateImageContentBounds(
+ value: unknown,
+ containerWidth: number,
+ containerHeight: number
+): AgentPsdTemplateImageContentBounds | undefined {
+ if (!value || typeof value !== "object") {
+ return undefined;
+ }
+
+ const record = value as Record;
+ const left = normalizePositiveInteger(record.left);
+ const top = normalizePositiveInteger(record.top);
+ const width = normalizePositiveInteger(record.width);
+ const height = normalizePositiveInteger(record.height);
+
+ if (width <= 0 || height <= 0) {
+ return undefined;
+ }
+
+ const clampedLeft = Math.min(left, Math.max(0, containerWidth - 1));
+ const clampedTop = Math.min(top, Math.max(0, containerHeight - 1));
+ const maxWidth = Math.max(1, containerWidth - clampedLeft);
+ const maxHeight = Math.max(1, containerHeight - clampedTop);
+ const clampedWidth = Math.min(width, maxWidth);
+ const clampedHeight = Math.min(height, maxHeight);
+
+ if (
+ clampedLeft === 0 &&
+ clampedTop === 0 &&
+ clampedWidth === containerWidth &&
+ clampedHeight === containerHeight
+ ) {
+ return undefined;
+ }
+
+ return {
+ left: clampedLeft,
+ top: clampedTop,
+ width: clampedWidth,
+ height: clampedHeight,
+ };
+}
+
export function normalizeAgentPsdTemplateSlotType(
value: unknown
): AgentPsdTemplateSlotType {
@@ -311,6 +362,11 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
isDefaultResultSource: isEditable && record.isDefaultResultSource === true,
allowUserUploadOverride: isEditable ? record.allowUserUploadOverride !== false : false,
replaceMode: normalizeAgentPsdTemplateReplaceMode(record.replaceMode),
+ contentBounds: normalizeAgentPsdTemplateImageContentBounds(
+ record.contentBounds,
+ width,
+ height
+ ),
maskUrl: normalizeOptionalUrl(record.maskUrl),
maskPath: normalizeText(record.maskPath) || undefined,
});
diff --git a/lib/agents/psd-template-parser.ts b/lib/agents/psd-template-parser.ts
index 0bf9dbc6d..a6403f207 100644
--- a/lib/agents/psd-template-parser.ts
+++ b/lib/agents/psd-template-parser.ts
@@ -6,6 +6,7 @@ import { analyzeTextLayerPixels, normalizeHexColor } from "@/lib/agents/psd-text
import {
isAgentPsdImageLayerSlot,
isAgentPsdTextLayerSlot,
+ type AgentPsdTemplateImageContentBounds,
type AgentPsdTemplateLayerSlot,
} from "@/lib/agents/psd-template-config";
@@ -104,6 +105,44 @@ export interface PsdTemplateSourceLayerEntry {
composite: () => Promise;
}
+function resolveVisibleContentBounds(params: {
+ rgba: Uint8ClampedArray;
+ width: number;
+ height: number;
+}): AgentPsdTemplateImageContentBounds | undefined {
+ const { rgba, width, height } = params;
+ const pixelCount = width * height;
+ let minX = width;
+ let minY = height;
+ let maxX = -1;
+ let maxY = -1;
+
+ for (let index = 0; index < pixelCount; index += 1) {
+ const alpha = rgba[index * 4 + 3] || 0;
+ if (alpha <= 0) {
+ continue;
+ }
+
+ const x = index % width;
+ const y = Math.floor(index / width);
+ if (x < minX) minX = x;
+ if (x > maxX) maxX = x;
+ if (y < minY) minY = y;
+ if (y > maxY) maxY = y;
+ }
+
+ if (maxX < minX || maxY < minY) {
+ return undefined;
+ }
+
+ return {
+ left: minX,
+ top: minY,
+ width: maxX - minX + 1,
+ height: maxY - minY + 1,
+ };
+}
+
function normalizeLayerName(value: unknown, fallback: string) {
const text = String(value || "").trim();
return text || fallback;
@@ -522,6 +561,11 @@ export async function preparePsdTemplateRenderAssets(params: {
try {
const rgba = await entry.node.composite();
+ const visibleContentBounds = resolveVisibleContentBounds({
+ rgba,
+ width: entry.layer.width,
+ height: entry.layer.height,
+ });
if (params.forceRefreshLayerAssets || !targetLayer.previewUrl) {
const thumbnailBuffer = await createLayerThumbnailBuffer({
@@ -623,6 +667,13 @@ export async function preparePsdTemplateRenderAssets(params: {
!targetLayer.isEditable ||
(!params.forceRefreshLayerAssets && targetLayer.maskUrl)
) {
+ const layerIndex = nextLayers.findIndex((layer) => layer.id === targetLayer.id);
+ if (layerIndex >= 0 && isAgentPsdImageLayerSlot(nextLayers[layerIndex])) {
+ nextLayers[layerIndex] = {
+ ...nextLayers[layerIndex],
+ contentBounds: visibleContentBounds,
+ };
+ }
continue;
}
@@ -658,6 +709,7 @@ export async function preparePsdTemplateRenderAssets(params: {
if (layerIndex >= 0 && isAgentPsdImageLayerSlot(nextLayers[layerIndex])) {
nextLayers[layerIndex] = {
...nextLayers[layerIndex],
+ contentBounds: visibleContentBounds,
maskUrl: uploaded.url,
maskPath: uploaded.pathname,
};
diff --git a/lib/agents/psd-template-renderer.ts b/lib/agents/psd-template-renderer.ts
index 5f9262a0a..d28c0dfbb 100644
--- a/lib/agents/psd-template-renderer.ts
+++ b/lib/agents/psd-template-renderer.ts
@@ -466,6 +466,7 @@ export async function renderAgentPsdTemplate(params: {
imageWidth: Math.max(1, Number(metadata.width || 1)),
imageHeight: Math.max(1, Number(metadata.height || 1)),
replaceMode: layer.replaceMode,
+ contentBounds: layer.contentBounds,
placement: imagePlacements[layer.id],
});