import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; import sharp from 'sharp'; import { ACCEPTED_MIME_TYPES, UnsupportedImageError, createImagePipeline, } from '../src/images/pipeline.ts'; const pipeline = createImagePipeline(); async function photo(width: number, height: number, format: 'jpeg' | 'png' | 'webp' = 'jpeg') { const base = sharp({ create: { width, height, channels: 3, background: { r: 20, g: 120, b: 170 } }, }); if (format === 'png') return base.png().toBuffer(); if (format === 'webp') return base.webp().toBuffer(); return base.jpeg().toBuffer(); } describe('createImagePipeline', () => { it('produces a Viewing image, a JPEG fallback and a Thumbnail', async () => { const derivatives = await pipeline.process(await photo(3000, 2000), 'IMG_0001.JPG'); assert.equal(derivatives.sourceExt, 'jpg'); assert.match(derivatives.hash, /^[0-9a-f]{12}$/); assert.equal(derivatives.webp.subarray(8, 12).toString(), 'WEBP'); const view = await sharp(derivatives.webp).metadata(); assert.equal(view.width, 1600); assert.equal(view.height, 1067); assert.equal(view.format, 'webp'); assert.equal(derivatives.width, 1600); assert.equal(derivatives.height, 1067); const jpeg = await sharp(derivatives.jpeg).metadata(); assert.equal(jpeg.width, 1600); assert.equal(jpeg.format, 'jpeg'); const thumb = await sharp(derivatives.thumb).metadata(); assert.equal(thumb.width, 480); assert.equal(thumb.format, 'webp'); assert.equal(derivatives.original.length > 0, true); }); it('keeps the Original bytes untouched and hashes them', async () => { const input = await photo(2400, 1600); const derivatives = await pipeline.process(input, 'a.jpg'); assert.equal(Buffer.compare(derivatives.original, input), 0); const again = await pipeline.process(input, 'a.jpg'); assert.equal(again.hash, derivatives.hash); const different = await pipeline.process(await photo(2400, 1600, 'png'), 'b.png'); assert.notEqual(different.hash, derivatives.hash); }); it('accepts PNG and WebP as well as JPEG', async () => { assert.equal((await pipeline.process(await photo(800, 600, 'png'), 'a.png')).sourceExt, 'png'); assert.equal( (await pipeline.process(await photo(800, 600, 'webp'), 'a.webp')).sourceExt, 'webp', ); assert.deepEqual([...ACCEPTED_MIME_TYPES], ['image/jpeg', 'image/png', 'image/webp']); }); it('does not enlarge an image smaller than the Viewing width', async () => { const derivatives = await pipeline.process(await photo(200, 150), 'small.jpg'); assert.equal(derivatives.width, 200); assert.equal(derivatives.height, 150); const thumb = await sharp(derivatives.thumb).metadata(); assert.equal(thumb.width, 200); }); it('applies EXIF orientation so the pixels are upright', async () => { // 1200x600 with orientation 6 (rotate 90 CW) is 600x1200 once upright. const rotated = await sharp({ create: { width: 1200, height: 600, channels: 3, background: { r: 200, g: 30, b: 30 } }, }) .withMetadata({ orientation: 6 }) .jpeg() .toBuffer(); const derivatives = await pipeline.process(rotated, 'rotated.jpg'); assert.equal(derivatives.width, 600); assert.equal(derivatives.height, 1200); const view = await sharp(derivatives.webp).metadata(); assert.equal(view.width, 600); assert.equal(view.height, 1200); assert.equal(view.orientation, undefined); }); it('rejects a file that is not an accepted image, naming the file', async () => { await assert.rejects( () => pipeline.process(Buffer.from('not an image at all'), 'notes.pdf'), (error: unknown) => error instanceof UnsupportedImageError && error.message.includes('notes.pdf'), ); }); it('rejects a GIF, which sharp can read but we do not accept', async () => { const gif = await sharp({ create: { width: 40, height: 40, channels: 3, background: '#fff' }, }) .gif() .toBuffer(); await assert.rejects( () => pipeline.process(gif, 'anim.gif'), (error: unknown) => error instanceof UnsupportedImageError && error.message.includes('gif'), ); }); it('strips metadata from the derivatives but keeps the Original as uploaded', async () => { const tagged = await sharp({ create: { width: 1200, height: 900, channels: 3, background: { r: 10, g: 90, b: 120 } }, }) .withExif({ IFD0: { Copyright: 'Test Kitchen', Artist: 'Nobody' } }) .jpeg() .toBuffer(); assert.notEqual((await sharp(tagged).metadata()).exif, undefined); const derivatives = await pipeline.process(tagged, 'tagged.jpg'); assert.equal((await sharp(derivatives.webp).metadata()).exif, undefined); assert.equal((await sharp(derivatives.jpeg).metadata()).exif, undefined); assert.equal((await sharp(derivatives.thumb).metadata()).exif, undefined); assert.equal(Buffer.compare(derivatives.original, tagged), 0); }); });