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).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(); }); });