rastermill
Rastermill is a Node 22+ image-processing library for byte buffers. It can inspect image headers, decode pixels to check real transparency, resize or crop, convert to JPEG/PNG/WebP, choose an output format based on alpha, and search quality or dimensions to meet raw or base64 byte limits. Common formats run through Photon in-process; automatic mode can fall back to sips, ImageMagick, GraphicsMagick, ffmpeg, or a Windows-native path for codecs such as HEIC and AVIF. It includes pixel-budget guards and structured errors aimed at server and agent workloads.
Promising for guarded server-side transcoding and size-constrained agent payloads, with unusually clear backend and failure policies. Its Node 22 floor, external-codec variability, metadata loss, and young 0.x API make Sharp the safer general-purpose default.
Use it if
- You process untrusted image uploads and want header-first input and output pixel budgets before a decoder allocates the full image
- You need one API that can run common JPEG/PNG/GIF/WebP work in-process and fall back to installed tools for HEIC or AVIF
- Your output must fit a model, messaging, or storage limit expressed as raw bytes or base64 payload bytes
- You want automatic JPEG-versus-PNG/WebP selection based on actual transparent pixels rather than filename or MIME guesses
- Your deployment runs Node 20, a browser, an edge isolate, or another runtime below Node 22; version 0.3.1 declares Node 22 or newer and accepts Node byte types
- You require HEIC/AVIF conversion or quality-controlled WebP in a sandbox with no child processes; Photon cannot provide those paths and internal mode throws an unavailable error
- You must retain EXIF, GPS, ICC, or XMP through resizing or conversion; the README says real transforms strip metadata because Photon cannot read, copy, or write it
- Your pipeline is stream- or path-oriented; the public ImageInput type accepts only Buffer, Uint8Array, or ArrayBuffer, so you must read files and collect streams yourself
- You need a settled API for a long-lived platform: the package began in May 2026, remains on 0.3.x, and 0.3.0 already removed the documented encodeWithinBytes export in favor of encode options
Setup reality
Rastermill is ESM-only and requires Node 22+. npm installs a pinned Photon Node dependency for in-process WASM processing, with no peer dependency or application config file. The zero-configuration functions lazily create an auto-mode instance with 25,000,000-pixel input and output budgets. That convenience can hide deployment differences: auto mode may spawn sips on macOS, PowerShell/System.Drawing on Windows, or ImageMagick, GraphicsMagick, and ffmpeg when present. HEIC/AVIF decoding needs one of those external paths, and WebP quality also requires an external encoder because Photon exposes only fixed-quality WebP. Use execution: "internal" when child processes are forbidden, accepting the codec gaps; use external when you need deterministic native-tool behavior. External work writes temporary files, looks up commands on PATH unless commandResolver overrides it, captures up to 1 MiB of process output by default, and applies a 20-second per-command timeout. Choose a private temp root in multi-tenant deployments and verify the runtime user can write it. Inputs must already be Buffer, Uint8Array, or ArrayBuffer, not file paths or streams. probe is lenient and returns null for unknown or over-budget headers; transparency and encode throw. Transforms strip metadata by default, and metadata: "preserve" only works when the original bytes can be returned unchanged. A byte-budget search does not guarantee success: inspect withinBudget because Rastermill returns its smallest candidate with false when no candidate fits. Finally, inputPixels protects decode allocation, while per-encode dimension limits and the configured outputPixels budget protect the target. Set both for your threat model instead of blindly accepting the defaults.
Patterns
Create a processor with explicit safety boundariesconfigure-processor
import {createRastermill} from 'rastermill';
const images = createRastermill({
execution: 'internal',
limits: {
inputPixels: 20_000_000,
outputPixels: 12_000_000
},
timeoutMs: 15_000,
maxProcessBufferBytes: 512 * 1024
});Internal mode forbids child processes, which also rules out HEIC/AVIF decoding and quality-controlled WebP. Use auto only when external execution is acceptable.
Read header metadata without decoding pixelsprobe-image
import {probe} from 'rastermill';
const info = await probe(buffer);
if (!info) {
throw new Error('Unknown, malformed, or over-budget image');
}
console.log(info.format, info.width, info.height, info.orientation);probe returns null instead of throwing for unknown dimensions, undecodable headers, or an input over the configured pixel budget. hasAlpha can also be null.
Distinguish an alpha channel from transparent pixelsinspect-transparency
const alpha = await images.transparency(buffer);
if (alpha.hasTransparentPixels) {
console.log('Choose an alpha-preserving output');
}An opaque RGBA PNG has an alpha channel but no transparent pixels. transparency decodes pixels, never spawns external tools, and cannot inspect HEIC/AVIF.
Fit an image inside a bounding boxresize-inside
const out = await images.encode(buffer, {
format: 'jpeg',
quality: 85,
resize: {width: 1600, height: 1200, fit: 'inside'}
});
await writeFile('output.jpg', out.data);inside preserves aspect ratio and does not enlarge by default. Set enlarge: true only when upscaling is intentional.
Center-crop a square thumbnailcrop-thumbnail
const thumb = await images.encode(buffer, {
format: 'png',
compressionLevel: 9,
resize: {width: 512, height: 512, fit: 'cover'}
});cover scales and center-crops to the target. Use fill only when stretching the image to exact dimensions is acceptable.
Convert HEIC or AVIF bytes to JPEGconvert-heic
const images = createRastermill({execution: 'auto'});
const jpeg = await images.encode(heicBuffer, {
format: 'jpeg',
quality: 85
});Photon cannot decode HEIC/AVIF. Auto mode needs sips, ImageMagick, GraphicsMagick, ffmpeg, or a suitable platform codec; otherwise this throws RastermillUnavailableError.
Choose outputs based on transparencychoose-format-automatically
const out = await images.encode(buffer, {
format: 'auto',
opaque: {format: 'jpeg', quality: 82},
transparent: {format: 'png', compressionLevel: 9},
transparency: 'preserve',
resize: {maxSide: 1800}
});
console.log(out.chosen.format, out.chosen.transparency);preserve refuses to flatten alpha, but under a byte budget it can still return the smallest transparent candidate with withinBudget false.
Search dimensions and quality for a raw byte limitfit-byte-budget
const out = await images.encode(buffer, {
format: 'jpeg',
maxBytes: 500_000,
search: {
maxSide: [1600, 1280, 1024, 800],
quality: [85, 75, 65]
}
});
if (!out.withinBudget) throw new Error(`Smallest result is ${out.bytes} bytes`);A maxBytes request is a search policy, not a guarantee. Rastermill returns its smallest candidate with withinBudget false when none fit.
Budget for a base64 API payloadfit-base64-budget
const out = await images.encode(buffer, {
format: 'auto',
maxBase64Bytes: 4_500_000,
limits: {maxWidth: 2000, maxHeight: 2000},
opaque: {format: 'jpeg', quality: 80},
transparency: 'flatten'
});
if (!out.withinBudget) throw new Error('Payload still exceeds the limit');
const base64 = out.data.toString('base64');Use maxBase64Bytes for APIs that limit encoded payload length. The result reports both bytes and base64Bytes, so do not estimate the expansion yourself.
Fit output dimensions while keeping decode limitsenforce-dimension-limits
const out = await images.encode(buffer, {
format: 'auto',
limits: {
maxWidth: 4096,
maxHeight: 4096,
maxPixels: 12_000_000
}
});Per-call limits fit output dimensions. They do not replace the instance inputPixels guard, which rejects oversized decoded input before resizing.
Cancel work with AbortSignalcancel-encoding
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
return await images.encode(buffer, {
format: 'jpeg',
resize: {maxSide: 2000},
signal: controller.signal
});
} finally {
clearTimeout(timer);
}The instance timeout applies per external tool invocation; AbortSignal is the caller-controlled cancellation path for the overall encode request.
Separate policy failures from missing backendshandle-errors
import {isRastermillError, isRastermillUnavailableError} from 'rastermill';
try {
return await images.encode(buffer, {format: 'jpeg'});
} catch (error) {
if (isRastermillUnavailableError(error)) {
return {status: 415, message: 'No installed backend supports this image'};
}
if (isRastermillError(error) && error.code === 'RASTERMILL_INPUT_TOO_LARGE') {
return {status: 413, message: 'Image dimensions exceed the limit'};
}
throw error;
}Do not turn every decode failure into backend unavailability. Malformed input, timeouts, option errors, and process-buffer overflow are intentionally surfaced separately.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| sharp | npm | You want a mature libvips-based pipeline with streams, files, broader transforms, and extensive production history |
| jimp | npm | You prefer a JavaScript-focused image API and can accept lower throughput and a different codec set |
| gm | npm | ImageMagick or GraphicsMagick is already an explicit system dependency and you want direct access to its operations |
| @napi-rs/canvas | npm | Your main job is canvas drawing, text, and composition rather than policy-driven transcoding and byte budgets |