From d964b4f0acc6019ad8a74b577dd0e08683265b5f Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Tue, 8 Sep 2026 17:49:39 -0400 Subject: [PATCH] feat: add sharp-backed image rendering --- .../images/sharp-image-renderer.js | 21 +++++++++++++++++++ .../image-derivative-processor.test.js | 14 +++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 apps/api/src/infrastructure/images/sharp-image-renderer.js diff --git a/apps/api/src/infrastructure/images/sharp-image-renderer.js b/apps/api/src/infrastructure/images/sharp-image-renderer.js new file mode 100644 index 0000000..159ddeb --- /dev/null +++ b/apps/api/src/infrastructure/images/sharp-image-renderer.js @@ -0,0 +1,21 @@ +import sharp from 'sharp'; + +export function createSharpImageRenderer() { + return async function renderDerivative({ data, maxWidth }) { + if (!Buffer.isBuffer(data)) { + throw new TypeError('Image data must be provided as a Buffer.'); + } + + if (!Number.isInteger(maxWidth) || maxWidth <= 0) { + throw new TypeError('maxWidth must be a positive integer.'); + } + + const image = sharp(data); + const metadata = await image.metadata(); + + const targetWidth = metadata.width && metadata.width > maxWidth ? maxWidth : metadata.width ?? maxWidth; + const resized = targetWidth === metadata.width ? image : image.resize({ width: targetWidth, withoutEnlargement: true }); + + return resized.webp().toBuffer(); + }; +} diff --git a/apps/api/tests/application/image-derivative-processor.test.js b/apps/api/tests/application/image-derivative-processor.test.js index bfa5eaa..9288590 100644 --- a/apps/api/tests/application/image-derivative-processor.test.js +++ b/apps/api/tests/application/image-derivative-processor.test.js @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { createImageDerivativeProcessor } from '../../src/infrastructure/images/image-derivative-processor.js'; +import { createSharpImageRenderer } from '../../src/infrastructure/images/sharp-image-renderer.js'; test('renders and stores thumbnail, medium, and large derivatives in order', async () => { const renderedWidths = []; @@ -36,4 +37,17 @@ test('requires a renderer implementation', () => { () => createImageDerivativeProcessor({ imageStorage: {} }), /renderer is required/, ); +}); + +test('creates a sharp-backed WebP renderer that resizes to the requested maximum width', async () => { + const renderer = createSharpImageRenderer(); + const data = Buffer.from( + '', + ); + const output = await renderer({ data, maxWidth: 320 }); + + assert.ok(output instanceof Buffer); + assert.ok(output.length > 0); + assert.equal(output.subarray(0, 4).toString('ascii'), 'RIFF'); + assert.equal(output.subarray(8, 12).toString('ascii'), 'WEBP'); }); \ No newline at end of file