shutterbox
Image processing pipeline for Next.js.
Complements next/image with processing capabilities: resize, crop, format conversion, responsive sets, and blur placeholders. Built for AI coding agent ergonomics with a fluent, serializable pipeline API.
Install
npm install shutterbox sharp
sharp is a peer dependency used for actual image processing. The pipeline builder and types work without it.
Quick Start
import { createDarkroom } from "shutterbox";
const darkroom = createDarkroom();
const pipe = darkroom.pipeline()
.resize({ width: 800 })
.format("webp")
.quality(80)
.toConfig();
const result = await darkroom.process("./photo.jpg", pipe);
Pipeline API
The fluent builder produces a serializable array of transforms. No sharp dependency at definition time.
import { pipeline } from "shutterbox";
const config = pipeline()
.resize({ width: 1200, height: 630, fit: "cover" })
.crop({ top: 0, left: 0, width: 1200, height: 630 })
.format("avif")
.quality(75)
.blur(2)
.toConfig();
console.log(JSON.stringify(config));
Available Transforms
.resize({ width?, height?, fit? }) | Resize. Fit: cover, contain, fill, inside, outside |
.crop({ top, left, width, height }) | Extract a region |
.format(fmt) | Convert to webp, avif, jpeg, or png |
.quality(n) | Set quality 1-100 |
.blur(sigma) | Gaussian blur |
Variants
Pre-define named transform sets in your config:
const darkroom = createDarkroom({
variants: {
thumbnail: {
name: "thumbnail",
transforms: [
{ type: "resize", width: 200, height: 200, fit: "cover" },
{ type: "format", format: "webp" },
{ type: "quality", quality: 75 },
],
},
hero: {
name: "hero",
transforms: [
{ type: "resize", width: 1920 },
{ type: "format", format: "avif" },
{ type: "quality", quality: 80 },
],
},
},
});
const pipe = darkroom.variant("thumbnail");
const result = await darkroom.process("./photo.jpg", pipe);
Responsive Images
Generate multiple sizes and formats for <picture> elements:
const responsive = await darkroom.responsive("./hero.jpg", {
breakpoints: [640, 1024, 1536],
formats: ["webp", "jpeg"],
quality: 80,
urlFor: (width, format) => `/images/hero-${width}.${format}`,
});
Blur Placeholders
Create tiny blurred placeholder images for progressive loading:
const placeholder = await darkroom.placeholder("./photo.jpg");
React Components
import { Picture, BlurImage, DarkroomProvider, useDarkroom } from "shutterbox/react";
<DarkroomProvider darkroom={darkroom}>
<App />
</DarkroomProvider>
<Picture src={responsiveSet} alt="Hero image" />
<BlurImage
src="/images/photo.jpg"
placeholder={blurDataUrl}
alt="Photo"
transitionMs={500}
/>
Next.js Image Handler
Create an API route that processes images on-the-fly:
import { createImageHandler } from "shutterbox/next";
import { createDarkroom } from "shutterbox";
const darkroom = createDarkroom();
export const GET = createImageHandler(darkroom, {
maxWidth: 3840,
defaultQuality: 80,
});
Then use query parameters:
/api/image?src=./public/photo.jpg&w=800&f=webp&q=80
Build-Time Optimization
Process all images in a directory during your build:
import { optimizeStaticImages } from "shutterbox/next";
await optimizeStaticImages("./public/images", {
formats: ["webp", "avif"],
widths: [640, 1024, 1536],
quality: 80,
outDir: "./public/images/optimized",
});
License
MIT