mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Codemod rewrites all five ad-hoc fixture-path styles to import from the typed registry (tests/fixtures/index.ts): - join(__dirname, "..", "fixtures", ...) inline patterns (100 files) - const FIXTURES = join(__dirname, ...) + join(FIXTURES, ...) (92 files) - process.cwd()-based paths (10 files) - path.resolve(__dirname, ...) patterns (5 files) Registry extended with 8 new keys (portraitJpg, portraitHeic, motorcycle, svgLogo, barcodeAvif, qrAvif, crossFormatChat, multipageTiff) and directory accessors (fixtureDir, fixtureRoot). Dynamic directory scanners (format-matrix-generated, format-matrix-multimodal, hostile-inputs) preserved via fixtureDir imports. No fixtures swapped, bytes identical. Parity gate: dropped 0, net-new 388.
448 lines
16 KiB
TypeScript
448 lines
16 KiB
TypeScript
import sharp from "sharp";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
addDropShadow,
|
|
applyEffects,
|
|
blurBackground,
|
|
compositeOnColor,
|
|
compositeOnImage,
|
|
createGradientBackground,
|
|
} from "../../../apps/api/src/lib/bg-effects.js";
|
|
|
|
async function createTestImage(
|
|
width: number,
|
|
height: number,
|
|
channels: 4 | 3 = 4,
|
|
): Promise<Buffer> {
|
|
return sharp({
|
|
create: {
|
|
width,
|
|
height,
|
|
channels,
|
|
background: { r: 255, g: 0, b: 0, alpha: 1 },
|
|
},
|
|
})
|
|
.png()
|
|
.toBuffer();
|
|
}
|
|
|
|
async function createMultiColorImage(width: number, height: number): Promise<Buffer> {
|
|
const pixels = Buffer.alloc(width * height * 3);
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const i = (y * width + x) * 3;
|
|
pixels[i] = Math.floor((x / width) * 255);
|
|
pixels[i + 1] = Math.floor((y / height) * 255);
|
|
pixels[i + 2] = 128;
|
|
}
|
|
}
|
|
return sharp(pixels, { raw: { width, height, channels: 3 } })
|
|
.png()
|
|
.toBuffer();
|
|
}
|
|
|
|
async function createSubjectWithAlpha(width: number, height: number): Promise<Buffer> {
|
|
const pixels = Buffer.alloc(width * height * 4);
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const i = (y * width + x) * 4;
|
|
pixels[i] = 255;
|
|
pixels[i + 1] = 0;
|
|
pixels[i + 2] = 0;
|
|
pixels[i + 3] = x < width / 2 ? 255 : 0;
|
|
}
|
|
}
|
|
return sharp(pixels, { raw: { width, height, channels: 4 } })
|
|
.png()
|
|
.toBuffer();
|
|
}
|
|
|
|
describe("blurBackground", () => {
|
|
it("returns a valid PNG buffer", async () => {
|
|
const original = await createTestImage(100, 100, 3);
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const result = await blurBackground(original, subject, 50);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
expect(meta.width).toBe(100);
|
|
expect(meta.height).toBe(100);
|
|
});
|
|
|
|
it("intensity 0 produces minimal blur (sigma ~1)", async () => {
|
|
const original = await createTestImage(100, 100, 3);
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const result = await blurBackground(original, subject, 0);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
expect(result.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("intensity 100 produces heavy blur (sigma 50)", async () => {
|
|
const original = await createTestImage(100, 100, 3);
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const result = await blurBackground(original, subject, 100);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("different intensity values produce different output", async () => {
|
|
const original = await createMultiColorImage(100, 100);
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const low = await blurBackground(original, subject, 10);
|
|
const high = await blurBackground(original, subject, 90);
|
|
expect(low.equals(high)).toBe(false);
|
|
});
|
|
|
|
it("clamps intensity below 0 to 0", async () => {
|
|
const original = await createTestImage(100, 100, 3);
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const clamped = await blurBackground(original, subject, -10);
|
|
const atZero = await blurBackground(original, subject, 0);
|
|
expect(clamped.equals(atZero)).toBe(true);
|
|
});
|
|
|
|
it("clamps intensity above 100 to 100", async () => {
|
|
const original = await createTestImage(100, 100, 3);
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const clamped = await blurBackground(original, subject, 200);
|
|
const atMax = await blurBackground(original, subject, 100);
|
|
expect(clamped.equals(atMax)).toBe(true);
|
|
});
|
|
|
|
it("output dimensions match subject dimensions", async () => {
|
|
const original = await createTestImage(200, 150, 3);
|
|
const subject = await createSubjectWithAlpha(80, 60);
|
|
const result = await blurBackground(original, subject, 50);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.width).toBe(80);
|
|
expect(meta.height).toBe(60);
|
|
});
|
|
});
|
|
|
|
describe("addDropShadow", () => {
|
|
it("returns a valid PNG buffer", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const result = await addDropShadow(subject, 50);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("output dimensions match input dimensions", async () => {
|
|
const subject = await createSubjectWithAlpha(120, 80);
|
|
const result = await addDropShadow(subject, 50);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.width).toBe(120);
|
|
expect(meta.height).toBe(80);
|
|
});
|
|
|
|
it("opacity 0 produces no visible shadow (alpha bytes near zero in shadow area)", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const result = await addDropShadow(subject, 0);
|
|
const { data, info } = await sharp(result).raw().toBuffer({ resolveWithObject: true });
|
|
let shadowAlphaSum = 0;
|
|
for (let y = 90; y < info.height; y++) {
|
|
for (let x = 60; x < info.width; x++) {
|
|
const i = (y * info.width + x) * 4;
|
|
shadowAlphaSum += data[i + 3];
|
|
}
|
|
}
|
|
expect(shadowAlphaSum).toBe(0);
|
|
});
|
|
|
|
it("opacity 100 produces visible shadow", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const result = await addDropShadow(subject, 100);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
expect(result.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("higher opacity produces more shadow alpha than lower opacity", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const lowResult = await addDropShadow(subject, 20);
|
|
const highResult = await addDropShadow(subject, 80);
|
|
expect(lowResult.equals(highResult)).toBe(false);
|
|
});
|
|
|
|
it("throws when buffer has no readable dimensions", async () => {
|
|
const invalid = Buffer.from("not an image");
|
|
await expect(addDropShadow(invalid, 50)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("createGradientBackground", () => {
|
|
it("creates correct size output", async () => {
|
|
const result = await createGradientBackground(200, 100, "#ff0000", "#0000ff");
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.width).toBe(200);
|
|
expect(meta.height).toBe(100);
|
|
});
|
|
|
|
it("output is a valid PNG buffer", async () => {
|
|
const result = await createGradientBackground(100, 100, "#ff0000", "#0000ff");
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("default angle is 180 (top to bottom)", async () => {
|
|
const withDefault = await createGradientBackground(100, 100, "#ff0000", "#0000ff");
|
|
const with180 = await createGradientBackground(100, 100, "#ff0000", "#0000ff", 180);
|
|
expect(withDefault.equals(with180)).toBe(true);
|
|
});
|
|
|
|
it("different angles produce different pixel data", async () => {
|
|
const angle0 = await createGradientBackground(100, 100, "#ff0000", "#0000ff", 0);
|
|
const angle90 = await createGradientBackground(100, 100, "#ff0000", "#0000ff", 90);
|
|
expect(angle0.equals(angle90)).toBe(false);
|
|
});
|
|
|
|
it("different colors produce different output", async () => {
|
|
const grad1 = await createGradientBackground(100, 100, "#ff0000", "#0000ff");
|
|
const grad2 = await createGradientBackground(100, 100, "#00ff00", "#ffff00");
|
|
expect(grad1.equals(grad2)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("compositeOnColor", () => {
|
|
it("composites subject onto solid color background", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const result = await compositeOnColor(subject, "#0000ff");
|
|
const { data } = await sharp(result).raw().toBuffer({ resolveWithObject: true });
|
|
const lastPixel = (99 * 100 + 99) * 4;
|
|
expect(data[lastPixel + 2]).toBeGreaterThan(200);
|
|
});
|
|
|
|
it("output dimensions match subject dimensions", async () => {
|
|
const subject = await createSubjectWithAlpha(150, 80);
|
|
const result = await compositeOnColor(subject, "#00ff00");
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.width).toBe(150);
|
|
expect(meta.height).toBe(80);
|
|
});
|
|
|
|
it("output is a valid PNG", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const result = await compositeOnColor(subject, "#ff0000");
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("accepts hex color without hash prefix", async () => {
|
|
const subject = await createSubjectWithAlpha(50, 50);
|
|
const result = await compositeOnColor(subject, "ff0000");
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("throws on invalid buffer with no dimensions", async () => {
|
|
const invalid = Buffer.from("not an image");
|
|
await expect(compositeOnColor(invalid, "#ff0000")).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("compositeOnImage", () => {
|
|
it("composites subject onto background image", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const background = await createTestImage(200, 200, 3);
|
|
const result = await compositeOnImage(subject, background);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("background is resized to cover subject dimensions", async () => {
|
|
const subject = await createSubjectWithAlpha(80, 60);
|
|
const background = await createTestImage(200, 200, 3);
|
|
const result = await compositeOnImage(subject, background);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.width).toBe(80);
|
|
expect(meta.height).toBe(60);
|
|
});
|
|
|
|
it("output dimensions match subject dimensions", async () => {
|
|
const subject = await createSubjectWithAlpha(120, 90);
|
|
const background = await createTestImage(50, 50, 3);
|
|
const result = await compositeOnImage(subject, background);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.width).toBe(120);
|
|
expect(meta.height).toBe(90);
|
|
});
|
|
|
|
it("output is a valid PNG", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const background = await createTestImage(100, 100);
|
|
const result = await compositeOnImage(subject, background);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("throws on invalid subject buffer", async () => {
|
|
const invalid = Buffer.from("not an image");
|
|
const background = await createTestImage(100, 100);
|
|
await expect(compositeOnImage(invalid, background)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("applyEffects", () => {
|
|
it("transparent backgroundType with no effects returns subject approximately unchanged", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const result = await applyEffects(subject, original, {
|
|
backgroundType: "transparent",
|
|
});
|
|
expect(result.equals(subject)).toBe(true);
|
|
});
|
|
|
|
it("shadowEnabled with opacity adds shadow", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const withShadow = await applyEffects(subject, original, {
|
|
backgroundType: "transparent",
|
|
shadowEnabled: true,
|
|
shadowOpacity: 80,
|
|
});
|
|
expect(withShadow.equals(subject)).toBe(false);
|
|
const meta = await sharp(withShadow).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("blurEnabled with transparent bgType blurs the original background", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const result = await applyEffects(subject, original, {
|
|
backgroundType: "transparent",
|
|
blurEnabled: true,
|
|
blurIntensity: 50,
|
|
});
|
|
expect(result.equals(subject)).toBe(false);
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
});
|
|
|
|
it("blurEnabled with blur bgType blurs the original background", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const result = await applyEffects(subject, original, {
|
|
backgroundType: "blur",
|
|
blurEnabled: true,
|
|
blurIntensity: 30,
|
|
});
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
expect(result.equals(subject)).toBe(false);
|
|
});
|
|
|
|
it("color backgroundType composites on solid color", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const result = await applyEffects(subject, original, {
|
|
backgroundType: "color",
|
|
backgroundColor: "#00ff00",
|
|
});
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
expect(result.equals(subject)).toBe(false);
|
|
});
|
|
|
|
it("gradient backgroundType composites on gradient", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const result = await applyEffects(subject, original, {
|
|
backgroundType: "gradient",
|
|
gradientColor1: "#ff0000",
|
|
gradientColor2: "#0000ff",
|
|
gradientAngle: 90,
|
|
});
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
expect(result.equals(subject)).toBe(false);
|
|
});
|
|
|
|
it("image backgroundType composites on provided image", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const bgImage = await sharp({
|
|
create: {
|
|
width: 200,
|
|
height: 200,
|
|
channels: 3,
|
|
background: { r: 0, g: 0, b: 255 },
|
|
},
|
|
})
|
|
.png()
|
|
.toBuffer();
|
|
const result = await applyEffects(subject, original, {
|
|
backgroundType: "image",
|
|
backgroundImageBuffer: bgImage,
|
|
});
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
expect(meta.width).toBe(100);
|
|
expect(meta.height).toBe(100);
|
|
});
|
|
|
|
it("image backgroundType with blurEnabled blurs the background image", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const bgImage = await createMultiColorImage(200, 200);
|
|
const withoutBlur = await applyEffects(subject, original, {
|
|
backgroundType: "image",
|
|
backgroundImageBuffer: bgImage,
|
|
});
|
|
const withBlur = await applyEffects(subject, original, {
|
|
backgroundType: "image",
|
|
backgroundImageBuffer: bgImage,
|
|
blurEnabled: true,
|
|
blurIntensity: 50,
|
|
});
|
|
expect(withBlur.equals(withoutBlur)).toBe(false);
|
|
});
|
|
|
|
it("combined shadow + color background works", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const result = await applyEffects(subject, original, {
|
|
backgroundType: "color",
|
|
backgroundColor: "#ffffff",
|
|
shadowEnabled: true,
|
|
shadowOpacity: 60,
|
|
});
|
|
const meta = await sharp(result).metadata();
|
|
expect(meta.format).toBe("png");
|
|
expect(meta.width).toBe(100);
|
|
expect(meta.height).toBe(100);
|
|
});
|
|
|
|
it("defaults blurIntensity to 50 when not provided", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const withDefault = await applyEffects(subject, original, {
|
|
backgroundType: "blur",
|
|
blurEnabled: true,
|
|
});
|
|
const withExplicit = await applyEffects(subject, original, {
|
|
backgroundType: "blur",
|
|
blurEnabled: true,
|
|
blurIntensity: 50,
|
|
});
|
|
expect(withDefault.equals(withExplicit)).toBe(true);
|
|
});
|
|
|
|
it("shadowOpacity 0 does not add shadow", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const result = await applyEffects(subject, original, {
|
|
backgroundType: "transparent",
|
|
shadowEnabled: true,
|
|
shadowOpacity: 0,
|
|
});
|
|
expect(result.equals(subject)).toBe(true);
|
|
});
|
|
|
|
it("defaults backgroundType to transparent when not provided", async () => {
|
|
const subject = await createSubjectWithAlpha(100, 100);
|
|
const original = await createTestImage(100, 100, 3);
|
|
const result = await applyEffects(subject, original, {});
|
|
expect(result.equals(subject)).toBe(true);
|
|
});
|
|
});
|