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.
npm install purejsimageThe 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.
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.
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.
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']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.
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.
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.
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.
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
| Fit | Result | Typical use |
|---|---|---|
cover | Fills exact dimensions and center-crops overflow. Default when both dimensions are present. | Cards and hero images |
contain | Fits inside exact dimensions and pads the remaining canvas. | Logos and product images |
fill | Uses the exact dimensions without preserving aspect ratio. | Known geometric assets |
inside | Preserves aspect ratio inside the maximum dimensions. | General upload limits |
outside | Preserves 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.
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
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.
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.
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
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.
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.
- Register only the codecs your endpoint accepts and produces.
- Set input limits to the smallest values the application accepts.
- Prefer downscaling and decoder-aware crops before expensive format conversion.
- Measure absolute peak RSS under realistic concurrent workloads—not only allocation deltas.
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,
},
})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.
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.