Recipes
EXIF-correct thumbnails
Photos from phones store rotation in EXIF, not pixels. Call rotate() first so the thumbnail isn't sideways, then resize with a high-quality filter:
import { Transformer, ResizeFilterType } from '@napi-rs/image'
const thumb = await new Transformer(photo)
.rotate() // bake in EXIF orientation
.resize(320, null, ResizeFilterType.Lanczos3) // width 320, keep aspect
.webp(80)
Passing no argument to rotate() uses the embedded EXIF value; pass an Orientation to override it.
Moving a thumbnail pipeline from sharp
For an existing sharp pipeline that reads bytes, applies EXIF orientation, resizes by width and encodes WebP:
import sharp from 'sharp'
const webp = await sharp(input).rotate().resize(320).webp({ quality: 80 }).toBuffer()
The corresponding @napi-rs/image API is:
import { Transformer, ResizeFilterType } from '@napi-rs/image'
const webp = await new Transformer(input).rotate().resize(320, null, ResizeFilterType.Lanczos3).webp(80)
Read input with readFile and write the returned Buffer with writeFile. The encoder is the final async call; there is no .toBuffer() step. This example maps the operation, not byte-for-byte or visual equivalence. Verify output dimensions, orientation, metadata, alpha and quality on representative images before migrating. Equal quality numbers do not imply equal visual quality. Aspect-ratio rounding can also differ: for this repository’s EXIF sample, width 320 produces 320×427 with sharp 0.35.2 and 320×426 with @napi-rs/image 1.15.0.
When supplying both width and height, choose a ResizeFit explicitly. rotate() takes an EXIF Orientation, not an arbitrary angle in degrees. composite() takes one overlay's bytes plus options per call, rather than an array of layers; separable blend modes with translucent inputs can differ from sharp.
Rasterize an SVG
import { Transformer } from '@napi-rs/image'
// background accepts any CSS3 color (including alpha)
const png = await Transformer.fromSvg(svgString, 'rgba(255,255,255,1)').png()
From raw RGBA pixels (e.g. blurhash)
import { Transformer } from '@napi-rs/image'
import { decode } from 'blurhash'
const pixels = decode('LEHV6nWB2yk8pyo0adR*.7kCMdnj', 32, 32) // Uint8ClampedArray
const placeholder = await Transformer.fromRgbaPixels(pixels, 32, 32).webp()
Watermark with overlay
import { Transformer } from '@napi-rs/image'
const watermarked = await new Transformer(base)
.overlay(logoPngBytes, 24, 24) // composite logo at (24, 24)
.jpeg(90)
Batch-optimize a folder (bounded concurrency)
The async methods run off the main thread, so you can process many files at once. Cap concurrency so you don't oversubscribe the thread pool:
import { readdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { losslessCompressPng } from '@napi-rs/image'
async function mapLimit<T>(items: T[], limit: number, fn: (t: T) => Promise<void>) {
const queue = [...items]
await Promise.all(
Array.from({ length: limit }, async () => {
while (queue.length) await fn(queue.shift()!)
}),
)
}
const dir = './images'
const pngs = (await readdir(dir)).filter((f) => f.endsWith('.png'))
await mapLimit(pngs, 8, async (name) => {
const out = await losslessCompressPng(await readFile(join(dir, name)))
await writeFile(join(dir, name), out)
})
Cancel in-flight work with AbortSignal
Every async method accepts a trailing AbortSignal — handy for request timeouts on a server:
const ac = new AbortController()
const t = setTimeout(() => ac.abort(), 2000)
try {
const avif = await new Transformer(input).avif({ quality: 60 }, ac.signal)
return avif
} finally {
clearTimeout(t)
}
Performance tuning
-
Prefer the async methods on servers. They run on libuv's thread pool and keep the event loop free.
-
Measure thread-pool changes under your workload. The default libuv pool is 4 threads. A larger pool allows more concurrent async work but can increase CPU and memory pressure:
bashUV_THREADPOOL_SIZE=10 node server.jsIn the repository’s historical single-JPEG benchmark on an M1 Max, changing the pool from 4 to 10 increased WebP pipeline throughput from 202 to 431 ops/s. This is not a prediction for other images, versions or machines. See the benchmark context.
-
Use
*Syncin CLIs and build scripts where blocking is fine and the per-call overhead of dispatching to the pool isn't worth it. -
Set AVIF
speedexplicitly and compare encode time, size and visual quality. Coordinate codec threads with batch concurrency to avoid oversubscribing the machine.
See the API Reference for full signatures and the Format Guides for quality/size trade-offs.