Usage guide

Install, register codecs, and execute an image pipeline.

This guide covers Node.js and browser entry points, accepted inputs, transform ordering, output methods, limits, and Lambda-specific storage behavior.

Node.js 22+Modern browsersStrict TypeScript

Install

The Node entry requires Node.js 22 or newer. Modern browser applications use purejsimage/browser. The published package has no runtime dependencies, native addons, external binaries, or required WebAssembly modules.

Terminal
npm install purejsimage

The package is ESM and includes its TypeScript declarations. Import the core API and each codec from its own entry point so your application loads only what it accepts.

Run in a browser

The browser entry uses the same codecs, limits, immutable pipeline, and pixel kernels as Node. Its platform adapter accepts DOM-native files and returns web-native output without Buffer polyfills or Node built-ins.

browser-images.ts
import { createImageLibrary } from 'purejsimage/browser'
import { jpegCodec } from 'purejsimage/codecs/jpeg'
import { pngCodec } from 'purejsimage/codecs/png'

const images = createImageLibrary([jpegCodec, pngCodec])
const image = await images.open(file)

const output = await image
  .autoOrient()
  .resize({ width: 1200, withoutEnlargement: true })
  .jpeg({ quality: 80 })
  .toBlob()

preview.src = URL.createObjectURL(output)

Use File/Blob, ArrayBuffer, Uint8Array, or a custom ImageSource for input. Use toUint8Array(), toBlob(), or toSink() for output. Browser security does not permit arbitrary local path strings, so toFile(path) is Node-only.

Bounded browser transforms. Rotation and row-reordering orientation use origin-private file storage when available. If that is unavailable, a lazy 64 MiB chunked-memory store handles ordinary photos and IndexedDB handles larger transforms without a source-sized heap allocation. Requests fail explicitly when no suitable persistent store is available.
PNG compression. Browser PNG output uses CompressionStream and currently accepts the default compressionLevel: 6 only. Other levels fail with UNSUPPORTED_OPERATION.

Create a library

A library is an immutable registry of codecs. Create it once at module scope and reuse it across requests and warm Lambda invocations.

images.ts
import { createImageLibrary } from 'purejsimage'
import { jpegCodec } from 'purejsimage/codecs/jpeg'
import { pngCodec } from 'purejsimage/codecs/png'
import { webpCodec } from 'purejsimage/codecs/webp'

export const images = createImageLibrary([
  jpegCodec,
  pngCodec,
  webpCodec,
])

console.log(images.formats())
// ['jpeg', 'png', 'webp']
Registration controls capability. Converting PNG to WebP requires both codecs: a decoder for the input and an encoder for the output. A missing codec fails explicitly; nothing is loaded dynamically.

For tools that need every default implementation, import allCodecs from purejsimage/codecs/all. Experimental HEIF/HEIC is deliberately excluded and requires the direct purejsimage/codecs/experimental/heic import described in the API reference.

Open an image

open() detects the registered format from content, not the filename. Node accepts a path, Buffer, Uint8Array, ArrayBuffer, Blob, or custom ImageSource. Browsers accept File/Blob, Uint8Array, ArrayBuffer, or a custom source.

inspect.ts
const image = await images.open('photo.jpg')
const metadata = await image.metadata()

console.log({
  width: metadata.width,
  height: metadata.height,
  format: metadata.format,
  orientation: metadata.orientation,
})

Metadata inspection is designed to avoid decoding all pixels. When called on a transformed pipeline, it reports the planned output dimensions and format.

Borrowed memory. Buffer, Uint8Array, and ArrayBuffer inputs are not copied. Do not mutate or detach them until every pipeline derived from the image has finished.

Remote or custom sources

A custom source exposes its byte length and a range-read function. Each in-range read must return exactly min(length, size - offset) bytes as a Uint8Array, or reject if the backing read fails. Returned bytes only need to remain valid until the next read begins.

range-source.ts
import type { ImageSource } from 'purejsimage'

const source: ImageSource = {
  size: contentLength,
  async read(offset, length) {
    return fetchRange(offset, length)
  },
}

const image = await images.open(source)

File, Blob, and custom sources use up to four aligned 256 KiB cache regions. This bounds the shared cache at 1 MiB while coalescing small reads. Contract violations and rejected custom reads surface as ImageError results.

Build a pipeline

Image pipelines are immutable and lazy. Each method returns a new Image; decoding begins only at toBuffer() or toFile(). You can safely branch from a common source.

variants.ts
const source = (await images.open(upload)).autoOrient()

const hero = source
  .resize({ width: 1600, height: 900, fit: 'cover' })
  .jpeg({ quality: 82 })

const thumb = source
  .resize({ width: 320, height: 320, fit: 'cover' })
  .webp({ quality: 76 })

const heroBytes = await hero.toBuffer()
const thumbBytes = await thumb.toBuffer()

Resize behavior

FitResultTypical use
coverFills exact dimensions and center-crops overflow. Default when both dimensions are present.Cards and hero images
containFits inside exact dimensions and pads the remaining canvas.Logos and product images
fillUses the exact dimensions without preserving aspect ratio.Known geometric assets
insidePreserves aspect ratio inside the maximum dimensions.General upload limits
outsidePreserves aspect ratio while meeting both minimum dimensions.Minimum-size masters

Resize uses lanczos3 by default to suppress downscale aliasing. Choose bilinear for a faster lower-quality path or nearest to preserve hard pixel edges. Set withoutEnlargement: true when small uploads should remain at their original size.

Contain options. position and background are available only with fit: 'contain', and fit options require both width and height.

Transform order

Spatial operations execute in call order. A crop after a resize uses resized coordinates, and a second resize works from the first resize result. rotate() uses clockwise degrees, flip() mirrors top-to-bottom, and flop() mirrors left-to-right.

Recipes

Normalize an upload to JPEG

normalize.ts
const output = await (await images.open(upload))
  .autoOrient()
  .resize({ width: 2048, fit: 'inside', withoutEnlargement: true })
  .jpeg({ quality: 80, background: '#ffffff' })
  .toBuffer()

Retain EXIF and ICC metadata

Metadata is stripped by default. Opt in to EXIF and ICC independently. Reorienting pixels also normalizes the retained EXIF orientation so downstream viewers do not rotate them a second time.

preserve-metadata.ts
await (await images.open('camera.jpg'))
  .keepExif()
  .keepIcc()
  .autoOrient()
  .resize({ width: 1600, withoutEnlargement: true })
  .jpeg({ quality: 82 })
  .toFile('camera-web.jpg')

JPEG, PNG, and WebP support retained EXIF and compatible ICC profiles. TIFF supports compatible ICC profiles but not retained EXIF. Unsupported combinations fail explicitly.

Convert a Windows icon to PNG

The ICO decoder selects the largest entry, then prefers useful bit depth and alpha. It decodes only that selected PNG- or DIB-backed image.

favicon.ts
import { createImageLibrary } from 'purejsimage'
import { icoCodec } from 'purejsimage/codecs/ico'
import { pngCodec } from 'purejsimage/codecs/png'

const icons = createImageLibrary([icoCodec, pngCodec])

await (await icons.open('favicon.ico'))
  .resize({ width: 64, height: 64, fit: 'contain' })
  .png({ compressionLevel: 7 })
  .toFile('favicon.png')

Create a transparent contained PNG

contain.ts
await (await images.open('logo.webp'))
  .resize({
    width: 800,
    height: 800,
    fit: 'contain',
    background: 'transparent',
  })
  .png({ compressionLevel: 7 })
  .toFile('logo.png')

Compose ordered transforms

Use coordinates in the image state at that point in the pipeline. Here the crop uses resized coordinates, then the cropped image is resized again and rotated clockwise onto an opaque canvas.

crop.ts
await (await images.open('camera.jpg'))
  .autoOrient()
  .resize({ width: 1600 })
  .crop({ x: 200, y: 150, width: 1200, height: 900 })
  .resize({ width: 600, kernel: 'lanczos3' })
  .rotate(12, { background: '#ffffffff' })
  .flop()
  .jpeg({ quality: 80 })
  .toFile('transformed.jpg')

Deploy on Lambda

Keep the library at module scope. The immutable registry and small coefficient caches can be reused on warm invocations.

  1. Register only the codecs your endpoint accepts and produces.
  2. Set input limits to the smallest values the application accepts.
  3. Prefer downscaling and decoder-aware crops before expensive format conversion.
  4. Measure absolute peak RSS under realistic concurrent workloads—not only allocation deltas.
Size for CPU as well as memory. A 256 MiB Lambda completed all four measured 4000 × 3000 resize/conversion workflows with 121–156 MiB maximum use. But JPEG → WebP warm operation time fell from 10,601 ms at 256 MiB to 5,261 ms at 512 MiB and 2,533 ms at 1024 MiB while maximum use stayed at 120–122 MiB. For latency-sensitive endpoints, start at 1024 MiB even if the process consumes only about 150 MiB; choose 256 MiB only when its lower CPU allocation and roughly 10-second latency are acceptable. Re-measure with your images and concurrency. See the Lambda results.
handler.ts
const image = await images.open(body, {
  limits: {
    maxInputBytes: 15 * 1024 * 1024,
    maxWidth: 12_000,
    maxHeight: 12_000,
    maxPixels: 60_000_000,
    maxDecodedBytes: 240_000_000,
  },
})
Orientation, rotation, and temporary storage. EXIF orientations 3–8 and arbitrary-angle rotations use a bounded-memory 32 × 32 tile spool under os.tmpdir(). Plan for about one decoded frame of temporary disk capacity. A 100-megapixel RGBA image needs roughly 400 MB; on Lambda, that consumes configured /tmp storage.

The temporary directory is removed on success or failure. Capacity errors such as ENOSPC become ImageError with code LIMIT_EXCEEDED.

Handle failures

PureJsImage uses five stable error categories. Check the code to decide whether to reject an upload, tighten limits, or report a capability boundary.

errors.ts
import { ImageError } from 'purejsimage'

try {
  return await pipeline.toBuffer()
} catch (error) {
  if (error instanceof ImageError) {
    console.warn(error.code, error.message)
  }
  throw error
}

See the error reference for the meaning of each category.