mrkeyoor.com_
Sat 08 Aug 21:57 UTC
npmUtilsupdated 08 Aug 2026

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.

Verdict

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.

API stability2/5The current public shape is compact and well typed around createRastermill, probe, transparency, and encode, with stable string codes promised for owned errors. The project is still below 1.0 and only began publishing in May 2026. Its 0.3.0 changelog records a real migration within days of launch: encodeWithinBytes was removed and folded into encode with maxBytes. New 0.3.1 fields then added maxBase64Bytes and base64Bytes. Those changes are coherent improvements, but they show that callers should pin versions and expect more contract refinement before treating the API as settled.
Docs5/5The package has focused pages for probing, transparency, encoding, backend selection, configuration, and error handling, plus complete exported TypeScript declarations. The docs state default pixel budgets, command order by platform and output format, timeout and process-buffer limits, metadata loss, alpha policy, no-op behavior, resize geometry, byte-search semantics, and every structured error code. They also say when a method returns null, when it throws, and when a smallest result can still miss its requested budget. One no-op metadata passage is less clear than the default strip contract, but the types and main API pages provide enough detail to build and test a production policy.
Maintenance5/5Version 0.3.1 was published on May 30, 2026 and the repository was pushed on July 27. The release added base64 budgets, expanded malformed-header, orientation, fallback, and external-error tests, raised the runtime floor deliberately, and added an installed-tarball import smoke gate. Project scripts run TypeScript checks, linting, formatting, documentation consistency, Vitest coverage with an 80 percent threshold, builds, and package smoke tests before publication. GitHub reports one open issue or pull request, though the small 36-star project remains dependent on a young maintainer community.
Ecosystem3/5Rastermill records 3,550,262 downloads for the measured week despite being only a few months old, and it builds on Photon's portable image engine while interoperating with established native tools on macOS, Windows, and Linux. Its codec story benefits from ImageMagick, GraphicsMagick, ffmpeg, and system decoders without forcing one native dependency for every install. The direct ecosystem is still small at 36 GitHub stars, there are no plugins or framework adapters, and behavior for HEIC, AVIF, and WebP quality depends on executables and codec delegates present on each host.

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
Skip it if

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

PackageRegistryPick it when
sharpnpmYou want a mature libvips-based pipeline with streams, files, broader transforms, and extensive production history
jimpnpmYou prefer a JavaScript-focused image API and can accept lower throughput and a different codec set
gmnpmImageMagick or GraphicsMagick is already an explicit system dependency and you want direct access to its operations
@napi-rs/canvasnpmYour main job is canvas drawing, text, and composition rather than policy-driven transcoding and byte budgets