2026-05-10 03:06:54 +07:00
|
|
|
import type { FaceBlurSettings } from "@pien-studio/types";
|
|
|
|
|
|
|
|
|
|
type BlurRegion = FaceBlurSettings["regions"][number];
|
|
|
|
|
|
|
|
|
|
function drawPixelatedRegion(
|
|
|
|
|
ctx: CanvasRenderingContext2D,
|
|
|
|
|
source: CanvasImageSource,
|
|
|
|
|
sourceX: number,
|
|
|
|
|
sourceY: number,
|
|
|
|
|
sourceWidth: number,
|
|
|
|
|
sourceHeight: number,
|
|
|
|
|
targetX: number,
|
|
|
|
|
targetY: number,
|
|
|
|
|
targetWidth: number,
|
|
|
|
|
targetHeight: number,
|
|
|
|
|
blockSize: number,
|
|
|
|
|
) {
|
|
|
|
|
const sampleCanvas = document.createElement("canvas");
|
|
|
|
|
sampleCanvas.width = Math.max(1, Math.round(targetWidth / blockSize));
|
|
|
|
|
sampleCanvas.height = Math.max(1, Math.round(targetHeight / blockSize));
|
|
|
|
|
const sampleCtx = sampleCanvas.getContext("2d");
|
|
|
|
|
if (!sampleCtx) return;
|
|
|
|
|
sampleCtx.imageSmoothingEnabled = false;
|
2026-05-16 11:51:44 +07:00
|
|
|
sampleCtx.drawImage(
|
|
|
|
|
source,
|
|
|
|
|
sourceX,
|
|
|
|
|
sourceY,
|
|
|
|
|
sourceWidth,
|
|
|
|
|
sourceHeight,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
sampleCanvas.width,
|
|
|
|
|
sampleCanvas.height,
|
|
|
|
|
);
|
2026-05-10 03:06:54 +07:00
|
|
|
ctx.imageSmoothingEnabled = false;
|
2026-05-16 11:51:44 +07:00
|
|
|
ctx.drawImage(
|
|
|
|
|
sampleCanvas,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
sampleCanvas.width,
|
|
|
|
|
sampleCanvas.height,
|
|
|
|
|
targetX,
|
|
|
|
|
targetY,
|
|
|
|
|
targetWidth,
|
|
|
|
|
targetHeight,
|
|
|
|
|
);
|
2026-05-10 03:06:54 +07:00
|
|
|
ctx.imageSmoothingEnabled = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 11:51:44 +07:00
|
|
|
function canUseCanvasFilter(ctx: CanvasRenderingContext2D) {
|
|
|
|
|
return "filter" in ctx && typeof ctx.filter === "string";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawBlurredRegionFallback(
|
|
|
|
|
ctx: CanvasRenderingContext2D,
|
|
|
|
|
source: CanvasImageSource,
|
|
|
|
|
sourceX: number,
|
|
|
|
|
sourceY: number,
|
|
|
|
|
sourceWidth: number,
|
|
|
|
|
sourceHeight: number,
|
|
|
|
|
targetX: number,
|
|
|
|
|
targetY: number,
|
|
|
|
|
targetWidth: number,
|
|
|
|
|
targetHeight: number,
|
|
|
|
|
amount: number,
|
|
|
|
|
) {
|
|
|
|
|
const regionCanvas = document.createElement("canvas");
|
|
|
|
|
regionCanvas.width = Math.max(1, Math.round(targetWidth));
|
|
|
|
|
regionCanvas.height = Math.max(1, Math.round(targetHeight));
|
|
|
|
|
const regionCtx = regionCanvas.getContext("2d");
|
|
|
|
|
if (!regionCtx) return;
|
|
|
|
|
|
|
|
|
|
regionCtx.drawImage(
|
|
|
|
|
source,
|
|
|
|
|
sourceX,
|
|
|
|
|
sourceY,
|
|
|
|
|
sourceWidth,
|
|
|
|
|
sourceHeight,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
regionCanvas.width,
|
|
|
|
|
regionCanvas.height,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const scale = Math.max(0.04, Math.min(0.5, 1 / Math.max(2, amount / 2)));
|
|
|
|
|
const blurCanvas = document.createElement("canvas");
|
|
|
|
|
blurCanvas.width = Math.max(1, Math.round(regionCanvas.width * scale));
|
|
|
|
|
blurCanvas.height = Math.max(1, Math.round(regionCanvas.height * scale));
|
|
|
|
|
const blurCtx = blurCanvas.getContext("2d");
|
|
|
|
|
if (!blurCtx) return;
|
|
|
|
|
|
|
|
|
|
blurCtx.imageSmoothingEnabled = true;
|
|
|
|
|
blurCtx.drawImage(regionCanvas, 0, 0, blurCanvas.width, blurCanvas.height);
|
|
|
|
|
regionCtx.imageSmoothingEnabled = true;
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
regionCtx.clearRect(0, 0, regionCanvas.width, regionCanvas.height);
|
|
|
|
|
regionCtx.drawImage(
|
|
|
|
|
blurCanvas,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
blurCanvas.width,
|
|
|
|
|
blurCanvas.height,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
regionCanvas.width,
|
|
|
|
|
regionCanvas.height,
|
|
|
|
|
);
|
|
|
|
|
blurCtx.clearRect(0, 0, blurCanvas.width, blurCanvas.height);
|
|
|
|
|
blurCtx.drawImage(
|
|
|
|
|
regionCanvas,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
regionCanvas.width,
|
|
|
|
|
regionCanvas.height,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
blurCanvas.width,
|
|
|
|
|
blurCanvas.height,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.drawImage(
|
|
|
|
|
regionCanvas,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
regionCanvas.width,
|
|
|
|
|
regionCanvas.height,
|
|
|
|
|
targetX,
|
|
|
|
|
targetY,
|
|
|
|
|
targetWidth,
|
|
|
|
|
targetHeight,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 03:06:54 +07:00
|
|
|
export function renderFaceBlurRegions(
|
|
|
|
|
ctx: CanvasRenderingContext2D,
|
|
|
|
|
image: HTMLImageElement,
|
2026-05-16 11:51:44 +07:00
|
|
|
blur: {
|
|
|
|
|
method: FaceBlurSettings["method"];
|
|
|
|
|
amount: number;
|
|
|
|
|
regions: BlurRegion[];
|
|
|
|
|
censorColor?: string;
|
|
|
|
|
},
|
2026-05-10 03:06:54 +07:00
|
|
|
targetWidth: number,
|
|
|
|
|
targetHeight: number,
|
|
|
|
|
): void {
|
|
|
|
|
if (!blur.regions.length) return;
|
|
|
|
|
const legacyScaleX = image.naturalWidth / Math.max(1, targetWidth);
|
|
|
|
|
const legacyScaleY = image.naturalHeight / Math.max(1, targetHeight);
|
|
|
|
|
|
|
|
|
|
for (const region of blur.regions) {
|
2026-05-10 12:11:58 +07:00
|
|
|
const sourceWidth = region.sourceWidth ?? 0;
|
|
|
|
|
const sourceHeight = region.sourceHeight ?? 0;
|
|
|
|
|
const hasSourceDims = sourceWidth > 0 && sourceHeight > 0;
|
|
|
|
|
const scaleX = hasSourceDims ? targetWidth / sourceWidth : 1;
|
|
|
|
|
const scaleY = hasSourceDims ? targetHeight / sourceHeight : 1;
|
2026-05-10 03:06:54 +07:00
|
|
|
|
|
|
|
|
const x = Math.max(0, Math.floor(region.x * scaleX));
|
|
|
|
|
const y = Math.max(0, Math.floor(region.y * scaleY));
|
|
|
|
|
const w = Math.max(1, Math.floor(region.width * scaleX));
|
|
|
|
|
const h = Math.max(1, Math.floor(region.height * scaleY));
|
2026-05-16 11:51:44 +07:00
|
|
|
const sx0 = hasSourceDims
|
|
|
|
|
? region.x
|
|
|
|
|
: Math.max(0, Math.floor(region.x * legacyScaleX));
|
|
|
|
|
const sy0 = hasSourceDims
|
|
|
|
|
? region.y
|
|
|
|
|
: Math.max(0, Math.floor(region.y * legacyScaleY));
|
|
|
|
|
const sw = hasSourceDims
|
|
|
|
|
? region.width
|
|
|
|
|
: Math.max(1, Math.floor(region.width * legacyScaleX));
|
|
|
|
|
const sh = hasSourceDims
|
|
|
|
|
? region.height
|
|
|
|
|
: Math.max(1, Math.floor(region.height * legacyScaleY));
|
2026-05-10 03:06:54 +07:00
|
|
|
|
|
|
|
|
if (blur.method === "censor") {
|
|
|
|
|
ctx.fillStyle = region.censorColor ?? blur.censorColor ?? "#111111";
|
|
|
|
|
ctx.fillRect(x, y, w, h);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (blur.method === "pixelate") {
|
|
|
|
|
const pixelSize = Math.max(4, Math.round(blur.amount / 2));
|
|
|
|
|
drawPixelatedRegion(ctx, image, sx0, sy0, sw, sh, x, y, w, h, pixelSize);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 11:51:44 +07:00
|
|
|
if (canUseCanvasFilter(ctx)) {
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.filter = `blur(${blur.amount}px)`;
|
|
|
|
|
ctx.drawImage(image, sx0, sy0, sw, sh, x, y, w, h);
|
|
|
|
|
ctx.restore();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawBlurredRegionFallback(
|
|
|
|
|
ctx,
|
|
|
|
|
image,
|
|
|
|
|
sx0,
|
|
|
|
|
sy0,
|
|
|
|
|
sw,
|
|
|
|
|
sh,
|
|
|
|
|
x,
|
|
|
|
|
y,
|
|
|
|
|
w,
|
|
|
|
|
h,
|
|
|
|
|
blur.amount,
|
|
|
|
|
);
|
2026-05-10 03:06:54 +07:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-16 11:51:44 +07:00
|
|
|
|
|
|
|
|
export function renderImageWithFaceBlur(
|
|
|
|
|
ctx: CanvasRenderingContext2D,
|
|
|
|
|
image: HTMLImageElement,
|
|
|
|
|
blur:
|
|
|
|
|
| {
|
|
|
|
|
method: FaceBlurSettings["method"];
|
|
|
|
|
amount: number;
|
|
|
|
|
regions: BlurRegion[];
|
|
|
|
|
censorColor?: string;
|
|
|
|
|
}
|
|
|
|
|
| undefined,
|
|
|
|
|
targetWidth: number,
|
|
|
|
|
targetHeight: number,
|
|
|
|
|
): void {
|
|
|
|
|
if (!blur || blur.regions.length === 0) {
|
|
|
|
|
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const canBlurAtSourceSize = blur.regions.every(
|
|
|
|
|
(region) => (region.sourceWidth ?? 0) > 0 && (region.sourceHeight ?? 0) > 0,
|
|
|
|
|
);
|
|
|
|
|
if (!canBlurAtSourceSize) {
|
|
|
|
|
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
|
|
|
renderFaceBlurRegions(ctx, image, blur, targetWidth, targetHeight);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sourceCanvas = document.createElement("canvas");
|
|
|
|
|
sourceCanvas.width = Math.max(1, image.naturalWidth);
|
|
|
|
|
sourceCanvas.height = Math.max(1, image.naturalHeight);
|
|
|
|
|
const sourceCtx = sourceCanvas.getContext("2d");
|
|
|
|
|
if (!sourceCtx) {
|
|
|
|
|
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sourceCtx.drawImage(image, 0, 0, sourceCanvas.width, sourceCanvas.height);
|
|
|
|
|
renderFaceBlurRegions(
|
|
|
|
|
sourceCtx,
|
|
|
|
|
image,
|
|
|
|
|
blur,
|
|
|
|
|
sourceCanvas.width,
|
|
|
|
|
sourceCanvas.height,
|
|
|
|
|
);
|
|
|
|
|
ctx.drawImage(
|
|
|
|
|
sourceCanvas,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
sourceCanvas.width,
|
|
|
|
|
sourceCanvas.height,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
targetWidth,
|
|
|
|
|
targetHeight,
|
|
|
|
|
);
|
|
|
|
|
}
|