fix: protect people in product locked scale adjust

This commit is contained in:
134 2026-07-06 02:29:49 +08:00
parent f6faf6bb44
commit f7a5accb61
2 changed files with 362 additions and 115 deletions

View File

@ -128,6 +128,7 @@ type ScaleAdjustPatchReference = {
compositeClipMaskUrl?: string | null;
productLockMethod?: string | null;
productLockOverlayUrl?: string | null;
productLockProtectMaskUrl?: string | null;
sourceCropUrl: string;
structureLockedDraft?: boolean;
targetDraftFullUrl?: string | null;
@ -145,6 +146,7 @@ type ScaleAdjustStructureLockedComposite = {
method?: "pixel-mask" | "rect-crop-guide" | "rect-crop-guide-rmbg-lock" | "rmbg-cutout";
overlay: { input: Buffer; left: number; top: number };
productLock?: { input: Buffer; left: number; top: number };
productLockProtectMask?: { input: Buffer; left: number; top: number };
targetClip?: { input: Buffer; left: number; top: number };
wash: { input: Buffer; left: number; top: number };
};
@ -1586,14 +1588,17 @@ function buildScaleAdjustStructureInitialMask(params: {
}
function buildScaleAdjustProtectedPersonSeedMask(params: {
background?: { b: number; g: number; luma: number; r: number } | null;
box: { left: number; top: number; width: number; height: number };
raw: Buffer;
side: ScaleAdjustProtectedPersonSide;
width: number;
}) {
const { box, raw, side, width } = params;
const { background = null, box, raw, side, width } = params;
const candidate = new Uint8Array(box.width * box.height);
const anchor = new Uint8Array(box.width * box.height);
const visited = new Uint8Array(box.width * box.height);
const seed = new Uint8Array(box.width * box.height);
let seedCount = 0;
const sideAllowsX = (localX: number) => {
if (side === "right") return localX >= box.width * 0.52;
if (side === "left") return localX <= box.width * 0.48;
@ -1611,17 +1616,106 @@ function buildScaleAdjustProtectedPersonSeedMask(params: {
const b = raw[rawIndex + 2] || 0;
const { luma, saturation } = getScaleAdjustPixelStats(r, g, b);
const likelySkin = isScaleAdjustLikelySkinPixel(r, g, b);
const colorDistance = background
? Math.sqrt(
(r - background.r) ** 2 +
(g - background.g) ** 2 +
(b - background.b) ** 2
)
: 999;
const backgroundLikeLightPixel =
Boolean(background) &&
luma >= (background?.luma || 0) - 3 &&
colorDistance < 18;
const likelyWhiteFabric =
isScaleAdjustLikelyWhiteFabricPixel(r, g, b, luma, saturation) &&
luma < 246 &&
saturation < 0.22;
saturation < 0.22 &&
!backgroundLikeLightPixel &&
(colorDistance > 16 || !background || luma < background.luma - 5);
if (!likelySkin && !likelyWhiteFabric) continue;
seed[localY * box.width + localX] = 255;
seedCount += 1;
const index = localY * box.width + localX;
candidate[index] = 255;
if (likelySkin) {
anchor[index] = 255;
}
}
}
const queueX: number[] = [];
const queueY: number[] = [];
let seedCount = 0;
let anchorCount = 0;
for (let y = 0; y < box.height; y += 1) {
for (let x = 0; x < box.width; x += 1) {
const startIndex = y * box.width + x;
if (candidate[startIndex] === 0 || visited[startIndex]) continue;
queueX.length = 0;
queueY.length = 0;
queueX.push(x);
queueY.push(y);
visited[startIndex] = 1;
let head = 0;
let area = 0;
let anchors = 0;
let minX = x;
let minY = y;
let maxX = x;
let maxY = y;
while (head < queueX.length) {
const cx = queueX[head] || 0;
const cy = queueY[head] || 0;
head += 1;
area += 1;
if (anchor[cy * box.width + cx]) anchors += 1;
minX = Math.min(minX, cx);
minY = Math.min(minY, cy);
maxX = Math.max(maxX, cx);
maxY = Math.max(maxY, cy);
for (let dy = -1; dy <= 1; dy += 1) {
for (let dx = -1; dx <= 1; dx += 1) {
if (dx === 0 && dy === 0) continue;
const nx = cx + dx;
const ny = cy + dy;
if (nx < 0 || ny < 0 || nx >= box.width || ny >= box.height) continue;
const nextIndex = ny * box.width + nx;
if (candidate[nextIndex] === 0 || visited[nextIndex]) continue;
visited[nextIndex] = 1;
queueX.push(nx);
queueY.push(ny);
}
}
}
const componentWidth = maxX - minX + 1;
const componentHeight = maxY - minY + 1;
const broadBackgroundStripe =
anchors === 0 &&
componentHeight >= box.height * 0.48 &&
componentWidth >= box.width * 0.16;
const tinyUnanchoredPatch = anchors === 0 && area < box.width * box.height * 0.004;
if (anchors === 0 || broadBackgroundStripe || tinyUnanchoredPatch) {
continue;
}
for (let index = 0; index < queueX.length; index += 1) {
const pixelX = queueX[index] || 0;
const pixelY = queueY[index] || 0;
const seedIndex = pixelY * box.width + pixelX;
if (seed[seedIndex]) continue;
seed[seedIndex] = 255;
seedCount += 1;
}
anchorCount += anchors;
}
}
const seedRatio = seedCount / Math.max(1, box.width * box.height);
const anchorRatio = anchorCount / Math.max(1, box.width * box.height);
if (anchorCount < 12 || anchorRatio < 0.00012) return null;
if (seedCount < 24 || seedRatio < 0.0006) return null;
return dilateScaleAdjustStructureMask({
@ -2148,6 +2242,19 @@ function thresholdScaleAdjustAlphaMask(params: {
return output;
}
function removeScaleAdjustMaskFromAlpha(params: {
alphaMask: Buffer | Uint8Array;
removeMask: Uint8Array | null;
}) {
const { alphaMask, removeMask } = params;
if (!removeMask || removeMask.length !== alphaMask.length) return alphaMask;
const output = Buffer.from(alphaMask);
for (let index = 0; index < output.length; index += 1) {
if (removeMask[index]) output[index] = 0;
}
return output;
}
async function applyScaleAdjustAlphaMaskToRgba(params: {
alphaMask: Uint8Array;
height: number;
@ -2343,7 +2450,9 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
userId,
imageBase64: sourceCropBuffer.toString("base64"),
imageBuffer: sourceCropBuffer,
prompt: "比例调整主体抠图:只保留框选的目标人物/家具/商品主体,输出透明背景 PNG",
prompt: protectedPersonSide
? "比例调整家具/商品抠图:只保留框选区域内需要缩放的目标家具/商品主体和它本身的配件结构,明确排除非目标人物、手、脚、腿、衣服、书本、身体遮挡物和背景,输出透明背景 PNG"
: "比例调整主体抠图:只保留框选的目标人物/家具/商品主体,输出透明背景 PNG",
skipGallerySave: true,
sourceImageUrl,
});
@ -2378,7 +2487,25 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
rawRgba: cutoutRaw,
width: cutoutWidth,
});
const protectedCleanedAlpha =
const background = estimateScaleAdjustBoxBackground({
box: placement.sourceBox,
raw,
width: sourceWidth,
});
const protectedPersonMask =
protectedPersonSide && sameSizeCutout && sourceCropRaw
? validateScaleAdjustProtectedPersonMask({
box: { width: cutoutWidth, height: cutoutHeight },
mask: buildScaleAdjustProtectedPersonSeedMask({
background,
box: { left: 0, top: 0, width: cutoutWidth, height: cutoutHeight },
raw: sourceCropRaw,
side: protectedPersonSide,
width: cutoutWidth,
}),
})
: null;
const sideProtectedCleanedAlpha =
protectedPersonSide && sameSizeCutout && sourceCropRaw
? suppressScaleAdjustProtectedFurnitureDarkAlpha({
alphaMask: Buffer.from(cleanedAlpha),
@ -2389,10 +2516,12 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
sourceBox: placement.sourceBox,
})
: cleanedAlpha;
if (protectedCleanedAlpha !== cleanedAlpha) {
for (let index = 0; index < protectedCleanedAlpha.length; index += 1) {
cleanedRaw[index * 4 + 3] = protectedCleanedAlpha[index] || 0;
}
const protectedCleanedAlpha = removeScaleAdjustMaskFromAlpha({
alphaMask: sideProtectedCleanedAlpha,
removeMask: protectedPersonMask,
});
for (let index = 0; index < protectedCleanedAlpha.length; index += 1) {
cleanedRaw[index * 4 + 3] = protectedCleanedAlpha[index] || 0;
}
const highConfidenceCutoutMask = thresholdScaleAdjustAlphaMask({
maskBuffer: protectedCleanedAlpha,
@ -2427,27 +2556,10 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
);
return null;
}
const background = estimateScaleAdjustBoxBackground({
box: placement.sourceBox,
raw,
width: sourceWidth,
});
const highConfidenceSourceMask = thresholdScaleAdjustAlphaMask({
maskBuffer: sourceMaskBuffer,
threshold: 96,
});
const protectedPersonMask =
protectedPersonSide && sameSizeCutout && sourceCropRaw
? validateScaleAdjustProtectedPersonMask({
box: { width: cutoutWidth, height: cutoutHeight },
mask: buildScaleAdjustProtectedPersonSeedMask({
box: { left: 0, top: 0, width: cutoutWidth, height: cutoutHeight },
raw: sourceCropRaw,
side: protectedPersonSide,
width: cutoutWidth,
}),
})
: null;
const filteredSourceMask = filterScaleAdjustStructureComponents({
box: placement.sourceBox,
mask: highConfidenceSourceMask,
@ -2517,7 +2629,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
});
const sourceCompositeClipMask = dilateScaleAdjustStructureMask({
height: placement.sourceBox.height,
mask: filteredSourceBbox ? filteredSourceMask : highConfidenceSourceMask,
mask: sourceWashBaseMask,
radius: Math.max(
1,
Math.round(
@ -2602,6 +2714,23 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
.toBuffer();
const targetClipMaskPatch =
await buildScaleAdjustOverlayClipMaskPatch(structureOverlay);
const productLockProtectMaskPatch = protectedPersonMask
? await buildScaleAdjustCompositeClipMaskPatch({
box: placement.sourceBox,
maskBuffer: dilateScaleAdjustStructureMask({
height: placement.sourceBox.height,
mask: protectedPersonMask,
radius: Math.max(
2,
Math.round(
Math.min(placement.sourceBox.width, placement.sourceBox.height) *
0.012
)
),
width: placement.sourceBox.width,
}),
})
: null;
console.info("[Canvas Local Edit] Scale adjust RMBG structure draft applied:", {
alphaRatio: Number(alphaRatio.toFixed(3)),
@ -2637,6 +2766,15 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
left: Math.round(targetCenterX - placedWidth / 2),
top: Math.round(targetCenterY - placedHeight / 2),
},
...(productLockProtectMaskPatch
? {
productLockProtectMask: {
input: productLockProtectMaskPatch,
left: placement.sourceBox.left,
top: placement.sourceBox.top,
},
}
: {}),
targetClip: {
input: targetClipMaskPatch,
left: Math.round(targetCenterX - placedWidth / 2),
@ -3220,11 +3358,16 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
left: number;
top: number;
}> = [];
const productLockComposites: Array<{
input: Buffer;
left: number;
top: number;
}> = [];
const productLockComposites: Array<{
input: Buffer;
left: number;
top: number;
}> = [];
const productLockProtectMaskComposites: Array<{
input: Buffer;
left: number;
top: number;
}> = [];
for (const region of regions) {
const placement = getScaleAdjustVisualGuidePlacement({
@ -3284,15 +3427,20 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
if (structureLockedComposite.compositeClip) {
compositeClipComposites.push(structureLockedComposite.compositeClip);
}
if (structureLockedComposite.targetClip) {
targetClipComposites.push(structureLockedComposite.targetClip);
if (structureLockedComposite.targetClip && !structureLockedComposite.productLock) {
targetClipComposites.push(structureLockedComposite.targetClip);
}
if (structureLockedComposite.productLock) {
productLockComposites.push(structureLockedComposite.productLock);
}
if (structureLockedComposite.productLockProtectMask) {
productLockProtectMaskComposites.push(
structureLockedComposite.productLockProtectMask
);
}
composites.push(structureLockedComposite.overlay);
continue;
}
if (structureLockedComposite.productLock) {
productLockComposites.push(structureLockedComposite.productLock);
}
composites.push(structureLockedComposite.overlay);
continue;
}
const shouldUseShrinkCutoutDraft =
process.env.CANVAS_SCALE_ADJUST_ENABLE_RMBG_SHRINK_CUTOUT === "1" &&
@ -3317,15 +3465,20 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
if (shrinkCutoutComposite.compositeClip) {
compositeClipComposites.push(shrinkCutoutComposite.compositeClip);
}
if (shrinkCutoutComposite.targetClip) {
targetClipComposites.push(shrinkCutoutComposite.targetClip);
if (shrinkCutoutComposite.targetClip && !shrinkCutoutComposite.productLock) {
targetClipComposites.push(shrinkCutoutComposite.targetClip);
}
if (shrinkCutoutComposite.productLock) {
productLockComposites.push(shrinkCutoutComposite.productLock);
}
if (shrinkCutoutComposite.productLockProtectMask) {
productLockProtectMaskComposites.push(
shrinkCutoutComposite.productLockProtectMask
);
}
composites.push(shrinkCutoutComposite.overlay);
continue;
}
if (shrinkCutoutComposite.productLock) {
productLockComposites.push(shrinkCutoutComposite.productLock);
}
composites.push(shrinkCutoutComposite.overlay);
continue;
}
sourceFootprintBoxes.push({
...placement.sourceFootprintBox,
@ -3412,31 +3565,50 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
.png({ compressionLevel: 6 })
.toBuffer()
: null;
const productLockOverlayBuffer = productLockComposites.length
? await sharp({
create: {
background: { alpha: 0, b: 0, g: 0, r: 0 },
const productLockOverlayBuffer = productLockComposites.length
? await sharp({
create: {
background: { alpha: 0, b: 0, g: 0, r: 0 },
channels: 4,
height: sourceHeight,
width: sourceWidth,
},
})
.composite(productLockComposites)
.png({ compressionLevel: 6 })
.toBuffer()
: null;
const compositeClipMaskBuffer =
compositeClipComposites.length || targetClipComposites.length
? await buildScaleAdjustCompositeClipMaskBuffer({
height: sourceHeight,
.png({ compressionLevel: 6 })
.toBuffer()
: null;
const productLockProtectMaskBuffer = productLockProtectMaskComposites.length
? await sharp({
create: {
background: { alpha: 0, b: 255, g: 255, r: 255 },
channels: 4,
height: sourceHeight,
width: sourceWidth,
},
})
.composite(productLockProtectMaskComposites)
.png({ compressionLevel: 6 })
.toBuffer()
: null;
const compositeClipMaskBuffer =
compositeClipComposites.length || targetClipComposites.length
? await buildScaleAdjustCompositeClipMaskBuffer({
height: sourceHeight,
sourceMaskComposites: compositeClipComposites,
targetMaskComposites: targetClipComposites,
width: sourceWidth,
})
: null;
return { backgroundPlateBuffer, buffer, compositeClipMaskBuffer, productLockOverlayBuffer };
}
return {
backgroundPlateBuffer,
buffer,
compositeClipMaskBuffer,
productLockOverlayBuffer,
productLockProtectMaskBuffer,
};
}
async function createScaleAdjustPatchReference(params: {
fullFrameReferenceOnly?: boolean;
@ -3478,12 +3650,13 @@ async function createScaleAdjustPatchReference(params: {
const targetDraftBuffer = targetDraftReference.buffer;
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
if (params.fullFrameReferenceOnly && structureLockAppliedRef.value) {
const [
targetDraftFullUpload,
backgroundPlateFullUpload,
productLockOverlayUpload,
compositeClipMaskUpload,
] = await Promise.all([
const [
targetDraftFullUpload,
backgroundPlateFullUpload,
productLockOverlayUpload,
productLockProtectMaskUpload,
compositeClipMaskUpload,
] = await Promise.all([
uploadFile(
`canvas-scale-adjust-full-target-draft-${suffix}.png`,
toArrayBuffer(targetDraftBuffer)
@ -3494,15 +3667,21 @@ async function createScaleAdjustPatchReference(params: {
toArrayBuffer(targetDraftReference.backgroundPlateBuffer)
)
: Promise.resolve(null),
targetDraftReference.productLockOverlayBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
)
: Promise.resolve(null),
targetDraftReference.compositeClipMaskBuffer
? uploadFile(
`canvas-scale-adjust-composite-clip-mask-${suffix}.png`,
targetDraftReference.productLockOverlayBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
)
: Promise.resolve(null),
targetDraftReference.productLockProtectMaskBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-protect-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockProtectMaskBuffer)
)
: Promise.resolve(null),
targetDraftReference.compositeClipMaskBuffer
? uploadFile(
`canvas-scale-adjust-composite-clip-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.compositeClipMaskBuffer)
)
: Promise.resolve(null),
@ -3510,10 +3689,11 @@ async function createScaleAdjustPatchReference(params: {
return {
backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null,
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
productLockOverlayUrl: productLockOverlayUpload?.url || null,
sourceCropUrl: sourceImageUrl || targetDraftFullUpload.url,
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
productLockOverlayUrl: productLockOverlayUpload?.url || null,
productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null,
sourceCropUrl: sourceImageUrl || targetDraftFullUpload.url,
structureLockedDraft: structureLockAppliedRef.value,
targetDraftFullUrl: targetDraftFullUpload.url,
targetDraftUrl: targetDraftFullUpload.url,
@ -3532,14 +3712,15 @@ async function createScaleAdjustPatchReference(params: {
.png({ compressionLevel: 6 })
.toBuffer(),
]);
const [
sourceCropUpload,
targetDraftUpload,
targetDraftFullUpload,
backgroundPlateFullUpload,
productLockOverlayUpload,
compositeClipMaskUpload,
] = await Promise.all([
const [
sourceCropUpload,
targetDraftUpload,
targetDraftFullUpload,
backgroundPlateFullUpload,
productLockOverlayUpload,
productLockProtectMaskUpload,
compositeClipMaskUpload,
] = await Promise.all([
uploadFile(
`canvas-scale-adjust-workspace-source-${suffix}.png`,
toArrayBuffer(sourceCropBuffer)
@ -3558,15 +3739,21 @@ async function createScaleAdjustPatchReference(params: {
toArrayBuffer(targetDraftReference.backgroundPlateBuffer)
)
: Promise.resolve(null),
targetDraftReference.productLockOverlayBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
)
: Promise.resolve(null),
targetDraftReference.compositeClipMaskBuffer
? uploadFile(
`canvas-scale-adjust-composite-clip-mask-${suffix}.png`,
targetDraftReference.productLockOverlayBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
)
: Promise.resolve(null),
targetDraftReference.productLockProtectMaskBuffer
? uploadFile(
`canvas-scale-adjust-product-lock-protect-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.productLockProtectMaskBuffer)
)
: Promise.resolve(null),
targetDraftReference.compositeClipMaskBuffer
? uploadFile(
`canvas-scale-adjust-composite-clip-mask-${suffix}.png`,
toArrayBuffer(targetDraftReference.compositeClipMaskBuffer)
)
: Promise.resolve(null),
@ -3574,10 +3761,11 @@ async function createScaleAdjustPatchReference(params: {
return {
backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null,
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
productLockOverlayUrl: productLockOverlayUpload?.url || null,
sourceCropUrl: sourceCropUpload.url,
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
productLockOverlayUrl: productLockOverlayUpload?.url || null,
productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null,
sourceCropUrl: sourceCropUpload.url,
structureLockedDraft: structureLockAppliedRef.value,
targetDraftFullUrl: targetDraftFullUpload.url,
targetDraftUrl: targetDraftUpload.url,
@ -5201,10 +5389,12 @@ export async function POST(request: NextRequest) {
useScaleAdjustProductLockComposite,
scaleAdjustProductLockMethod:
scaleAdjustPatchReference?.productLockMethod || null,
scaleAdjustProductLockOverlayUrl:
scaleAdjustPatchReference?.productLockOverlayUrl || null,
scaleAdjustPatchCompositeClipMaskUrl:
scaleAdjustPatchReference?.compositeClipMaskUrl || null,
scaleAdjustProductLockOverlayUrl:
scaleAdjustPatchReference?.productLockOverlayUrl || null,
scaleAdjustProductLockProtectMaskUrl:
scaleAdjustPatchReference?.productLockProtectMaskUrl || null,
scaleAdjustPatchCompositeClipMaskUrl:
scaleAdjustPatchReference?.compositeClipMaskUrl || null,
scaleAdjustPatchWorkspacePixelRect:
scaleAdjustPatchReference?.workspacePixelRect || null,
scaleAdjustPatchWorkspaceRect:
@ -5365,13 +5555,15 @@ export async function POST(request: NextRequest) {
workspaceRegion: scaleAdjustPatchReference.workspaceRect,
}
: undefined,
scaleAdjustProductLockComposite: useScaleAdjustProductLockComposite
? {
overlayImageUrl:
scaleAdjustPatchReference?.productLockOverlayUrl as string,
sourceHeight,
sourceWidth,
}
scaleAdjustProductLockComposite: useScaleAdjustProductLockComposite
? {
overlayImageUrl:
scaleAdjustPatchReference?.productLockOverlayUrl as string,
protectMaskImageUrl:
scaleAdjustPatchReference?.productLockProtectMaskUrl || undefined,
sourceHeight,
sourceWidth,
}
: undefined,
scaleAdjustVerification: scaleAdjustVerificationConfig,
localEditComposite: isScaleAdjustOperation

View File

@ -73,6 +73,7 @@ export interface CanvasScaleAdjustPatchCompositeConfig {
export interface CanvasScaleAdjustProductLockCompositeConfig {
overlayImageUrl: string;
protectMaskImageUrl?: string;
sourceHeight: number;
sourceWidth: number;
}
@ -555,12 +556,16 @@ function normalizeScaleAdjustProductLockCompositeConfig(
if (!value || typeof value !== "object") return null;
const overlayImageUrl = String(value.overlayImageUrl || "").trim();
if (!/^https?:\/\//i.test(overlayImageUrl)) return null;
const protectMaskImageUrl = String(value.protectMaskImageUrl || "").trim();
const sourceWidth = Math.round(Number(value.sourceWidth || 0));
const sourceHeight = Math.round(Number(value.sourceHeight || 0));
if (sourceWidth <= 0 || sourceHeight <= 0) return null;
return {
overlayImageUrl,
...(protectMaskImageUrl && /^https?:\/\//i.test(protectMaskImageUrl)
? { protectMaskImageUrl }
: {}),
sourceHeight,
sourceWidth,
};
@ -2807,6 +2812,44 @@ async function applyRawAlphaToImage(params: {
.toBuffer();
}
async function applyScaleAdjustProductProtectMaskToOverlay(params: {
height: number;
overlayBuffer: Buffer;
protectMaskBuffer: Buffer;
width: number;
}) {
const { height, overlayBuffer, protectMaskBuffer, width } = params;
const overlayRaw = await sharp(overlayBuffer, { failOn: "none" })
.resize(width, height, { fit: "fill" })
.ensureAlpha()
.raw()
.toBuffer();
const protectRaw = await sharp(protectMaskBuffer, { failOn: "none" })
.rotate()
.resize(width, height, { fit: "fill" })
.ensureAlpha()
.extractChannel(3)
.raw()
.toBuffer();
const pixelCount = Math.min(
protectRaw.length,
Math.floor(overlayRaw.length / 4)
);
for (let index = 0; index < pixelCount; index += 1) {
const protect = protectRaw[index] || 0;
if (protect <= 0) continue;
const alphaIndex = index * 4 + 3;
const alpha = overlayRaw[alphaIndex] || 0;
overlayRaw[alphaIndex] = Math.round((alpha * Math.max(0, 255 - protect)) / 255);
}
return sharp(overlayRaw, {
raw: { channels: 4, height, width },
})
.png({ compressionLevel: 6 })
.toBuffer();
}
async function applyLocalEditCompositeToImages(params: {
images: ParsedImage[];
composite: CanvasLocalEditCompositeConfig | null;
@ -3204,6 +3247,9 @@ async function applyScaleAdjustProductLockCompositeToImages(params: {
.ensureAlpha()
.png({ compressionLevel: 6 })
.toBuffer();
const protectMaskBuffer = composite.protectMaskImageUrl
? await fetchImageBufferFromUrl(composite.protectMaskImageUrl)
: null;
const overlayMetadata = await sharp(overlayBuffer, { failOn: "none" }).metadata();
const overlayWidth = Math.round(Number(overlayMetadata.width || 0));
const overlayHeight = Math.round(Number(overlayMetadata.height || 0));
@ -3230,13 +3276,21 @@ async function applyScaleAdjustProductLockCompositeToImages(params: {
continue;
}
const resizedOverlayBuffer = await sharp(overlayBuffer, { failOn: "none" })
const resizedOverlayBaseBuffer = await sharp(overlayBuffer, { failOn: "none" })
.resize(generatedWidth, generatedHeight, {
fit: "fill",
kernel: sharp.kernel.lanczos3,
})
.png({ compressionLevel: 6 })
.toBuffer();
const resizedOverlayBuffer = protectMaskBuffer
? await applyScaleAdjustProductProtectMaskToOverlay({
height: generatedHeight,
overlayBuffer: resizedOverlayBaseBuffer,
protectMaskBuffer,
width: generatedWidth,
})
: resizedOverlayBaseBuffer;
const mergedBuffer = await sharp(normalizedGeneratedBuffer, {
failOn: "none",
})
@ -3271,6 +3325,7 @@ async function applyScaleAdjustProductLockCompositeToImages(params: {
imageCount: mergedImages.length,
overlayHeight,
overlayWidth,
protectMaskApplied: Boolean(protectMaskBuffer),
sourceHeight: composite.sourceHeight,
sourceWidth: composite.sourceWidth,
});