import { createRequire } from "node:module"; import path from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; // sharp is only installed in the image-engine package, so resolve it from there const require = createRequire( path.resolve(__dirname, "../../../packages/image-engine/src/index.ts"), ); const sharp = require("sharp") as typeof import("sharp").default; const exifReader = require( path.resolve(__dirname, "../../../packages/image-engine/node_modules/exif-reader"), ) as typeof import("exif-reader").default; import { brightness, colorChannels, compress, contrast, convert, crop, editMetadata, flip, grayscale, invert, optimizeForWeb, parseExif, parseGps, parseXmp, processImage, resize, rotate, sanitizeValue, saturation, sepia, sharpen, sharpenAdvanced, stripMetadata, } from "@snapotter/image-engine"; import { fixtures, readFixture } from "../../fixtures/index.js"; // Helper to get metadata from a sharp pipeline result async function getMeta(img: sharp.Sharp) { const buf = await img.toBuffer(); return sharp(buf).metadata(); } // Helper to get raw pixel data async function _getPixels(img: sharp.Sharp) { return img.raw().toBuffer({ resolveWithObject: true }); } let png200x150: Buffer; let png1x1: Buffer; let jpg100x100: Buffer; let webp50x50: Buffer; let jpgWithExif: Buffer; beforeAll(() => { png200x150 = readFixture(fixtures.image.base.png200); png1x1 = readFixture(fixtures.image.edge.px1); jpg100x100 = readFixture(fixtures.image.base.jpg100); webp50x50 = readFixture(fixtures.image.base.webp50); jpgWithExif = readFixture(fixtures.image.exifGps); }); // --------------------------------------------------------------------------- // resize // --------------------------------------------------------------------------- describe("resize", () => { it("resizes to explicit width and height", async () => { const img = sharp(png200x150); const result = await resize(img, { width: 100, height: 75 }); const meta = await getMeta(result); expect(meta.width).toBe(100); expect(meta.height).toBe(75); }); it("resizes with width only (height derived)", async () => { const img = sharp(png200x150); const result = await resize(img, { width: 50 }); const meta = await getMeta(result); expect(meta.width).toBe(50); // height should be derived based on cover fit expect(meta.height).toBeGreaterThan(0); }); it("resizes with height only (width derived)", async () => { const img = sharp(png200x150); const result = await resize(img, { height: 30 }); const meta = await getMeta(result); expect(meta.height).toBe(30); expect(meta.width).toBeGreaterThan(0); }); it("resizes by percentage", async () => { const img = sharp(png200x150); const result = await resize(img, { percentage: 50 }); const meta = await getMeta(result); expect(meta.width).toBe(100); expect(meta.height).toBe(75); }); it("percentage = 200 doubles size", async () => { const img = sharp(jpg100x100); const result = await resize(img, { percentage: 200 }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(200); }); it("percentage rounds correctly for non-integer results", async () => { // 200 * 33/100 = 66, 150 * 33/100 = 49.5 -> 50 const img = sharp(png200x150); const result = await resize(img, { percentage: 33 }); const meta = await getMeta(result); expect(meta.width).toBe(66); expect(meta.height).toBe(50); }); it("withoutEnlargement prevents upscaling", async () => { const img = sharp(png1x1); const result = await resize(img, { width: 500, height: 500, withoutEnlargement: true, }); const meta = await getMeta(result); expect(meta.width).toBeLessThanOrEqual(1); expect(meta.height).toBeLessThanOrEqual(1); }); it("fit=contain preserves aspect ratio within box", async () => { const img = sharp(png200x150); const result = await resize(img, { width: 100, height: 100, fit: "contain" }); const meta = await getMeta(result); // contain fits the image within the box; dimensions are at most 100x100 // 200x150 (4:3) contained in 100x100 -> width=100, height stays 100 // (sharp contain fits inside the box, output matches requested dimensions with padding) expect(meta.width).toBeLessThanOrEqual(100); expect(meta.height).toBeLessThanOrEqual(100); }); it("fit=fill stretches to exact dimensions", async () => { const img = sharp(png200x150); const result = await resize(img, { width: 50, height: 50, fit: "fill" }); const meta = await getMeta(result); expect(meta.width).toBe(50); expect(meta.height).toBe(50); }); // -- Error cases -- it("throws on width = 0", async () => { const img = sharp(png200x150); await expect(resize(img, { width: 0 })).rejects.toThrow("Resize width must be greater than 0"); }); it("throws on negative width", async () => { const img = sharp(png200x150); await expect(resize(img, { width: -10, height: 50 })).rejects.toThrow( "Resize width must be greater than 0", ); }); it("throws on negative height", async () => { const img = sharp(png200x150); await expect(resize(img, { width: 50, height: -1 })).rejects.toThrow( "Resize height must be greater than 0", ); }); it("throws on height = 0", async () => { const img = sharp(png200x150); await expect(resize(img, { height: 0 })).rejects.toThrow( "Resize height must be greater than 0", ); }); it("throws when neither width, height, nor percentage given", async () => { const img = sharp(png200x150); await expect(resize(img, {})).rejects.toThrow("Resize requires width, height, or percentage"); }); it("throws on percentage = 0", async () => { const img = sharp(png200x150); await expect(resize(img, { percentage: 0 })).rejects.toThrow( "Resize percentage must be greater than 0", ); }); it("throws on negative percentage", async () => { const img = sharp(png200x150); await expect(resize(img, { percentage: -50 })).rejects.toThrow( "Resize percentage must be greater than 0", ); }); it("withoutEnlargement clamps height only (no width given)", async () => { const img = sharp(png1x1); const result = await resize(img, { height: 500, withoutEnlargement: true, }); const meta = await getMeta(result); expect(meta.height).toBeLessThanOrEqual(1); }); it("withoutEnlargement does not clamp when target is smaller", async () => { const img = sharp(png200x150); const result = await resize(img, { width: 50, height: 50, withoutEnlargement: true, }); const meta = await getMeta(result); expect(meta.width).toBe(50); expect(meta.height).toBe(50); }); it("percentage on a 1x1 image rounds to at least 1px (percentage >= 50)", async () => { // 1 * 50 / 100 = 0.5 -> rounds to 1 (Math.round), so width=1 const img = sharp(png1x1); const result = await resize(img, { percentage: 50 }); const meta = await getMeta(result); // Math.round(0.5) = 1 in some environments, 0 in others; if 0, sharp should error expect(meta.width).toBeGreaterThanOrEqual(0); }); it("percentage=1 on 1x1 clamps to 1px minimum", async () => { const img = sharp(png1x1); const result = await resize(img, { percentage: 1 }); const meta = await result.toBuffer().then((b) => sharp(b).metadata()); expect(meta.width).toBe(1); expect(meta.height).toBe(1); }); }); // --------------------------------------------------------------------------- // crop // --------------------------------------------------------------------------- describe("crop", () => { it("crops a valid region", async () => { const img = sharp(png200x150); const result = await crop(img, { left: 10, top: 20, width: 50, height: 40 }); const meta = await getMeta(result); expect(meta.width).toBe(50); expect(meta.height).toBe(40); }); it("crops the full image (0,0 to full dimensions)", async () => { const img = sharp(png200x150); const result = await crop(img, { left: 0, top: 0, width: 200, height: 150 }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("crops a 1x1 region from top-left corner", async () => { const img = sharp(png200x150); const result = await crop(img, { left: 0, top: 0, width: 1, height: 1 }); const meta = await getMeta(result); expect(meta.width).toBe(1); expect(meta.height).toBe(1); }); it("crops from the last pixel", async () => { const img = sharp(png200x150); const result = await crop(img, { left: 199, top: 149, width: 1, height: 1 }); const meta = await getMeta(result); expect(meta.width).toBe(1); expect(meta.height).toBe(1); }); it("crops using percent unit", async () => { const img = sharp(png200x150); // 50% of 200 = 100, 50% of 150 = 75, starting at 10% left, 10% top const result = await crop(img, { left: 10, top: 10, width: 50, height: 50, unit: "percent", }); const meta = await getMeta(result); expect(meta.width).toBe(100); // 50% of 200 expect(meta.height).toBe(75); // 50% of 150 (Math.round) }); it("crops 100% of image using percent unit", async () => { const img = sharp(png200x150); const result = await crop(img, { left: 0, top: 0, width: 100, height: 100, unit: "percent", }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); // -- Error cases -- it("throws on width = 0", async () => { const img = sharp(png200x150); await expect(crop(img, { left: 0, top: 0, width: 0, height: 50 })).rejects.toThrow( "Crop width and height must be greater than 0", ); }); it("throws on height = 0", async () => { const img = sharp(png200x150); await expect(crop(img, { left: 0, top: 0, width: 50, height: 0 })).rejects.toThrow( "Crop width and height must be greater than 0", ); }); it("throws on negative width", async () => { const img = sharp(png200x150); await expect(crop(img, { left: 0, top: 0, width: -10, height: 50 })).rejects.toThrow( "Crop width and height must be greater than 0", ); }); it("throws on negative left", async () => { const img = sharp(png200x150); await expect(crop(img, { left: -1, top: 0, width: 10, height: 10 })).rejects.toThrow( "Crop left and top must be non-negative", ); }); it("throws on negative top", async () => { const img = sharp(png200x150); await expect(crop(img, { left: 0, top: -5, width: 10, height: 10 })).rejects.toThrow( "Crop left and top must be non-negative", ); }); it("throws when crop region exceeds image width", async () => { const img = sharp(png200x150); await expect(crop(img, { left: 150, top: 0, width: 100, height: 10 })).rejects.toThrow( "Crop region exceeds image width", ); }); it("throws when crop region exceeds image height", async () => { const img = sharp(png200x150); await expect(crop(img, { left: 0, top: 100, width: 10, height: 100 })).rejects.toThrow( "Crop region exceeds image height", ); }); it("throws when left alone puts region out of bounds", async () => { const img = sharp(png200x150); await expect(crop(img, { left: 200, top: 0, width: 1, height: 1 })).rejects.toThrow( "Crop region exceeds image width", ); }); it("throws when crop is 1px past the boundary", async () => { // left=199, width=2 -> 201 > 200 const img = sharp(png200x150); await expect(crop(img, { left: 199, top: 0, width: 2, height: 1 })).rejects.toThrow( "Crop region exceeds image width", ); }); }); // --------------------------------------------------------------------------- // rotate // --------------------------------------------------------------------------- describe("rotate", () => { it("rotates 90 degrees (dimensions swap)", async () => { const img = sharp(png200x150); const result = await rotate(img, { angle: 90 }); const meta = await getMeta(result); expect(meta.width).toBe(150); expect(meta.height).toBe(200); }); it("rotates 180 degrees (dimensions stay same)", async () => { const img = sharp(png200x150); const result = await rotate(img, { angle: 180 }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("rotates 270 degrees (dimensions swap)", async () => { const img = sharp(png200x150); const result = await rotate(img, { angle: 270 }); const meta = await getMeta(result); expect(meta.width).toBe(150); expect(meta.height).toBe(200); }); it("rotates 0 degrees (no-op)", async () => { const img = sharp(png200x150); const result = await rotate(img, { angle: 0 }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("rotates 360 degrees (no-op equivalent)", async () => { const img = sharp(png200x150); const result = await rotate(img, { angle: 360 }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("arbitrary angle produces a larger bounding box", async () => { const img = sharp(jpg100x100); const result = await rotate(img, { angle: 45 }); const meta = await getMeta(result); // 100x100 rotated 45 deg -> bounding box is ~141x141 expect(meta.width!).toBeGreaterThan(100); expect(meta.height!).toBeGreaterThan(100); }); it("arbitrary angle uses custom background color", async () => { const img = sharp(jpg100x100); const result = await rotate(img, { angle: 30, background: "#ff0000" }); // Just check it does not throw and produces output const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("arbitrary angle defaults background to black", async () => { // Rotate an image by an odd angle and verify the corner pixel // is black (the default fill). const img = sharp( await sharp({ create: { width: 10, height: 10, channels: 3, background: "#ffffff" } }) .png() .toBuffer(), ); const result = await rotate(img, { angle: 45 }); const { data, info } = await result.raw().toBuffer({ resolveWithObject: true }); // top-left corner pixel should be close to black (the fill) const r = data[0]; const g = data[1]; const b = data[2]; // Allow some tolerance for anti-aliasing expect(r).toBeLessThan(30); expect(g).toBeLessThan(30); expect(b).toBeLessThan(30); }); it("negative angle rotates counter-clockwise", async () => { const img = sharp(png200x150); const result = await rotate(img, { angle: -90 }); const meta = await getMeta(result); expect(meta.width).toBe(150); expect(meta.height).toBe(200); }); }); // --------------------------------------------------------------------------- // flip // --------------------------------------------------------------------------- describe("flip", () => { it("flips horizontally", async () => { const img = sharp(png200x150); const result = await flip(img, { horizontal: true }); const meta = await getMeta(result); // Dimensions unchanged expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("flips vertically", async () => { const img = sharp(png200x150); const result = await flip(img, { vertical: true }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("flips both horizontal and vertical", async () => { const img = sharp(png200x150); const result = await flip(img, { horizontal: true, vertical: true }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("horizontal flip actually mirrors pixels", async () => { // Create a 2x1 image: left pixel red, right pixel blue const _buf = await sharp({ create: { width: 2, height: 1, channels: 3, background: "#000000" }, }) .raw() .toBuffer(); // Manually set: pixel 0 = red, pixel 1 = blue const pixels = Buffer.from([255, 0, 0, 0, 0, 255]); const imgBuf = await sharp(pixels, { raw: { width: 2, height: 1, channels: 3 } }) .png() .toBuffer(); const img = sharp(imgBuf); const result = await flip(img, { horizontal: true }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // After horizontal flip: pixel 0 should be blue, pixel 1 should be red expect(data[0]).toBe(0); // R of first pixel expect(data[1]).toBe(0); // G expect(data[2]).toBe(255); // B expect(data[3]).toBe(255); // R of second pixel expect(data[4]).toBe(0); expect(data[5]).toBe(0); }); it("vertical flip actually mirrors pixels", async () => { // 1x2 image: top pixel red, bottom pixel blue const pixels = Buffer.from([255, 0, 0, 0, 0, 255]); const imgBuf = await sharp(pixels, { raw: { width: 1, height: 2, channels: 3 } }) .png() .toBuffer(); const img = sharp(imgBuf); const result = await flip(img, { vertical: true }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // After vertical flip: top pixel should be blue, bottom should be red expect(data[0]).toBe(0); expect(data[1]).toBe(0); expect(data[2]).toBe(255); expect(data[3]).toBe(255); expect(data[4]).toBe(0); expect(data[5]).toBe(0); }); // -- Error cases -- it("throws when neither horizontal nor vertical specified", async () => { const img = sharp(png200x150); await expect(flip(img, {})).rejects.toThrow( "Flip requires at least one of horizontal or vertical", ); }); it("throws when both are explicitly false", async () => { const img = sharp(png200x150); await expect(flip(img, { horizontal: false, vertical: false })).rejects.toThrow( "Flip requires at least one of horizontal or vertical", ); }); }); // --------------------------------------------------------------------------- // convert // --------------------------------------------------------------------------- describe("convert", () => { it.each([ ["jpg", "jpeg"], ["png", "png"], ["webp", "webp"], ["tiff", "tiff"], ["gif", "gif"], ] as const)("converts to %s format", async (fmt, expectedSharpFmt) => { const img = sharp(png200x150); const result = await convert(img, { format: fmt as any }); const meta = await getMeta(result); expect(meta.format).toBe(expectedSharpFmt); }); it("converts to avif format", async () => { const img = sharp(png200x150); const result = await convert(img, { format: "avif" }); const meta = await getMeta(result); expect(meta.format).toBe("heif"); }); it("applies quality parameter", async () => { const imgHigh = sharp(png200x150); const resultHigh = await convert(imgHigh, { format: "jpg", quality: 95 }); const bufHigh = await resultHigh.toBuffer(); const imgLow = sharp(png200x150); const resultLow = await convert(imgLow, { format: "jpg", quality: 10 }); const bufLow = await resultLow.toBuffer(); // Low quality JPEG should be smaller than high quality expect(bufLow.length).toBeLessThan(bufHigh.length); }); it("quality = 1 (minimum) does not throw", async () => { const img = sharp(png200x150); const result = await convert(img, { format: "webp", quality: 1 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("quality = 100 (maximum) does not throw", async () => { const img = sharp(png200x150); const result = await convert(img, { format: "webp", quality: 100 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); // -- Error cases -- it("throws on invalid format", async () => { const img = sharp(png200x150); await expect(convert(img, { format: "bmp" as any })).rejects.toThrow( "Unsupported output format: bmp", ); }); it("throws on quality = 0", async () => { const img = sharp(png200x150); await expect(convert(img, { format: "jpg", quality: 0 })).rejects.toThrow( "Quality must be between 1 and 100", ); }); it("throws on quality = 101", async () => { const img = sharp(png200x150); await expect(convert(img, { format: "jpg", quality: 101 })).rejects.toThrow( "Quality must be between 1 and 100", ); }); it("throws on negative quality", async () => { const img = sharp(png200x150); await expect(convert(img, { format: "jpg", quality: -5 })).rejects.toThrow( "Quality must be between 1 and 100", ); }); it("throws on quality = 999", async () => { const img = sharp(png200x150); await expect(convert(img, { format: "png", quality: 999 })).rejects.toThrow( "Quality must be between 1 and 100", ); }); }); // --------------------------------------------------------------------------- // compress // --------------------------------------------------------------------------- describe("compress", () => { it("compresses with explicit quality", async () => { const img = sharp(png200x150); const result = await compress(img, { quality: 50, format: "jpg" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("defaults to quality 80 when no quality or target specified", async () => { const img = sharp(png200x150); // Should not throw const result = await compress(img, { format: "jpg" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("lower quality produces smaller output", async () => { const bufHigh = await ( await compress(sharp(png200x150), { quality: 90, format: "jpg" }) ).toBuffer(); const bufLow = await ( await compress(sharp(png200x150), { quality: 10, format: "jpg" }) ).toBuffer(); expect(bufLow.length).toBeLessThan(bufHigh.length); }); it("compresses with target size (binary search)", async () => { // Target 5KB for a 200x150 PNG const targetBytes = 5000; const img = sharp(png200x150); const result = await compress(img, { targetSizeBytes: targetBytes, format: "jpg" }); const buf = await result.toBuffer(); // The binary search should get within tolerance (5%) or as close as possible // Allow some slack since very small images may not compress below target expect(buf.length).toBeGreaterThan(0); }); it("target size binary search gets reasonably close", async () => { const bigBuf = await sharp({ create: { width: 400, height: 300, channels: 3, background: "#884422" }, }) .png() .toBuffer(); const targetBytes = 10000; const result = await compress(sharp(bigBuf), { targetSizeBytes: targetBytes, format: "jpg", }); const buf = await result.toBuffer(); expect(buf.length).toBeLessThanOrEqual(targetBytes); }); it("uses input format when no format specified", async () => { const img = sharp(jpg100x100); const result = await compress(img, { quality: 50 }); const meta = await getMeta(result); expect(meta.format).toBe("jpeg"); }); // -- Error cases -- it("throws on quality = 0", async () => { const img = sharp(png200x150); await expect(compress(img, { quality: 0, format: "jpg" })).rejects.toThrow( "Quality must be between 1 and 100", ); }); it("throws on quality = 101", async () => { const img = sharp(png200x150); await expect(compress(img, { quality: 101, format: "jpg" })).rejects.toThrow( "Quality must be between 1 and 100", ); }); it("throws on negative quality", async () => { const img = sharp(png200x150); await expect(compress(img, { quality: -10, format: "jpg" })).rejects.toThrow( "Quality must be between 1 and 100", ); }); it("throws on targetSizeBytes = 0", async () => { const img = sharp(png200x150); await expect(compress(img, { targetSizeBytes: 0, format: "jpg" })).rejects.toThrow( "Target size must be greater than 0", ); }); it("throws on negative targetSizeBytes", async () => { const img = sharp(png200x150); await expect(compress(img, { targetSizeBytes: -100, format: "jpg" })).rejects.toThrow( "Target size must be greater than 0", ); }); it("falls back to PNG for SVG input (NO_ENCODER format)", async () => { const svgBuf = readFixture(fixtures.image.formats("svg")); const img = sharp(svgBuf); const result = await compress(img, { quality: 80 }); const meta = await getMeta(result); expect(meta.format).toBe("png"); }); it("target size early break when within tolerance", async () => { // Use a large solid-color image so binary search finishes quickly const bigBuf = await sharp({ create: { width: 800, height: 600, channels: 3, background: "#cc6633" }, }) .jpeg({ quality: 95 }) .toBuffer(); // Set target to the actual size so it matches within tolerance immediately const targetBytes = bigBuf.length; const result = await compress(sharp(bigBuf), { targetSizeBytes: targetBytes, format: "jpg", }); const buf = await result.toBuffer(); // Should be within 5% tolerance of target expect(Math.abs(buf.length - targetBytes) / targetBytes).toBeLessThan(0.5); }); it("compresses with format=avif adds effort option", async () => { const img = sharp(png200x150); const result = await compress(img, { quality: 50, format: "avif" }); const meta = await getMeta(result); expect(meta.format).toBe("heif"); }); it("target size falls back to dimension reduction when quality alone fails", async () => { const bigBuf = await sharp({ create: { width: 200, height: 200, channels: 3, background: "#ff8800" }, }) .jpeg({ quality: 100 }) .toBuffer(); const result = await compress(sharp(bigBuf), { targetSizeBytes: 1, format: "jpg", }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); const meta = await sharp(buf).metadata(); expect(meta.width).toBeLessThan(200); }); }); // --------------------------------------------------------------------------- // compress -- target-size accuracy // --------------------------------------------------------------------------- describe("compress target-size accuracy", () => { it("output never exceeds target size for JPEG", async () => { const largeBuf = await sharp({ create: { width: 800, height: 600, channels: 3, background: "#3366aa" }, }) .jpeg({ quality: 100 }) .toBuffer(); const targetBytes = 5000; const result = await compress(sharp(largeBuf), { targetSizeBytes: targetBytes, format: "jpg", }); const buf = await result.toBuffer(); expect(buf.length).toBeLessThanOrEqual(targetBytes); }); it("output never exceeds target size for PNG", async () => { const largeBuf = await sharp({ create: { width: 600, height: 400, channels: 4, background: "#cc4488" }, }) .png() .toBuffer(); const targetBytes = 3000; const result = await compress(sharp(largeBuf), { targetSizeBytes: targetBytes, format: "png", }); const buf = await result.toBuffer(); expect(buf.length).toBeLessThanOrEqual(targetBytes); }); it("output never exceeds target size for WebP", async () => { const largeBuf = await sharp({ create: { width: 800, height: 600, channels: 3, background: "#99bb22" }, }) .webp({ quality: 100 }) .toBuffer(); const targetBytes = 4000; const result = await compress(sharp(largeBuf), { targetSizeBytes: targetBytes, format: "webp", }); const buf = await result.toBuffer(); expect(buf.length).toBeLessThanOrEqual(targetBytes); }); it("aggressive target on large image triggers dimension reduction", async () => { const width = 1200; const height = 900; const channels = 3; const rawData = Buffer.alloc(width * height * channels); for (let i = 0; i < rawData.length; i++) { rawData[i] = (i * 7 + 13) % 256; } const largeBuf = await sharp(rawData, { raw: { width, height, channels } }) .jpeg({ quality: 95 }) .toBuffer(); const targetBytes = 5000; const result = await compress(sharp(largeBuf), { targetSizeBytes: targetBytes, format: "jpg", }); const buf = await result.toBuffer(); expect(buf.length).toBeLessThanOrEqual(targetBytes); const meta = await sharp(buf).metadata(); expect(meta.width).toBeLessThan(1200); expect(meta.height).toBeLessThan(900); }); it("500KB-to-50KB scenario produces output at or below target", async () => { const noisyBuf = await sharp({ create: { width: 1600, height: 1200, channels: 3, background: "#447799" }, }) .jpeg({ quality: 100 }) .toBuffer(); const targetBytes = 50 * 1024; const result = await compress(sharp(noisyBuf), { targetSizeBytes: targetBytes, format: "jpg", }); const buf = await result.toBuffer(); expect(buf.length).toBeLessThanOrEqual(targetBytes); expect(buf.length).toBeGreaterThan(0); }); it("preserves requested format during dimension reduction", async () => { const width = 1200; const height = 900; const channels = 3; const rawData = Buffer.alloc(width * height * channels); for (let i = 0; i < rawData.length; i++) { rawData[i] = (i * 7 + 13) % 256; } const largeBuf = await sharp(rawData, { raw: { width, height, channels } }) .jpeg({ quality: 95 }) .toBuffer(); const targetBytes = 5000; const result = await compress(sharp(largeBuf), { targetSizeBytes: targetBytes, format: "webp", }); const buf = await result.toBuffer(); const meta = await sharp(buf).metadata(); expect(meta.format).toBe("webp"); expect(buf.length).toBeLessThanOrEqual(targetBytes); expect(meta.width).toBeLessThan(1200); }); it("does not reduce dimensions when quality alone suffices", async () => { const buf = await sharp({ create: { width: 400, height: 300, channels: 3, background: "#112233" }, }) .jpeg({ quality: 95 }) .toBuffer(); const targetBytes = buf.length + 5000; const result = await compress(sharp(buf), { targetSizeBytes: targetBytes, format: "jpg", }); const outBuf = await result.toBuffer(); const meta = await sharp(outBuf).metadata(); expect(meta.width).toBe(400); expect(meta.height).toBe(300); expect(outBuf.length).toBeLessThanOrEqual(targetBytes); }); }); // --------------------------------------------------------------------------- // strip-metadata // --------------------------------------------------------------------------- describe("stripMetadata", () => { it("returns an image (default strips all)", async () => { const img = sharp(jpg100x100); const result = await stripMetadata(img); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("stripAll=true returns image", async () => { const img = sharp(jpg100x100); const result = await stripMetadata(img, { stripAll: true }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("selective strip (stripExif only) returns image", async () => { const img = sharp(jpg100x100); const result = await stripMetadata(img, { stripExif: true }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("empty options defaults to strip all", async () => { const img = sharp(jpg100x100); const result = await stripMetadata(img, {}); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("preserves image dimensions", async () => { const img = sharp(png200x150); const result = await stripMetadata(img); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("stripIcc=true enters selective stripping path", async () => { const img = sharp(jpg100x100); const result = await stripMetadata(img, { stripIcc: true }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("stripGps=true enters selective stripping path", async () => { const img = sharp(jpg100x100); const result = await stripMetadata(img, { stripGps: true }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("stripXmp=true enters selective stripping path", async () => { const img = sharp(jpg100x100); const result = await stripMetadata(img, { stripXmp: true }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); }); // --------------------------------------------------------------------------- // brightness // --------------------------------------------------------------------------- describe("brightness", () => { it("value = 0 makes no visible change", async () => { const img = sharp(png200x150); const result = await brightness(img, { value: 0 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("positive value brightens the image", async () => { // Create a dark gray image and brighten it const darkBuf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#404040" }, }) .png() .toBuffer(); const result = await brightness(sharp(darkBuf), { value: 50 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // Brightness multiplier = 1.5 => 0x40 (64) * 1.5 = 96 (0x60) // Allow tolerance for rounding expect(data[0]).toBeGreaterThan(64); }); it("negative value darkens the image", async () => { const lightBuf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#c0c0c0" }, }) .png() .toBuffer(); const result = await brightness(sharp(lightBuf), { value: -50 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // 0xC0 (192) * 0.5 = 96 expect(data[0]).toBeLessThan(192); }); it("value = -100 produces black", async () => { const whiteBuf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#ffffff" }, }) .png() .toBuffer(); const result = await brightness(sharp(whiteBuf), { value: -100 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // multiplier = 0 -> all pixels should be 0 expect(data[0]).toBe(0); expect(data[1]).toBe(0); expect(data[2]).toBe(0); }); it("boundary value = 100 does not throw", async () => { const img = sharp(png200x150); const result = await brightness(img, { value: 100 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("boundary value = -100 does not throw", async () => { const img = sharp(png200x150); const result = await brightness(img, { value: -100 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); // -- Error cases -- it("throws on value = -101", async () => { const img = sharp(png200x150); await expect(brightness(img, { value: -101 })).rejects.toThrow( "Brightness value must be between -100 and +100", ); }); it("throws on value = 101", async () => { const img = sharp(png200x150); await expect(brightness(img, { value: 101 })).rejects.toThrow( "Brightness value must be between -100 and +100", ); }); it("throws on extreme values", async () => { const img = sharp(png200x150); await expect(brightness(img, { value: 1000 })).rejects.toThrow( "Brightness value must be between -100 and +100", ); }); }); // --------------------------------------------------------------------------- // contrast // --------------------------------------------------------------------------- describe("contrast", () => { it("value = 0 makes no visible change", async () => { const img = sharp(png200x150); const result = await contrast(img, { value: 0 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("positive value increases contrast", async () => { // A mid-gray image with contrast boost should remain mid-gray (128 is the pivot) // But a light pixel should get lighter const grayBuf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#c0c0c0" }, }) .png() .toBuffer(); const result = await contrast(sharp(grayBuf), { value: 50 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // slope=1.5, intercept=128*(1-1.5)=-64 -> 192*1.5 - 64 = 224 expect(data[0]).toBeGreaterThan(192); }); it("negative contrast pushes values toward mid-gray", async () => { const whiteBuf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#ffffff" }, }) .png() .toBuffer(); const result = await contrast(sharp(whiteBuf), { value: -50 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // slope=0.5, intercept=64 -> 255*0.5 + 64 = 191.5 expect(data[0]).toBeLessThan(255); expect(data[0]).toBeGreaterThan(128); }); // -- Error cases -- it("throws on value = -101", async () => { const img = sharp(png200x150); await expect(contrast(img, { value: -101 })).rejects.toThrow( "Contrast value must be between -100 and +100", ); }); it("throws on value = 101", async () => { const img = sharp(png200x150); await expect(contrast(img, { value: 101 })).rejects.toThrow( "Contrast value must be between -100 and +100", ); }); }); // --------------------------------------------------------------------------- // saturation // --------------------------------------------------------------------------- describe("saturation", () => { it("value = 0 makes no change", async () => { const img = sharp(png200x150); const result = await saturation(img, { value: 0 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("value = -100 desaturates to grayscale-like", async () => { // Create a colored image const colorBuf = await sharp({ create: { width: 10, height: 10, channels: 3, background: "#ff0000" }, }) .png() .toBuffer(); const result = await saturation(sharp(colorBuf), { value: -100 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // With saturation=0, R/G/B should all be the same (grayscale) expect(data[0]).toBe(data[1]); expect(data[1]).toBe(data[2]); }); it("boundary value 100 does not throw", async () => { const img = sharp(png200x150); const result = await saturation(img, { value: 100 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); // -- Error cases -- it("throws on value = -101", async () => { const img = sharp(png200x150); await expect(saturation(img, { value: -101 })).rejects.toThrow( "Saturation value must be between -100 and +100", ); }); it("throws on value = 101", async () => { const img = sharp(png200x150); await expect(saturation(img, { value: 101 })).rejects.toThrow( "Saturation value must be between -100 and +100", ); }); }); // --------------------------------------------------------------------------- // colorChannels // --------------------------------------------------------------------------- describe("colorChannels", () => { it("100/100/100 is a no-op", async () => { // All channels at 100 = multiplier 1.0, should be identity const buf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#804020" }, }) .png() .toBuffer(); const original = await sharp(buf).raw().toBuffer({ resolveWithObject: true }); const result = await colorChannels(sharp(buf), { red: 100, green: 100, blue: 100 }); const processed = await result.raw().toBuffer({ resolveWithObject: true }); // Should be identical (or very close) expect(Math.abs(processed.data[0] - original.data[0])).toBeLessThanOrEqual(1); expect(Math.abs(processed.data[1] - original.data[1])).toBeLessThanOrEqual(1); expect(Math.abs(processed.data[2] - original.data[2])).toBeLessThanOrEqual(1); }); it("zeroing red channel removes red", async () => { const buf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#ff8040" }, }) .png() .toBuffer(); const result = await colorChannels(sharp(buf), { red: 0, green: 100, blue: 100 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); expect(data[0]).toBe(0); // Red should be 0 }); it("doubling green channel doubles green value (clamped)", async () => { const buf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#006400" }, }) .png() .toBuffer(); const result = await colorChannels(sharp(buf), { red: 100, green: 200, blue: 100 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // Green was 100 (0x64), doubled = 200 expect(data[1]).toBe(200); }); it("boundary values 0/0/0 makes everything black", async () => { const buf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#ffffff" }, }) .png() .toBuffer(); const result = await colorChannels(sharp(buf), { red: 0, green: 0, blue: 0 }); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); expect(data[0]).toBe(0); expect(data[1]).toBe(0); expect(data[2]).toBe(0); }); // -- Error cases -- it("throws when red < 0", async () => { const img = sharp(png200x150); await expect(colorChannels(img, { red: -1, green: 100, blue: 100 })).rejects.toThrow( "Red channel value must be between 0 and 200", ); }); it("throws when red > 200", async () => { const img = sharp(png200x150); await expect(colorChannels(img, { red: 201, green: 100, blue: 100 })).rejects.toThrow( "Red channel value must be between 0 and 200", ); }); it("throws when green < 0", async () => { const img = sharp(png200x150); await expect(colorChannels(img, { red: 100, green: -1, blue: 100 })).rejects.toThrow( "Green channel value must be between 0 and 200", ); }); it("throws when green > 200", async () => { const img = sharp(png200x150); await expect(colorChannels(img, { red: 100, green: 201, blue: 100 })).rejects.toThrow( "Green channel value must be between 0 and 200", ); }); it("throws when blue < 0", async () => { const img = sharp(png200x150); await expect(colorChannels(img, { red: 100, green: 100, blue: -1 })).rejects.toThrow( "Blue channel value must be between 0 and 200", ); }); it("throws when blue > 200", async () => { const img = sharp(png200x150); await expect(colorChannels(img, { red: 100, green: 100, blue: 201 })).rejects.toThrow( "Blue channel value must be between 0 and 200", ); }); }); // --------------------------------------------------------------------------- // grayscale // --------------------------------------------------------------------------- describe("grayscale", () => { it("converts a color image to grayscale", async () => { const colorBuf = await sharp({ create: { width: 10, height: 10, channels: 3, background: "#ff0000" }, }) .png() .toBuffer(); const result = await grayscale(sharp(colorBuf)); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // In grayscale, all R=G=B per pixel (single channel or equal channels) // For a pure red (255,0,0), luminance is around 54 expect(data[0]).toBeGreaterThan(0); expect(data[0]).toBeLessThan(255); }); it("preserves dimensions", async () => { const img = sharp(png200x150); const result = await grayscale(img); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("already grayscale image does not throw", async () => { // sharp create requires channels 3 or 4; create a gray image then convert to grayscale first const grayBuf = await sharp({ create: { width: 5, height: 5, channels: 3, background: "#808080" }, }) .grayscale() .png() .toBuffer(); const result = await grayscale(sharp(grayBuf)); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); }); // --------------------------------------------------------------------------- // sepia // --------------------------------------------------------------------------- describe("sepia", () => { it("applies sepia tone to a white image", async () => { const whiteBuf = await sharp({ create: { width: 10, height: 10, channels: 3, background: "#ffffff" }, }) .png() .toBuffer(); const result = await sepia(sharp(whiteBuf)); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); // Sepia matrix on white (255,255,255): // R = 255*(0.393+0.769+0.189) = 255*1.351 -> clamped to 255 // G = 255*(0.349+0.686+0.168) = 255*1.203 -> clamped to 255 // B = 255*(0.272+0.534+0.131) = 255*0.937 -> ~239 // R >= G >= B is the characteristic sepia ordering expect(data[0]).toBeGreaterThanOrEqual(data[1]); // R >= G expect(data[1]).toBeGreaterThanOrEqual(data[2]); // G >= B }); it("applies sepia tone to a colored image", async () => { const img = sharp(jpg100x100); const result = await sepia(img); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("preserves dimensions", async () => { const img = sharp(png200x150); const result = await sepia(img); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("black stays black", async () => { const blackBuf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#000000" }, }) .png() .toBuffer(); const result = await sepia(sharp(blackBuf)); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); expect(data[0]).toBe(0); expect(data[1]).toBe(0); expect(data[2]).toBe(0); }); }); // --------------------------------------------------------------------------- // invert // --------------------------------------------------------------------------- describe("invert", () => { it("inverts white to black", async () => { const whiteBuf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#ffffff" }, }) .png() .toBuffer(); const result = await invert(sharp(whiteBuf)); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); expect(data[0]).toBe(0); expect(data[1]).toBe(0); expect(data[2]).toBe(0); }); it("inverts black to white", async () => { const blackBuf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#000000" }, }) .png() .toBuffer(); const result = await invert(sharp(blackBuf)); const { data } = await result.raw().toBuffer({ resolveWithObject: true }); expect(data[0]).toBe(255); expect(data[1]).toBe(255); expect(data[2]).toBe(255); }); it("double invert returns to original", async () => { const buf = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#804020" }, }) .png() .toBuffer(); const original = await sharp(buf).raw().toBuffer({ resolveWithObject: true }); const firstInvert = await invert(sharp(buf)); const firstBuf = await firstInvert.png().toBuffer(); const secondInvert = await invert(sharp(firstBuf)); const final = await secondInvert.raw().toBuffer({ resolveWithObject: true }); // Should be back to original values expect(Math.abs(final.data[0] - original.data[0])).toBeLessThanOrEqual(1); expect(Math.abs(final.data[1] - original.data[1])).toBeLessThanOrEqual(1); expect(Math.abs(final.data[2] - original.data[2])).toBeLessThanOrEqual(1); }); it("preserves dimensions", async () => { const img = sharp(png200x150); const result = await invert(img); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); }); // --------------------------------------------------------------------------- // processImage (engine pipeline) // --------------------------------------------------------------------------- describe("processImage", () => { it("applies a single resize operation", async () => { const result = await processImage(png200x150, [ { type: "resize", options: { width: 50, height: 50 } }, ]); expect(result.info.width).toBe(50); expect(result.info.height).toBe(50); expect(result.buffer).toBeInstanceOf(Buffer); expect(result.buffer.length).toBeGreaterThan(0); }); it("chains multiple operations in sequence", async () => { // Use resize then flip then grayscale - operations that have predictable // outcomes even when chained in a lazy pipeline const result = await processImage(png200x150, [ { type: "resize", options: { width: 100, height: 75 } }, { type: "flip", options: { horizontal: true } }, { type: "grayscale", options: {} }, ]); // resize to 100x75, flip preserves dimensions, grayscale preserves dimensions expect(result.info.width).toBe(100); expect(result.info.height).toBe(75); expect(result.buffer.length).toBeGreaterThan(0); }); it("applies crop then flip", async () => { const result = await processImage(png200x150, [ { type: "crop", options: { left: 0, top: 0, width: 100, height: 100 } }, { type: "flip", options: { horizontal: true } }, ]); expect(result.info.width).toBe(100); expect(result.info.height).toBe(100); }); it("converts output format via outputFormat parameter", async () => { const result = await processImage( png200x150, [{ type: "resize", options: { width: 50 } }], "webp", ); expect(result.info.format).toBe("webp"); }); it("processes with no operations (passthrough)", async () => { const result = await processImage(png200x150, []); expect(result.info.width).toBe(200); expect(result.info.height).toBe(150); expect(result.buffer.length).toBeGreaterThan(0); }); it("applies brightness then contrast then saturation", async () => { const result = await processImage(png200x150, [ { type: "brightness", options: { value: 20 } }, { type: "contrast", options: { value: -10 } }, { type: "saturation", options: { value: 30 } }, ]); expect(result.info.width).toBe(200); expect(result.info.height).toBe(150); expect(result.buffer.length).toBeGreaterThan(0); }); it("resize then compress pipeline", async () => { const result = await processImage(png200x150, [ { type: "resize", options: { width: 50, height: 50 } }, { type: "compress", options: { quality: 60, format: "jpg" } }, ]); expect(result.buffer.length).toBeGreaterThan(0); }); it("convert + strip-metadata pipeline", async () => { const result = await processImage(jpg100x100, [ { type: "convert", options: { format: "png" } }, { type: "strip-metadata", options: {} }, ]); expect(result.info.format).toBe("png"); }); it("invert then sepia pipeline", async () => { const result = await processImage(png200x150, [ { type: "invert", options: {} }, { type: "sepia", options: {} }, ]); expect(result.buffer.length).toBeGreaterThan(0); }); it("color-channels operation through processImage", async () => { const result = await processImage(png200x150, [ { type: "color-channels", options: { red: 150, green: 50, blue: 100 } }, ]); expect(result.buffer.length).toBeGreaterThan(0); }); it("all 14 operation types run without error", async () => { // Smaller image for speed; we just verify no crashes const smallBuf = await sharp({ create: { width: 20, height: 20, channels: 3, background: "#808080" }, }) .png() .toBuffer(); // Run each operation individually to isolate failures const ops: Array<{ type: string; options: Record }> = [ { type: "resize", options: { width: 10 } }, { type: "crop", options: { left: 0, top: 0, width: 10, height: 10 } }, { type: "rotate", options: { angle: 90 } }, { type: "flip", options: { horizontal: true } }, { type: "convert", options: { format: "png" } }, { type: "compress", options: { quality: 50, format: "jpg" } }, { type: "strip-metadata", options: {} }, { type: "brightness", options: { value: 10 } }, { type: "contrast", options: { value: 10 } }, { type: "saturation", options: { value: 10 } }, { type: "color-channels", options: { red: 100, green: 100, blue: 100 } }, { type: "grayscale", options: {} }, { type: "sepia", options: {} }, { type: "invert", options: {} }, ]; for (const op of ops) { const result = await processImage(smallBuf, [op]); expect(result.buffer.length).toBeGreaterThan(0); } }); // -- Error cases -- it("throws on unknown operation type", async () => { await expect( processImage(png200x150, [{ type: "nonexistent-op", options: {} }]), ).rejects.toThrow("Unknown operation: nonexistent-op"); }); it("throws on unknown output format", async () => { await expect(processImage(png200x150, [], "bmp" as any)).rejects.toThrow( "Unsupported output format: bmp", ); }); it("propagates operation-level errors through pipeline", async () => { // Crop that exceeds bounds should propagate await expect( processImage(png200x150, [ { type: "crop", options: { left: 0, top: 0, width: 999, height: 999 } }, ]), ).rejects.toThrow("Crop region exceeds"); }); it("error in second operation aborts pipeline", async () => { // First op succeeds (resize to 50x50), second op fails (crop 100x100 from 50x50) // The error may come from our validation or from sharp's internal extract check // depending on whether the pipeline has been materialized await expect( processImage(png200x150, [ { type: "resize", options: { width: 50, height: 50 } }, { type: "crop", options: { left: 0, top: 0, width: 100, height: 100 } }, ]), ).rejects.toThrow(); }); it("invalid buffer input throws", async () => { const garbage = Buffer.from("this is not an image"); await expect(processImage(garbage, [{ type: "grayscale", options: {} }])).rejects.toThrow(); }); it("empty buffer throws", async () => { const empty = Buffer.alloc(0); await expect(processImage(empty, [{ type: "invert", options: {} }])).rejects.toThrow(); }); it("result contains accurate ImageInfo metadata", async () => { const result = await processImage( png200x150, [{ type: "resize", options: { width: 80, height: 60 } }], "png", ); expect(result.info.width).toBe(80); expect(result.info.height).toBe(60); expect(result.info.format).toBe("png"); expect(result.info.size).toBe(result.buffer.length); expect(typeof result.info.channels).toBe("number"); expect(typeof result.info.hasAlpha).toBe("boolean"); }); }); // --------------------------------------------------------------------------- // Shared metadata parsing utilities // --------------------------------------------------------------------------- describe("sanitizeValue", () => { it("converts Date to ISO string", () => { const d = new Date("2026-01-15T10:30:00Z"); expect(sanitizeValue(d)).toBe("2026-01-15T10:30:00.000Z"); }); it("converts small Buffer to number array", () => { const buf = Buffer.from([1, 2, 3]); expect(sanitizeValue(buf)).toEqual([1, 2, 3]); }); it("converts large Buffer to placeholder string", () => { const buf = Buffer.alloc(300, 0); expect(sanitizeValue(buf)).toBe(""); }); it("recursively sanitizes objects", () => { const d = new Date("2026-01-01T00:00:00Z"); const result = sanitizeValue({ nested: { date: d } }); expect(result).toEqual({ nested: { date: "2026-01-01T00:00:00.000Z" } }); }); it("passes through primitives unchanged", () => { expect(sanitizeValue("hello")).toBe("hello"); expect(sanitizeValue(42)).toBe(42); expect(sanitizeValue(null)).toBe(null); expect(sanitizeValue(true)).toBe(true); }); }); describe("parseExif", () => { it("parses EXIF buffer from test fixture", async () => { const metadata = await sharp(jpgWithExif).metadata(); expect(metadata.exif).toBeTruthy(); const result = parseExif(metadata.exif!); expect(result.image.Artist).toBe("Test Artist"); expect(result.image.Copyright).toBe("2026 Test Copyright"); expect(result.image.Software).toBe("SnapOtter Test"); expect(result.image.ImageDescription).toBe("Test Description"); }); it("returns empty sections for empty buffer", () => { const result = parseExif(Buffer.from([])); expect(result.image).toEqual({}); expect(result.gps).toEqual({}); }); }); describe("parseGps", () => { it("parses DMS coordinates to decimal degrees", () => { const result = parseGps({ GPSLatitude: [51, 30, 26.4], GPSLatitudeRef: "N", GPSLongitude: [0, 7, 39.6], GPSLongitudeRef: "W", GPSAltitude: 10, GPSAltitudeRef: 0, }); expect(result.latitude).toBeCloseTo(51.5073, 3); expect(result.longitude).toBeCloseTo(-0.1277, 3); expect(result.altitude).toBe(10); }); it("returns nulls for empty GPS data", () => { const result = parseGps({}); expect(result.latitude).toBeNull(); expect(result.longitude).toBeNull(); expect(result.altitude).toBeNull(); }); it("handles southern hemisphere", () => { const result = parseGps({ GPSLatitude: [33, 51, 54], GPSLatitudeRef: "S", GPSLongitude: [151, 12, 36], GPSLongitudeRef: "E", }); expect(result.latitude).toBeCloseTo(-33.865, 2); expect(result.longitude).toBeCloseTo(151.21, 2); }); }); describe("parseXmp", () => { it("extracts key-value pairs from XMP XML", () => { const xml = Buffer.from( '' + '' + '' + "", ); const result = parseXmp(xml); expect(result["dc:creator"]).toBe("Alice"); expect(result["dc:title"]).toBe("My Photo"); }); it("skips xmlns and rdf namespace prefixes", () => { const xml = Buffer.from( '' + '' + "", ); const result = parseXmp(xml); expect(result["xmlns:x"]).toBeUndefined(); expect(result["xmlns:dc"]).toBeUndefined(); expect(result["rdf:about"]).toBeUndefined(); expect(result["dc:format"]).toBe("image/jpeg"); }); it("returns empty object for empty buffer", () => { const result = parseXmp(Buffer.from("")); expect(result).toEqual({}); }); }); // --------------------------------------------------------------------------- // editMetadata // --------------------------------------------------------------------------- describe("editMetadata", () => { it("writes common fields readable via exif-reader", async () => { const image = sharp(jpgWithExif); const result = await editMetadata(image, { artist: "New Artist", copyright: "New Copyright", }); const buf = await result.jpeg().toBuffer(); const meta = await sharp(buf).metadata(); expect(meta.exif).toBeTruthy(); const parsed = exifReader(meta.exif!); expect(parsed.Image?.Artist).toBe("New Artist"); expect(parsed.Image?.Copyright).toBe("New Copyright"); // Original fields should be preserved via withExifMerge expect(parsed.Image?.Software).toBe("SnapOtter Test"); }); it("clears GPS while preserving other EXIF", async () => { // First write GPS to the image const withGps = sharp(jpgWithExif).withExif({ IFD0: { Artist: "GPS Test" }, IFD3: { GPSLatitudeRef: "N" }, }); const gpsBuf = await withGps.jpeg().toBuffer(); const image = sharp(gpsBuf); const result = await editMetadata(image, { clearGps: true }); const buf = await result.jpeg().toBuffer(); const meta = await sharp(buf).metadata(); const parsed = exifReader(meta.exif!); // GPS should be gone expect(parsed.GPSInfo).toBeUndefined(); // Other EXIF should still be present expect(parsed.Image?.Artist).toBe("GPS Test"); }); it("removes specific fields via fieldsToRemove", async () => { const image = sharp(jpgWithExif); const result = await editMetadata(image, { fieldsToRemove: ["Software"], }); const buf = await result.jpeg().toBuffer(); const meta = await sharp(buf).metadata(); const parsed = exifReader(meta.exif!); expect(parsed.Image?.Software).toBeUndefined(); // Other fields preserved expect(parsed.Image?.Artist).toBe("Test Artist"); }); it("preserves metadata with no options", async () => { const image = sharp(jpgWithExif); const result = await editMetadata(image, {}); const buf = await result.jpeg().toBuffer(); const meta = await sharp(buf).metadata(); expect(meta.exif).toBeTruthy(); const parsed = exifReader(meta.exif!); expect(parsed.Image?.Artist).toBe("Test Artist"); }); it("edit wins over remove for same field", async () => { const image = sharp(jpgWithExif); const result = await editMetadata(image, { artist: "Override Artist", fieldsToRemove: ["Artist"], }); const buf = await result.jpeg().toBuffer(); const meta = await sharp(buf).metadata(); const parsed = exifReader(meta.exif!); expect(parsed.Image?.Artist).toBe("Override Artist"); }); it("writes fresh EXIF to image without existing metadata", async () => { const image = sharp(png1x1); const result = await editMetadata(image, { artist: "Fresh Artist", copyright: "Fresh Copyright", }); const buf = await result.png().toBuffer(); // The operation should not throw expect(buf.length).toBeGreaterThan(0); }); }); // --------------------------------------------------------------------------- // sharpen (basic) // --------------------------------------------------------------------------- describe("sharpen", () => { it("value = 0 returns image unchanged (no-op)", async () => { const img = sharp(png200x150); const result = await sharpen(img, { value: 0 }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("negative value returns image unchanged (no-op)", async () => { const img = sharp(png200x150); const result = await sharpen(img, { value: -5 }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("value = 1 applies minimal sharpening", async () => { const img = sharp(png200x150); const result = await sharpen(img, { value: 1 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("value = 50 applies moderate sharpening", async () => { const img = sharp(png200x150); const result = await sharpen(img, { value: 50 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("value = 100 applies maximum sharpening", async () => { const img = sharp(png200x150); const result = await sharpen(img, { value: 100 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("throws on value > 100", async () => { const img = sharp(png200x150); await expect(sharpen(img, { value: 101 })).rejects.toThrow( "Sharpness value must be between 0 and 100", ); }); it("throws on value = 200", async () => { const img = sharp(png200x150); await expect(sharpen(img, { value: 200 })).rejects.toThrow( "Sharpness value must be between 0 and 100", ); }); it("preserves image dimensions", async () => { const img = sharp(jpg100x100); const result = await sharpen(img, { value: 75 }); const meta = await getMeta(result); expect(meta.width).toBe(100); expect(meta.height).toBe(100); }); it("works on different image formats (webp)", async () => { const img = sharp(webp50x50); const result = await sharpen(img, { value: 30 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("works on a 1x1 pixel image", async () => { const img = sharp(png1x1); const result = await sharpen(img, { value: 50 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("sharpening actually modifies pixel data", async () => { // Create a blurry-ish image with an edge const edgeBuf = await sharp({ create: { width: 20, height: 20, channels: 3, background: "#808080" }, }) .composite([ { input: await sharp({ create: { width: 10, height: 20, channels: 3, background: "#ffffff" }, }) .png() .toBuffer(), left: 10, top: 0, }, ]) .blur(1) .png() .toBuffer(); const original = await sharp(edgeBuf).raw().toBuffer(); const sharpened = await (await sharpen(sharp(edgeBuf), { value: 80 })).raw().toBuffer(); // At least some pixels should differ from the original let diffCount = 0; for (let i = 0; i < original.length; i++) { if (original[i] !== sharpened[i]) diffCount++; } expect(diffCount).toBeGreaterThan(0); }); }); // --------------------------------------------------------------------------- // sharpenAdvanced // --------------------------------------------------------------------------- describe("sharpenAdvanced", () => { it("adaptive method with defaults does not throw", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "adaptive" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("adaptive method with custom params", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "adaptive", sigma: 2.0, m1: 0.5, m2: 5.0, x1: 3.0, y2: 15, y3: 25, }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("unsharp-mask method with defaults does not throw", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "unsharp-mask" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("unsharp-mask method with custom params", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "unsharp-mask", amount: 200, radius: 2.0, threshold: 10, }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("high-pass method with 3x3 kernel (default)", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "high-pass" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("high-pass method with 5x5 kernel", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "high-pass", kernelSize: 5, strength: 75, }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("high-pass method with custom strength", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "high-pass", strength: 25, kernelSize: 3, }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("throws on unknown method", async () => { const img = sharp(png200x150); await expect(sharpenAdvanced(img, { method: "nonexistent" as any })).rejects.toThrow( "Unknown sharpening method: nonexistent", ); }); it("preserves dimensions for all methods", async () => { const methods: Array<"adaptive" | "unsharp-mask" | "high-pass"> = [ "adaptive", "unsharp-mask", "high-pass", ]; for (const method of methods) { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); } }); // -- Denoise pre-pass -- it("denoise=off applies no median filter", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "adaptive", denoise: "off" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("denoise=light applies median(3) pre-pass", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "adaptive", denoise: "light" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("denoise=medium applies median(5) pre-pass", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "unsharp-mask", denoise: "medium" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("denoise=strong applies median(7) pre-pass", async () => { const img = sharp(png200x150); const result = await sharpenAdvanced(img, { method: "high-pass", denoise: "strong" }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("denoise changes pixel data", async () => { const noisyBuf = await sharp({ create: { width: 20, height: 20, channels: 3, background: "#808080" }, }) .png() .toBuffer(); const withoutDenoise = await ( await sharpenAdvanced(sharp(noisyBuf), { method: "adaptive", denoise: "off" }) ) .raw() .toBuffer(); const withDenoise = await ( await sharpenAdvanced(sharp(noisyBuf), { method: "adaptive", denoise: "strong" }) ) .raw() .toBuffer(); // The outputs should differ because of the median filter pre-pass let diffCount = 0; for (let i = 0; i < withoutDenoise.length; i++) { if (withoutDenoise[i] !== withDenoise[i]) diffCount++; } // For a uniform image, median filter may not change much but the sharpen // parameters interact differently, so we allow 0 diffs on uniform images expect(typeof diffCount).toBe("number"); }); it("sharpen through processImage pipeline", async () => { const result = await processImage(png200x150, [{ type: "sharpen", options: { value: 50 } }]); expect(result.buffer.length).toBeGreaterThan(0); expect(result.info.width).toBe(200); expect(result.info.height).toBe(150); }); it("sharpen-advanced through processImage pipeline", async () => { const result = await processImage(png200x150, [ { type: "sharpen-advanced", options: { method: "unsharp-mask", amount: 100 } }, ]); expect(result.buffer.length).toBeGreaterThan(0); expect(result.info.width).toBe(200); expect(result.info.height).toBe(150); }); }); // --------------------------------------------------------------------------- // optimizeForWeb // --------------------------------------------------------------------------- describe("optimizeForWeb", () => { it("converts to webp with quality", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "webp", quality: 80 }); const meta = await getMeta(result); expect(meta.format).toBe("webp"); }); it("converts to jpeg with quality and progressive", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "jpeg", quality: 75, progressive: true, }); const meta = await getMeta(result); expect(meta.format).toBe("jpeg"); }); it("converts to avif with quality", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "avif", quality: 60 }); const meta = await getMeta(result); expect(meta.format).toBe("heif"); }); it("converts to png with palette optimization", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "png", quality: 80 }); const meta = await getMeta(result); expect(meta.format).toBe("png"); }); it("throws on unsupported format", async () => { const img = sharp(png200x150); await expect(optimizeForWeb(img, { format: "bmp" as any, quality: 80 })).rejects.toThrow( "Unsupported format: bmp", ); }); // -- maxWidth / maxHeight -- it("resizes to maxWidth without enlargement", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "webp", quality: 80, maxWidth: 100, }); const meta = await getMeta(result); expect(meta.width).toBeLessThanOrEqual(100); // Height should be proportionally reduced expect(meta.height).toBeLessThanOrEqual(150); }); it("resizes to maxHeight without enlargement", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "webp", quality: 80, maxHeight: 75, }); const meta = await getMeta(result); expect(meta.height).toBeLessThanOrEqual(75); expect(meta.width).toBeLessThanOrEqual(200); }); it("resizes to both maxWidth and maxHeight (fit inside)", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "jpeg", quality: 80, maxWidth: 80, maxHeight: 60, }); const meta = await getMeta(result); expect(meta.width).toBeLessThanOrEqual(80); expect(meta.height).toBeLessThanOrEqual(60); }); it("does not enlarge image when maxWidth > actual width", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "webp", quality: 80, maxWidth: 500, maxHeight: 400, }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); it("without maxWidth/maxHeight preserves original dimensions", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "webp", quality: 80 }); const meta = await getMeta(result); expect(meta.width).toBe(200); expect(meta.height).toBe(150); }); // -- stripMetadata -- it("strips metadata by default", async () => { const img = sharp(jpgWithExif); const result = await optimizeForWeb(img, { format: "jpeg", quality: 80 }); const buf = await result.toBuffer(); const _meta = await sharp(buf).metadata(); // With stripMetadata=true (default), EXIF should be gone or minimal // Sharp strips metadata by default unless withMetadata is called expect(buf.length).toBeGreaterThan(0); }); it("preserves metadata when stripMetadata=false", async () => { const img = sharp(jpgWithExif); const result = await optimizeForWeb(img, { format: "jpeg", quality: 80, stripMetadata: false, }); const buf = await result.toBuffer(); const meta = await sharp(buf).metadata(); // withMetadata() preserves EXIF expect(meta.exif).toBeTruthy(); }); it("stripMetadata=true (explicit) does not include metadata", async () => { const img = sharp(jpgWithExif); const result = await optimizeForWeb(img, { format: "jpeg", quality: 80, stripMetadata: true, }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); // -- progressive -- it("progressive=false for jpeg does not throw", async () => { const img = sharp(png200x150); const result = await optimizeForWeb(img, { format: "jpeg", quality: 80, progressive: false, }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); it("progressive defaults to true", async () => { const img = sharp(png200x150); // Not passing progressive -- should default to true const result = await optimizeForWeb(img, { format: "jpeg", quality: 80 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); // -- Quality impact -- it("lower quality produces smaller file for webp", async () => { const buf = readFixture(fixtures.image.base.jpg100); const bufHigh = await ( await optimizeForWeb(sharp(buf), { format: "webp", quality: 95 }) ).toBuffer(); const bufLow = await ( await optimizeForWeb(sharp(buf), { format: "webp", quality: 10 }) ).toBuffer(); expect(bufLow.length).toBeLessThan(bufHigh.length); }); it("lower quality produces smaller file for jpeg", async () => { const bufHigh = await ( await optimizeForWeb(sharp(png200x150), { format: "jpeg", quality: 95 }) ).toBuffer(); const bufLow = await ( await optimizeForWeb(sharp(png200x150), { format: "jpeg", quality: 10 }) ).toBuffer(); expect(bufLow.length).toBeLessThan(bufHigh.length); }); // -- Different input formats -- it("handles jpg input", async () => { const img = sharp(jpg100x100); const result = await optimizeForWeb(img, { format: "webp", quality: 80 }); const meta = await getMeta(result); expect(meta.format).toBe("webp"); }); it("handles webp input", async () => { const img = sharp(webp50x50); const result = await optimizeForWeb(img, { format: "jpeg", quality: 80 }); const meta = await getMeta(result); expect(meta.format).toBe("jpeg"); }); it("handles 1x1 pixel image", async () => { const img = sharp(png1x1); const result = await optimizeForWeb(img, { format: "webp", quality: 80 }); const buf = await result.toBuffer(); expect(buf.length).toBeGreaterThan(0); }); // -- Combined options -- it("combines maxWidth + stripMetadata=false + progressive", async () => { const img = sharp(jpgWithExif); const result = await optimizeForWeb(img, { format: "jpeg", quality: 70, maxWidth: 50, stripMetadata: false, progressive: true, }); const buf = await result.toBuffer(); const meta = await sharp(buf).metadata(); expect(meta.width).toBeLessThanOrEqual(50); expect(meta.exif).toBeTruthy(); }); });