From 53be242e894a6e5e9a8e73c7e75a499a65ed2c14 Mon Sep 17 00:00:00 2001 From: 134 Date: Fri, 3 Jul 2026 23:19:17 +0800 Subject: [PATCH] fix: clamp scale patch alpha to edit regions --- app/api/canvas/local-edit/route.ts | 29 ++++++- lib/canvas/generate-image.ts | 131 ++++++++++++++++++++++++++--- 2 files changed, 146 insertions(+), 14 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index ae64a4f93..8b9860264 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -790,6 +790,29 @@ function buildScaleAdjustPatchCompositeRegions( ]); } +function buildScaleAdjustProtectedChangeRegions( + regions: NormalizedLocalEditRegion[], + scalePercent?: ScaleAdjustPercent | null +) { + return regions.flatMap((region) => { + const targetRect = getScaleAdjustTargetRect(region, scalePercent); + const unionRect = getRectUnion(region.rect, targetRect); + const marginX = Math.min( + 0.045, + Math.max(0.012, unionRect.width * 0.08) + ); + const marginY = Math.min( + 0.045, + Math.max(0.012, unionRect.height * 0.08) + ); + + return [ + expandRectWithMargins(region.rect, marginX, marginY), + expandRectWithMargins(targetRect, marginX, marginY), + ]; + }); +} + function getScaleAdjustPatchWorkspaceRect( regions: NormalizedLocalEditRegion[], scalePercent?: ScaleAdjustPercent | null @@ -3873,9 +3896,13 @@ export async function POST(request: NextRequest) { const scaleAdjustPatchCompositeRegions = isScaleAdjustOperation ? buildScaleAdjustPatchCompositeRegions(regions, scalePercent) : []; + const scaleAdjustProtectedChangeRegions = + isScaleAdjustOperation && scaleAdjustPatchReference + ? buildScaleAdjustProtectedChangeRegions(regions, scalePercent) + : []; const scaleAdjustProtectedRegions = isScaleAdjustOperation && scaleAdjustPatchReference - ? [...scaleAdjustCompositeRegions, scaleAdjustPatchReference.workspaceRect] + ? scaleAdjustProtectedChangeRegions : scaleAdjustCompositeRegions; const scaleAdjustVerificationConfig = isScaleAdjustOperation ? { diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 3443b2e8d..0dd666870 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -2574,6 +2574,75 @@ async function buildLocalEditDiffCompositeMask(params: { .toBuffer(); } +async function normalizeAlphaMaskToRaw(params: { + maskBuffer: Buffer; + width: number; + height: number; +}) { + const { height, maskBuffer, width } = params; + return sharp(maskBuffer, { failOn: "none" }) + .resize(width, height, { fit: "fill" }) + .removeAlpha() + .extractChannel(0) + .raw() + .toBuffer(); +} + +async function normalizeOpacityMaskToRaw(params: { + maskBuffer: Buffer; + width: number; + height: number; +}) { + const { height, maskBuffer, width } = params; + return sharp(maskBuffer, { failOn: "none" }) + .resize(width, height, { fit: "fill" }) + .ensureAlpha() + .extractChannel(3) + .raw() + .toBuffer(); +} + +function multiplyAlphaMasks(primaryMask: Buffer, clipMask: Buffer) { + if (primaryMask.length !== clipMask.length) return primaryMask; + const multiplied = Buffer.allocUnsafe(primaryMask.length); + for (let index = 0; index < primaryMask.length; index += 1) { + multiplied[index] = Math.round((primaryMask[index] * clipMask[index]) / 255); + } + return multiplied; +} + +function boostAlphaMask(mask: Buffer, cutoff: number, multiplier: number) { + const boosted = Buffer.allocUnsafe(mask.length); + for (let index = 0; index < mask.length; index += 1) { + const value = mask[index]; + boosted[index] = + value < cutoff ? 0 : Math.min(255, Math.round(value * multiplier)); + } + return boosted; +} + +async function applyRawAlphaToImage(params: { + imageBuffer: Buffer; + alphaMaskRaw: Buffer; + width: number; + height: number; +}) { + const { alphaMaskRaw, height, imageBuffer, width } = params; + const rgbRaw = await sharp(imageBuffer, { failOn: "none" }) + .resize(width, height, { fit: "fill" }) + .removeAlpha() + .toColorspace("srgb") + .raw() + .toBuffer(); + + return sharp(rgbRaw, { raw: { channels: 3, height, width } }) + .joinChannel(alphaMaskRaw, { + raw: { channels: 1, height, width }, + }) + .png({ compressionLevel: 6 }) + .toBuffer(); +} + async function applyLocalEditCompositeToImages(params: { images: ParsedImage[]; composite: CanvasLocalEditCompositeConfig | null; @@ -2613,19 +2682,22 @@ async function applyLocalEditCompositeToImages(params: { .toBuffer(); const overlayBuffer = composite.maskMode === "diff" - ? await sharp(resizedGeneratedBuffer, { failOn: "none" }) - .removeAlpha() - .joinChannel( - await buildLocalEditDiffCompositeMask({ + ? await applyRawAlphaToImage({ + alphaMaskRaw: await normalizeAlphaMaskToRaw({ + height: sourceHeight, + maskBuffer: await buildLocalEditDiffCompositeMask({ composite, generatedBuffer: resizedGeneratedBuffer, height: sourceHeight, sourceBuffer, width: sourceWidth, - }) - ) - .png({ compressionLevel: 6 }) - .toBuffer() + }), + width: sourceWidth, + }), + height: sourceHeight, + imageBuffer: resizedGeneratedBuffer, + width: sourceWidth, + }) : await sharp(resizedGeneratedBuffer, { failOn: "none" }) .ensureAlpha() .composite([ @@ -2778,6 +2850,25 @@ async function applyScaleAdjustPatchCompositeToImages(params: { touchesTop, width: workspaceBox.width, }); + const regionClipPatchMask = maskRegionBoxes?.length + ? buildScaleAdjustPatchCompositeMaskSvg({ + featherPx, + height: workspaceBox.height, + regions: maskRegionBoxes, + touchesBottom, + touchesLeft, + touchesRight, + touchesTop, + width: workspaceBox.width, + }) + : null; + const regionClipPatchMaskRaw = regionClipPatchMask + ? await normalizeOpacityMaskToRaw({ + height: workspaceBox.height, + maskBuffer: regionClipPatchMask, + width: workspaceBox.width, + }) + : null; const sourcePatchBuffer = await sharp(sourceBuffer, { failOn: "none" }) .extract(workspaceBox) .png({ compressionLevel: 6 }) @@ -2829,11 +2920,24 @@ async function applyScaleAdjustPatchCompositeToImages(params: { throw new Error("比例调整局部结果合成失败:无法生成合成蒙版"); } const patchOverlayBuffer = diffPatchMask - ? await sharp(resizedGeneratedBuffer, { failOn: "none" }) - .removeAlpha() - .joinChannel(diffPatchMask) - .png({ compressionLevel: 6 }) - .toBuffer() + ? await applyRawAlphaToImage({ + alphaMaskRaw: boostAlphaMask( + multiplyAlphaMasks( + await normalizeAlphaMaskToRaw({ + height: workspaceBox.height, + maskBuffer: diffPatchMask, + width: workspaceBox.width, + }), + regionClipPatchMaskRaw || + Buffer.alloc(workspaceBox.width * workspaceBox.height, 255) + ), + 8, + 3 + ), + height: workspaceBox.height, + imageBuffer: resizedGeneratedBuffer, + width: workspaceBox.width, + }) : await sharp(resizedGeneratedBuffer, { failOn: "none" }) .ensureAlpha() .composite([{ input: patchMask, blend: "dest-in" }]) @@ -2877,6 +2981,7 @@ async function applyScaleAdjustPatchCompositeToImages(params: { debugLabel, featherPx, imageCount: mergedImages.length, + diffRegionClipApplied: Boolean(regionClipPatchMask), maskMode: composite.maskMode || "workspace", maskRegionCount: maskRegionBoxes?.length || 0, sourceHeight,