ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.