sharp responsive image variants with BlurHash placeholders: working versions (tested 2026-10-15)
A sharp pipeline that applies EXIF rotation, strips metadata and renders WebP widths without upscaling, plus BlurHash encode from raw pixels and decode to a tiny PNG placeholder, tested on synthetic images generated in-test.
passed5 passed · 0 failed2026-10-15 15:03 UTC- Status
- passed
- Last passed
- 2026-10-15 15:03 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
sharp@0.35.5blurhash@2.0.5tsx@4.23.15@types/node@26.6.5
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38755782594
The code
Exactly the files that passed. Your agent can pull them with get_recipe("sharp-blurhash-image-pipeline") (install), or see them on GitHub.
package.json
{
"name": "sharp-blurhash-image-pipeline",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"sharp": "0.35.5",
"blurhash": "2.0.5"
},
"devDependencies": {
"tsx": "4.23.15",
"@types/node": "26.6.5"
}
}
src/pipeline.ts
import sharp from "sharp";
import { encode, decode, isBlurhashValid } from "blurhash";
export type Variant = { width: number; format: "webp" | "avif" | "jpeg"; buffer: Buffer; bytes: number; height: number };
/** Rotates by EXIF, strips metadata, and renders responsive widths without upscaling. */
export async function makeVariants(input: Buffer, widths = [320, 640, 1280], format: Variant["format"] = "webp"): Promise<Variant[]> {
const base = sharp(input).rotate(); // apply EXIF orientation, then metadata is dropped by default
const meta = await sharp(input).metadata();
// metadata() reports stored dimensions; orientations 5 to 8 swap width and height once rotated.
const uprightWidth = (meta.orientation ?? 1) >= 5 ? meta.height : meta.width;
const out: Variant[] = [];
for (const w of widths) {
if (uprightWidth && w > uprightWidth) continue;
const { data, info } = await base.clone().resize({ width: w, withoutEnlargement: true }).toFormat(format, { quality: 70 }).toBuffer({ resolveWithObject: true });
out.push({ width: info.width, height: info.height, format, buffer: data, bytes: info.size });
}
return out;
}
/** BlurHash placeholder: downscale to 32px wide, feed raw RGBA pixels to blurhash.encode. */
export async function blurhashOf(input: Buffer, componentsX = 4, componentsY = 3): Promise<string> {
const { data, info } = await sharp(input).rotate().resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
return encode(new Uint8ClampedArray(data), info.width, info.height, componentsX, componentsY);
}
/** Decodes a BlurHash back into a tiny PNG, e.g. to inline as a data URL. */
export async function placeholderPng(hash: string, width = 16, height = 12): Promise<Buffer> {
if (!isBlurhashValid(hash).result) throw new Error("invalid blurhash");
const pixels = decode(hash, width, height);
return sharp(Buffer.from(pixels), { raw: { width, height, channels: 4 } }).png().toBuffer();
}
test/pipeline.test.ts
import { test, before } from "node:test";
import assert from "node:assert/strict";
import sharp from "sharp";
import { makeVariants, blurhashOf, placeholderPng } from "../src/pipeline.ts";
let photo: Buffer;
before(async () => {
// Synthetic 1000x600 JPEG: red left half, blue right half, tagged with EXIF orientation 6 (rotate 90).
const left = await sharp({ create: { width: 500, height: 600, channels: 3, background: "#d02020" } }).png().toBuffer();
photo = await sharp({ create: { width: 1000, height: 600, channels: 3, background: "#2040d0" } })
.composite([{ input: left, left: 0, top: 0 }])
.jpeg()
.withMetadata({ orientation: 6 })
.toBuffer();
});
test("fixture really carries EXIF orientation", async () => {
const m = await sharp(photo).metadata();
assert.equal(m.orientation, 6);
assert.equal(m.width, 1000);
});
test("variants are auto-rotated, never upscaled, and stripped of EXIF", async () => {
const v = await makeVariants(photo, [320, 640, 1280]);
// upright size is 600x1000, so 1280 and 640 are skipped rather than upscaled
assert.deepEqual(v.map((x) => [x.width, x.height]), [[320, 533]]);
const all = await makeVariants(photo, [200, 600]);
assert.deepEqual(all.map((x) => [x.width, x.height]), [[200, 333], [600, 1000]]);
for (const x of all) assert.equal((await sharp(x.buffer).metadata()).exif, undefined);
});
test("rotated output is portrait and a webp", async () => {
const [small] = await makeVariants(photo, [300]);
const m = await sharp(small.buffer).metadata();
assert.equal(m.format, "webp");
assert.equal(m.width, 300);
assert.equal(m.height, 500);
assert.equal(m.orientation, undefined);
});
test("blurhash is stable and decodes back to a placeholder PNG", async () => {
const a = await blurhashOf(photo);
const b = await blurhashOf(photo);
assert.equal(a, b);
assert.equal(a.length, 4 + 2 * 4 * 3 - 2 + 2); // 1 size + 1 max AC + 4 DC + 2 per AC component
const png = await placeholderPng(a);
const m = await sharp(png).metadata();
assert.deepEqual([m.format, m.width, m.height], ["png", 16, 12]);
});
test("placeholder colours follow the source: top is red, bottom is blue after rotation", async () => {
const png = await placeholderPng(await blurhashOf(photo), 8, 8);
const { data } = await sharp(png).raw().toBuffer({ resolveWithObject: true });
const px = (x: number, y: number) => { const i = (y * 8 + x) * 4; return { r: data[i], b: data[i + 2] }; };
assert.ok(px(4, 0).r > px(4, 0).b, "top should be reddish");
assert.ok(px(4, 7).b > px(4, 7).r, "bottom should be bluish");
await assert.rejects(placeholderPng("not-a-hash"), /invalid blurhash/);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.