From 2959b5d340a3cdc12b297568fbbcfc5b44957521 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Thu, 16 Jul 2026 15:45:02 +0800 Subject: [PATCH] feat(image-settings): add transparent background toggle for image generation --- CHANGELOG.md | 2 ++ .../canvas/canvas-config-node-panel.tsx | 1 + .../canvas/canvas-node-prompt-panel.tsx | 1 + web/src/components/image-settings-panel.tsx | 14 +++++++++++++- web/src/pages/canvas/project.tsx | 5 ++++- web/src/services/api/image.ts | 17 +++++++++++++++-- web/src/stores/use-config-store.ts | 2 ++ web/src/types/canvas.ts | 1 + 8 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb8350..995e059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased ++ [新增] 图像设置新增「透明背景」开关,开启后生成无背景的透明图像。 + ## v0.8.1 - 2026-07-16 + [新增] 插件 SDK 扩展:AI 生成能力、面板控制能力。 diff --git a/web/src/components/canvas/canvas-config-node-panel.tsx b/web/src/components/canvas/canvas-config-node-panel.tsx index 4e265bf..319c77e 100644 --- a/web/src/components/canvas/canvas-config-node-panel.tsx +++ b/web/src/components/canvas/canvas-config-node-panel.tsx @@ -156,6 +156,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can model, quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality, size: node.metadata?.size || globalConfig.size || defaultConfig.size, + background: node.metadata?.background ?? globalConfig.background ?? defaultConfig.background, videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds, vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality, videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio, diff --git a/web/src/components/canvas/canvas-node-prompt-panel.tsx b/web/src/components/canvas/canvas-node-prompt-panel.tsx index a96e32b..e1dc80b 100644 --- a/web/src/components/canvas/canvas-node-prompt-panel.tsx +++ b/web/src/components/canvas/canvas-node-prompt-panel.tsx @@ -145,6 +145,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can model, quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality, size: node.metadata?.size || globalConfig.size || defaultConfig.size, + background: node.metadata?.background ?? globalConfig.background ?? defaultConfig.background, videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds, vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality, videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio, diff --git a/web/src/components/image-settings-panel.tsx b/web/src/components/image-settings-panel.tsx index ee86d61..725e3d1 100644 --- a/web/src/components/image-settings-panel.tsx +++ b/web/src/components/image-settings-panel.tsx @@ -33,7 +33,7 @@ export const imageAspectOptions = aspectOptions.map((item) => ({ value: item.siz type ImageSettingsPanelProps = { config: AiConfig; - onConfigChange: (key: "quality" | "size" | "count", value: string) => void; + onConfigChange: (key: "quality" | "size" | "count" | "background", value: string) => void; theme: CanvasTheme; showTitle?: boolean; className?: string; @@ -46,6 +46,7 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle = const quality = config.quality || "auto"; const count = Math.max(1, Math.min(maxCount, Math.floor(Math.abs(Number(config.count)) || 1))); const activeSize = config.size || "auto"; + const transparentBackground = config.background === "transparent"; const selectedAspect = aspectOptions.find((item) => (item.size || item.value) === activeSize || item.value === activeSize); const dimensions = readSizeDimensions(activeSize, selectedAspect || aspectOptions[0]); const selectAspect = (value: string) => { @@ -117,6 +118,17 @@ export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle = ))} +
+
+ 透明背景 +
+ 开启后生成无背景的透明图像 +
+
+ event.stopPropagation()}> + onConfigChange("background", checked ? "transparent" : "")} /> + +
生成张数
diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index ae9d5fe..ee97e4b 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -2522,6 +2522,7 @@ function InfiniteCanvasPage() { model: savedImageMetadata.model || effectiveConfig.imageModel || effectiveConfig.model, quality: savedImageMetadata.quality || effectiveConfig.quality, size: savedImageMetadata.size || effectiveConfig.size, + background: savedImageMetadata.background ?? effectiveConfig.background, count: "1", } : { ...buildGenerationConfig(effectiveConfig, sourceNode, node.type === CanvasNodeType.Text ? "text" : node.type === CanvasNodeType.Video ? "video" : node.type === CanvasNodeType.Audio ? "audio" : "image"), count: "1" }; @@ -2579,7 +2580,7 @@ function InfiniteCanvasPage() { const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image]; const imageSize = fitNodeSize(uploadedImage.width, uploadedImage.height, imageConfig.width, imageConfig.height); const generationMetadata = savedImageMetadata?.generationType - ? { generationType: savedImageMetadata.generationType, model: generationConfig.model, size: generationConfig.size, quality: generationConfig.quality, count: savedImageMetadata.count || 1, references: savedImageMetadata.references } + ? { generationType: savedImageMetadata.generationType, model: generationConfig.model, size: generationConfig.size, quality: generationConfig.quality, ...(generationConfig.background ? { background: generationConfig.background } : {}), count: savedImageMetadata.count || 1, references: savedImageMetadata.references } : buildImageGenerationMetadata(useReferenceImages ? "edit" : "generation", generationConfig, 1, retryImages); setNodes((prev) => prev.map((item) => @@ -3282,6 +3283,7 @@ function buildImageGenerationMetadata(type: CanvasImageGenerationType, config: A model: config.model, size: config.size, quality: config.quality, + ...(config.background ? { background: config.background } : {}), count, references: references.map(referenceUrl).filter((url): url is string => Boolean(url)), }; @@ -3460,6 +3462,7 @@ function buildGenerationConfig(config: AiConfig, node: CanvasNodeData | undefine model: node?.metadata?.model || defaultModel || (mode === "audio" ? defaultConfig.audioModel : config.model || defaultConfig.model), quality: node?.metadata?.quality || config.quality || defaultConfig.quality, size: node?.metadata?.size || config.size || defaultConfig.size, + background: node?.metadata?.background ?? config.background ?? defaultConfig.background, videoSeconds: node?.metadata?.seconds || config.videoSeconds || defaultConfig.videoSeconds, vquality: node?.metadata?.vquality || config.vquality || defaultConfig.vquality, videoGenerateAudio: node?.metadata?.generateAudio || config.videoGenerateAudio || defaultConfig.videoGenerateAudio, diff --git a/web/src/services/api/image.ts b/web/src/services/api/image.ts index 3ffa00d..251bd50 100644 --- a/web/src/services/api/image.ts +++ b/web/src/services/api/image.ts @@ -122,6 +122,11 @@ function normalizeQuality(quality: string) { return QUALITY_BASE[normalized] ? normalized : undefined; } +/** Only "transparent" is forwarded; any other value (incl. empty) means keep the default opaque background. */ +function normalizeBackground(background: string | undefined) { + return background?.trim().toLowerCase() === "transparent" ? "transparent" : undefined; +} + /** Map "quality + ratio" to an explicit pixel dimension like "3840x2160". */ function resolveSize(quality: string | undefined, ratio: string): string { const parsedRatio = parseImageRatio(ratio); @@ -661,6 +666,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option if (script) { const quality = normalizeQuality(config.quality); const requestSize = resolveRequestSize(quality, config.size); + const background = normalizeBackground(config.background); try { const result = await runModelPlugin({ capability: "image", @@ -668,7 +674,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option config: requestConfig, prompt: withSystemPrompt(requestConfig, prompt), images: [], - params: { size: requestSize, quality, count: n }, + params: { size: requestSize, quality, count: n, ...(background ? { background } : {}) }, signal: options?.signal, }); return normalizePluginImages(result).map((dataUrl) => ({ id: nanoid(), dataUrl })); @@ -685,6 +691,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option } const quality = normalizeQuality(config.quality); const requestSize = resolveRequestSize(quality, config.size); + const background = normalizeBackground(config.background); try { const response = await axios.post( aiApiUrl(requestConfig, "/images/generations"), @@ -694,6 +701,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option n, ...(quality ? { quality } : {}), ...(requestSize ? { size: requestSize } : {}), + ...(background ? { background } : {}), response_format: "b64_json", output_format: IMAGE_OUTPUT_FORMAT, }, @@ -717,6 +725,7 @@ export async function requestEdit(config: AiConfig, prompt: string, references: if (script) { const quality = normalizeQuality(config.quality); const requestSize = resolveRequestSize(quality, config.size); + const background = normalizeBackground(config.background); const refs = await Promise.all(references.map((image) => imageToDataUrl(image))); try { const result = await runModelPlugin({ @@ -725,7 +734,7 @@ export async function requestEdit(config: AiConfig, prompt: string, references: config: requestConfig, prompt: withSystemPrompt(requestConfig, requestPrompt), images: refs, - params: { size: requestSize, quality, count: n }, + params: { size: requestSize, quality, count: n, ...(background ? { background } : {}) }, signal: options?.signal, }); return normalizePluginImages(result).map((dataUrl) => ({ id: nanoid(), dataUrl })); @@ -743,6 +752,7 @@ export async function requestEdit(config: AiConfig, prompt: string, references: } const quality = normalizeQuality(config.quality); const requestSize = resolveRequestSize(quality, config.size); + const background = normalizeBackground(config.background); const formData = new FormData(); formData.set("model", requestConfig.model); formData.set("prompt", withSystemPrompt(requestConfig, requestPrompt)); @@ -755,6 +765,9 @@ export async function requestEdit(config: AiConfig, prompt: string, references: if (requestSize) { formData.set("size", requestSize); } + if (background) { + formData.set("background", background); + } const files = await Promise.all(references.map(async (image) => dataUrlToFile({ ...image, dataUrl: await imageToDataUrl(image) }))); files.forEach((file) => formData.append("image", file)); if (mask) formData.set("mask", dataUrlToFile(mask)); diff --git a/web/src/stores/use-config-store.ts b/web/src/stores/use-config-store.ts index de9d1f4..ffe2864 100644 --- a/web/src/stores/use-config-store.ts +++ b/web/src/stores/use-config-store.ts @@ -44,6 +44,7 @@ export type AiConfig = { models: string[]; quality: string; size: string; + background: string; count: string; canvasImageCount: string; }; @@ -99,6 +100,7 @@ export const defaultConfig: AiConfig = { models: ["default::gpt-image-2", "default::grok-imagine-video", "default::gpt-5.5", "default::gpt-4o-mini-tts"], quality: "auto", size: "1:1", + background: "", count: "1", canvasImageCount: "3", }; diff --git a/web/src/types/canvas.ts b/web/src/types/canvas.ts index 4256c36..48a7d76 100644 --- a/web/src/types/canvas.ts +++ b/web/src/types/canvas.ts @@ -37,6 +37,7 @@ export type CanvasNodeMetadata = { model?: string; size?: string; quality?: string; + background?: string; count?: number; seconds?: string; vquality?: string;