Files
Pien-Studio/apps/web/lib/effects/face-blur.test.ts
T
2026-05-31 03:04:30 +07:00

347 lines
8.2 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { faceBlurRenderer } from "./face-blur";
import type { FaceBlurEffect } from "@pien-studio/types";
function makeContext() {
return {
fillStyle: "",
filter: "none",
imageSmoothingEnabled: true,
fillRect: vi.fn(),
drawImage: vi.fn(),
save: vi.fn(),
restore: vi.fn(),
} as unknown as CanvasRenderingContext2D;
}
function makeImage(width = 1200, height = 800) {
return { naturalWidth: width, naturalHeight: height } as HTMLImageElement;
}
function makeContext2d(
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
tw = 600,
th = 400,
) {
return { ctx, image, targetWidth: tw, targetHeight: th };
}
describe("faceBlurRenderer.render (regions)", () => {
it("renders gaussian blur region with source dimensions", () => {
const ctx = makeContext();
const image = makeImage();
const effect: FaceBlurEffect = {
kind: "face-blur",
enabled: true,
method: "gaussian",
amount: 24,
regions: [
{
x: 120,
y: 80,
width: 300,
height: 200,
sourceWidth: 1200,
sourceHeight: 800,
},
],
};
faceBlurRenderer.render(makeContext2d(ctx, image), effect);
expect(ctx.save).toHaveBeenCalledOnce();
expect(ctx.filter).toBe("blur(24px)");
expect(ctx.drawImage).toHaveBeenCalledWith(
image,
120,
80,
300,
200,
60,
40,
150,
100,
);
expect(ctx.restore).toHaveBeenCalledOnce();
});
it("renders pixelate using sampled offscreen canvas", () => {
const doc = globalThis.document;
expect(doc).toBeDefined();
if (!doc) return;
const ctx = makeContext();
const image = makeImage();
const sampleDrawImage = vi.fn();
const sampleCtx = {
imageSmoothingEnabled: true,
drawImage: sampleDrawImage,
} as unknown as CanvasRenderingContext2D;
const sampleCanvas = {
width: 0,
height: 0,
getContext: vi.fn(() => sampleCtx),
} as unknown as HTMLCanvasElement;
const nativeCreateElement = doc.createElement.bind(doc);
const createElement = vi
.spyOn(doc, "createElement")
.mockImplementation((tagName: string) => {
if (tagName === "canvas") return sampleCanvas;
return nativeCreateElement(tagName);
});
const effect: FaceBlurEffect = {
kind: "face-blur",
enabled: true,
method: "pixelate",
amount: 10,
regions: [
{
x: 200,
y: 100,
width: 160,
height: 120,
sourceWidth: 1200,
sourceHeight: 800,
},
],
};
faceBlurRenderer.render(makeContext2d(ctx, image), effect);
expect(sampleCanvas.width).toBe(16);
expect(sampleCanvas.height).toBe(12);
expect(sampleDrawImage).toHaveBeenCalledWith(
image,
200,
100,
160,
120,
0,
0,
16,
12,
);
expect(ctx.drawImage).toHaveBeenCalledWith(
sampleCanvas,
0,
0,
16,
12,
100,
50,
80,
60,
);
createElement.mockRestore();
});
it("renders censor with region color priority", () => {
const ctx = makeContext();
const image = makeImage();
const effect: FaceBlurEffect = {
kind: "face-blur",
enabled: true,
method: "censor",
amount: 20,
censorColor: "#ff0000",
regions: [
{
x: 20,
y: 30,
width: 40,
height: 50,
sourceWidth: 1200,
sourceHeight: 800,
censorColor: "#00ff00",
},
],
};
faceBlurRenderer.render(makeContext2d(ctx, image), effect);
expect(ctx.fillStyle).toBe("#00ff00");
expect(ctx.fillRect).toHaveBeenCalledWith(10, 15, 20, 25);
});
it("falls back to legacy region scaling when source dimensions are missing", () => {
const ctx = makeContext();
const image = makeImage(2400, 1600);
const effect: FaceBlurEffect = {
kind: "face-blur",
enabled: true,
method: "gaussian",
amount: 16,
regions: [{ x: 100, y: 120, width: 300, height: 200 }],
};
faceBlurRenderer.render(makeContext2d(ctx, image), effect);
expect(ctx.drawImage).toHaveBeenCalledWith(
image,
400,
480,
1200,
800,
100,
120,
300,
200,
);
});
});
describe("faceBlurRenderer.renderLayer", () => {
it("applies blur to source-sized image before drawing the resized layer", () => {
const doc = globalThis.document;
expect(doc).toBeDefined();
if (!doc) return;
const ctx = makeContext();
const image = makeImage();
const sourceDrawImage = vi.fn();
const sourceCtx = {
...makeContext(),
drawImage: sourceDrawImage,
} as unknown as CanvasRenderingContext2D;
const sourceCanvas = {
width: 0,
height: 0,
getContext: vi.fn(() => sourceCtx),
} as unknown as HTMLCanvasElement;
const nativeCreateElement = doc.createElement.bind(doc);
const createElement = vi
.spyOn(doc, "createElement")
.mockImplementation((tagName: string) => {
if (tagName === "canvas") return sourceCanvas;
return nativeCreateElement(tagName);
});
const effect: FaceBlurEffect = {
kind: "face-blur",
enabled: true,
method: "gaussian",
amount: 24,
regions: [
{
x: 120,
y: 80,
width: 300,
height: 200,
sourceWidth: 1200,
sourceHeight: 800,
},
],
};
faceBlurRenderer.renderLayer(makeContext2d(ctx, image), effect);
expect(sourceCanvas.width).toBe(1200);
expect(sourceCanvas.height).toBe(800);
expect(sourceDrawImage).toHaveBeenNthCalledWith(1, image, 0, 0, 1200, 800);
expect(sourceDrawImage).toHaveBeenNthCalledWith(
2,
image,
120,
80,
300,
200,
120,
80,
300,
200,
);
expect(ctx.drawImage).toHaveBeenCalledWith(
sourceCanvas,
0,
0,
1200,
800,
0,
0,
600,
400,
);
createElement.mockRestore();
});
it("uses a canvas-filter fallback for gaussian blur when filters are unavailable", () => {
const doc = globalThis.document;
expect(doc).toBeDefined();
if (!doc) return;
const ctx = makeContext();
delete (ctx as Partial<CanvasRenderingContext2D>).filter;
const image = makeImage();
const regionDrawImage = vi.fn();
const blurDrawImage = vi.fn();
const regionCtx = {
...makeContext(),
clearRect: vi.fn(),
drawImage: regionDrawImage,
} as unknown as CanvasRenderingContext2D;
const blurCtx = {
...makeContext(),
clearRect: vi.fn(),
drawImage: blurDrawImage,
} as unknown as CanvasRenderingContext2D;
const regionCanvas = {
width: 0,
height: 0,
getContext: vi.fn(() => regionCtx),
} as unknown as HTMLCanvasElement;
const blurCanvas = {
width: 0,
height: 0,
getContext: vi.fn(() => blurCtx),
} as unknown as HTMLCanvasElement;
const nativeCreateElement = doc.createElement.bind(doc);
const createElement = vi
.spyOn(doc, "createElement")
.mockImplementation((tagName: string) => {
if (tagName !== "canvas") return nativeCreateElement(tagName);
return createElement.mock.calls.length === 1
? regionCanvas
: blurCanvas;
});
const effect: FaceBlurEffect = {
kind: "face-blur",
enabled: true,
method: "gaussian",
amount: 24,
regions: [
{
x: 120,
y: 80,
width: 300,
height: 200,
sourceWidth: 1200,
sourceHeight: 800,
},
],
};
faceBlurRenderer.render(makeContext2d(ctx, image), effect);
expect(ctx.save).not.toHaveBeenCalled();
expect(regionCanvas.width).toBe(150);
expect(regionCanvas.height).toBe(100);
expect(regionDrawImage).toHaveBeenCalledWith(
image,
120,
80,
300,
200,
0,
0,
150,
100,
);
expect(ctx.drawImage).toHaveBeenCalledWith(
regionCanvas,
0,
0,
150,
100,
60,
40,
150,
100,
);
createElement.mockRestore();
});
});