diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 04a54f42f..ae64a4f93 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -780,6 +780,16 @@ function buildScaleAdjustCompositeRegions( }); } +function buildScaleAdjustPatchCompositeRegions( + regions: NormalizedLocalEditRegion[], + scalePercent?: ScaleAdjustPercent | null +) { + return regions.flatMap((region) => [ + region.rect, + getScaleAdjustTargetRect(region, scalePercent), + ]); +} + function getScaleAdjustPatchWorkspaceRect( regions: NormalizedLocalEditRegion[], scalePercent?: ScaleAdjustPercent | null @@ -3860,6 +3870,9 @@ export async function POST(request: NextRequest) { const scaleAdjustCompositeRegions = isScaleAdjustOperation ? buildScaleAdjustCompositeRegions(regions, scalePercent) : []; + const scaleAdjustPatchCompositeRegions = isScaleAdjustOperation + ? buildScaleAdjustPatchCompositeRegions(regions, scalePercent) + : []; const scaleAdjustProtectedRegions = isScaleAdjustOperation && scaleAdjustPatchReference ? [...scaleAdjustCompositeRegions, scaleAdjustPatchReference.workspaceRect] @@ -4060,9 +4073,14 @@ export async function POST(request: NextRequest) { referenceImages, scaleAdjustPatchComposite: scaleAdjustPatchReference ? { + diffCompareBlurPx: 0.8, + diffHighThreshold: 42, + diffLowThreshold: 14, featherMaxPx: 42, featherMinPx: 10, featherRatio: 0.018, + maskMode: "diff", + regions: scaleAdjustPatchCompositeRegions, sourceImageUrl, workspaceRegion: scaleAdjustPatchReference.workspaceRect, } diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 28617af9b..1002ef17c 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -57,6 +57,10 @@ export interface CanvasScaleAdjustPatchCompositeConfig { sourceImageUrl: string; workspaceRegion: CanvasLocalEditCompositeRegion; regions?: CanvasLocalEditCompositeRegion[]; + maskMode?: "workspace" | "diff"; + diffLowThreshold?: number; + diffHighThreshold?: number; + diffCompareBlurPx?: number; featherMinPx?: number; featherMaxPx?: number; featherRatio?: number; @@ -397,6 +401,19 @@ function normalizeScaleAdjustPatchCompositeConfig( sourceImageUrl, workspaceRegion, ...(regions.length > 0 ? { regions } : {}), + maskMode: value.maskMode === "diff" ? "diff" : "workspace", + diffHighThreshold: normalizeOptionalNumber(value.diffHighThreshold, { + max: 120, + min: 1, + }), + diffLowThreshold: normalizeOptionalNumber(value.diffLowThreshold, { + max: 80, + min: 0, + }), + diffCompareBlurPx: normalizeOptionalNumber(value.diffCompareBlurPx, { + max: 12, + min: 0, + }), featherMaxPx: normalizeOptionalNumber(value.featherMaxPx, { max: 180, min: 0, @@ -2748,27 +2765,69 @@ async function applyScaleAdjustPatchCompositeToImages(params: { }; }) .filter((box) => box.width > 0 && box.height > 0); - const patchMask = buildScaleAdjustPatchCompositeMaskSvg({ - featherPx, - height: workspaceBox.height, - regions: maskRegionBoxes, - touchesBottom, - touchesLeft, - touchesRight, - touchesTop, - width: workspaceBox.width, - }); + const staticPatchMask = + composite.maskMode === "diff" + ? null + : buildScaleAdjustPatchCompositeMaskSvg({ + featherPx, + height: workspaceBox.height, + regions: maskRegionBoxes, + touchesBottom, + touchesLeft, + touchesRight, + touchesTop, + width: workspaceBox.width, + }); + const sourcePatchBuffer = await sharp(sourceBuffer, { failOn: "none" }) + .extract(workspaceBox) + .png({ compressionLevel: 6 }) + .toBuffer(); + const diffRegions = (maskRegionBoxes?.length + ? maskRegionBoxes + : [{ left: 0, top: 0, width: workspaceBox.width, height: workspaceBox.height }] + ).map((box) => ({ + x: box.left / Math.max(1, workspaceBox.width), + y: box.top / Math.max(1, workspaceBox.height), + width: box.width / Math.max(1, workspaceBox.width), + height: box.height / Math.max(1, workspaceBox.height), + })); const mergedImages: ParsedImage[] = []; for (const [index, image] of images.entries()) { const generatedUrl = image.originalUrl || image.thumbnailUrl; const generatedBuffer = await fetchImageBufferFromUrl(generatedUrl); - const patchOverlayBuffer = await sharp(generatedBuffer, { failOn: "none" }) + const resizedGeneratedBuffer = await sharp(generatedBuffer, { failOn: "none" }) .rotate() .resize(workspaceBox.width, workspaceBox.height, { fit: "cover", position: "center", }) + .png({ compressionLevel: 6 }) + .toBuffer(); + const patchMask = + composite.maskMode === "diff" + ? await buildLocalEditDiffCompositeMask({ + composite: { + diffCompareBlurPx: composite.diffCompareBlurPx, + diffHighThreshold: composite.diffHighThreshold, + diffLowThreshold: composite.diffLowThreshold, + featherMaxPx: composite.featherMaxPx, + featherMinPx: composite.featherMinPx, + featherRatio: composite.featherRatio, + maskMode: "diff", + regions: diffRegions, + sourceImageUrl: composite.sourceImageUrl, + }, + generatedBuffer: resizedGeneratedBuffer, + height: workspaceBox.height, + sourceBuffer: sourcePatchBuffer, + width: workspaceBox.width, + }) + : staticPatchMask; + if (!patchMask) { + throw new Error("比例调整局部结果合成失败:无法生成合成蒙版"); + } + const patchOverlayBuffer = await sharp(resizedGeneratedBuffer, { failOn: "none" }) .ensureAlpha() .composite([{ input: patchMask, blend: "dest-in" }]) .png({ compressionLevel: 6 }) @@ -2811,6 +2870,7 @@ async function applyScaleAdjustPatchCompositeToImages(params: { debugLabel, featherPx, imageCount: mergedImages.length, + maskMode: composite.maskMode || "workspace", maskRegionCount: maskRegionBoxes?.length || 0, sourceHeight, sourceWidth,