feat: add image derivative orchestration
Define thumbnail, medium, and large WebP derivative variants behind an injected renderer and write them through ImageStorage with deterministic keys. Add focused tests and document the remaining worker-backed renderer. Verification: npm test passed 70 tests and integration passed 14 tests. No build script is configured.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
const variants = Object.freeze([
|
||||
{ name: 'thumbnail', maxWidth: 320 },
|
||||
{ name: 'medium', maxWidth: 1280 },
|
||||
{ name: 'large', maxWidth: 2400 },
|
||||
]);
|
||||
|
||||
function derivativeKey(sourceKey, variantName) {
|
||||
const extensionIndex = sourceKey.lastIndexOf('.');
|
||||
const base = extensionIndex === -1 ? sourceKey : sourceKey.slice(0, extensionIndex);
|
||||
return `${base}-${variantName}.webp`;
|
||||
}
|
||||
|
||||
export function createImageDerivativeProcessor({ imageStorage, renderer }) {
|
||||
if (typeof renderer !== 'function') {
|
||||
throw new TypeError('Image derivative renderer is required.');
|
||||
}
|
||||
|
||||
return {
|
||||
async process({ sourceKey, data }) {
|
||||
const results = [];
|
||||
|
||||
for (const variant of variants) {
|
||||
const derivativeData = await renderer({ data, maxWidth: variant.maxWidth });
|
||||
const key = derivativeKey(sourceKey, variant.name);
|
||||
await imageStorage.put({ key, data: derivativeData });
|
||||
results.push({ name: variant.name, key, maxWidth: variant.maxWidth });
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createImageDerivativeProcessor } from '../../src/infrastructure/images/image-derivative-processor.js';
|
||||
|
||||
test('renders and stores thumbnail, medium, and large derivatives in order', async () => {
|
||||
const renderedWidths = [];
|
||||
const stored = [];
|
||||
const processor = createImageDerivativeProcessor({
|
||||
imageStorage: {
|
||||
async put(value) { stored.push(value); },
|
||||
},
|
||||
async renderer({ data, maxWidth }) {
|
||||
renderedWidths.push(maxWidth);
|
||||
return Buffer.concat([data, Buffer.from(`-${maxWidth}`)]);
|
||||
},
|
||||
});
|
||||
|
||||
const results = await processor.process({
|
||||
sourceKey: 'inventory/item/front.jpg',
|
||||
data: Buffer.from('source'),
|
||||
});
|
||||
|
||||
assert.deepEqual(renderedWidths, [320, 1280, 2400]);
|
||||
assert.deepEqual(results.map(({ name, key }) => ({ name, key })), [
|
||||
{ name: 'thumbnail', key: 'inventory/item/front-thumbnail.webp' },
|
||||
{ name: 'medium', key: 'inventory/item/front-medium.webp' },
|
||||
{ name: 'large', key: 'inventory/item/front-large.webp' },
|
||||
]);
|
||||
assert.equal(stored.length, 3);
|
||||
assert.equal(stored[1].data.toString(), 'source-1280');
|
||||
});
|
||||
|
||||
test('requires a renderer implementation', () => {
|
||||
assert.throws(
|
||||
() => createImageDerivativeProcessor({ imageStorage: {} }),
|
||||
/renderer is required/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user