npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

imagepipe

v0.6.0

Published

Zero-dependency, TypeScript-first, in-browser image editing. Small, composable, tree-shakeable.

Readme

imagepipe

Zero-dependency, TypeScript-first image editing for the browser. Small, composable, tree-shakeable.

CI npm license types

imagepipe is the Lodash of image editing: a library, not an app. Chainable, lazy, immutable pipelines over Canvas 2D — with transparent WebGL2 acceleration where available — that UI tools can be built on top of.

Status: pre-release, feature-complete for the initial release. CPU path, WebGL2 acceleration (verified against the CPU reference on real WebGL2 in CI), and worker offloading are implemented; the same pipeline renders identically everywhere — acceleration only changes speed.

Renamed: this package was previously published as tinctjs (≤ 0.2.0). Same library, same API surface modulo the entry names: tinctimagepipe, TinctImageImagePipe.

Installation

npm install imagepipe

Quick start

import { imagepipe } from 'imagepipe'
import { grayscale, blur } from 'imagepipe/filters'

const image = await imagepipe.load(fileOrUrlOrImageData)

const result = await image
  .crop({ aspect: '16:9', gravity: 'center' })
  .resize({ width: 1280 })
  .rotate(90)
  .adjust({ brightness: 0.1, contrast: 0.05, saturation: -0.2 })
  .apply(grayscale())
  .apply(blur({ radius: 4 }))
  .toBlob({ format: 'webp', quality: 0.85 })

Nothing renders until you ask for output. Every operation returns a new immutable instance, so undo/redo is just keeping references:

const original = await imagepipe.load(file)
const step1 = original.crop({ aspect: '1:1' })
const step2 = step1.apply(grayscale())
// "undo" = use step1 again; original is untouched

Core examples

// Custom filters: a CPU kernel, optionally accelerated by a WebGL2 shader
import { defineFilter } from 'imagepipe'

const sepia = defineFilter({
  name: 'sepia',
  fragment: sepiaShader, // optional GLSL ES 3.00
  fallback: sepiaCpu, // required: (pixels: ImageData, options) => ImageData | void
})

// Introspection and serialization of the edit pipeline
const ops = edited.history() // JSON-safe, storable, diffable
const replayed = image.pipe(ops) // replay on any image

// Progress events for large images
image.on('progress', ({ pct }) => {
  progressBar.value = pct
})

API

| API | Description | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | imagepipe.load(source, options?) | Load a File, Blob, URL, ImageData, <img>, <canvas>, or OffscreenCanvas. EXIF orientation is applied automatically; all other metadata (EXIF/GPS) is stripped on export — a privacy feature when handling user uploads | | imagepipe.capabilities() | Feature detection: { webgl2, offscreenCanvas, workers } | | .crop(options) | Pixel/percent region, or aspect ratio + gravity (incl. content-aware 'face') | | .resize(options) | High-quality resampling (Lanczos multi-step downscale) | | .rotate(angle, options?) | 90° increments lossless; arbitrary angles expand the canvas | | .flip(axis) | 'horizontal' or 'vertical' | | .adjust(options) | brightness, contrast, saturation, exposure (-1..1), hue (deg), gamma | | .overlay(pixels, options?) | Watermark/logo compositing with gravity, margin, opacity | | .apply(filter) | Apply a built-in or custom filter | | .history() | Serialize the pipeline to JSON-safe ops | | .pipe(ops) | Replay serialized ops | | .on(event, listener) | progress events during rendering | | .toBlob(options?) | Encode to PNG / JPEG / WebP; maxBytes targets a file size, signal cancels | | .toDataURL(options?) | Encode to a data URL | | .toImageData() | Raw pixels | | .toCanvas() | Render into a canvas |

Built-in filters (imagepipe/filters): grayscale, sepia, invert, blur, sharpen, pixelate, vignette, duotone, noise, posterize, curves (serializable tone curves — presets as JSON), and median (denoise).

Companion modules, each ~1.5 kB and tree-shaken when unused: imagepipe/face (content-aware cropping), imagepipe/hash (ThumbHash placeholders), imagepipe/palette (dominant colors), imagepipe/layers (multi-image documents).

Layers

imagepipe/layers turns imagepipe from a one-image pipeline into a document model — collage makers, meme tools, thumbnail builders, template renderers. Every layer's content is a full pipeline, so filters, adjustments, and geometry work per layer for free:

import { document, layer } from 'imagepipe/layers'

const doc = document({ width: 1080, height: 1350, background: '#ffffff' })
  .add(layer(photo))
  .add(
    layer(sticker.resize({ width: 300 }))
      .at(650, 80)
      .opacity(0.9)
      .blend('multiply')
      .name('sticker'),
  )

const dragged = doc.move('sticker', { dx: 20, dy: -10 }) // new document
const blob = await dragged.flatten().toBlob({ format: 'webp' })

| API | Description | | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | document({ width, height, background }) | Fixed canvas, ordered layer stack | | layer(image) | Wrap a pipeline; .at(), .opacity(), .blend(), .visible(), .name() read with no argument and derive with one | | .add .insert .remove .update .move .reorder | All return a new document, addressing layers by name or index | | .boundsOf(ref) | The layer's rectangle — arithmetic only, safe in a drag loop | | .layerAt(x, y) | Top-most alpha-aware hit test: clicking a transparent hole selects what's behind it | | .flatten() | Composite to a ImagePipe — chain, export, or reuse it as a layer | | .toJSON() / fromJSON(data) | Versioned envelope with a shared sources table |

Blend modes: source-over, multiply, screen, darken, lighten.

Documents are immutable and share structure, so undo/redo is keeping references — and layer pixels are cached per pipeline, so dragging, reordering, or changing a layer's opacity re-runs only the composite pass, never the layers below. UIs own the mouse; imagepipe owns the model and the math.

Batch processing

import { batch } from 'imagepipe/batch'

const results = await batch(files, { concurrency: 4 })
  .pipe(storedRecipe) // any serialized history — or .map((img) => img.resize(...))
  .toBlobs({ format: 'webp', maxBytes: 300_000 })

for (const r of results) {
  if (r.ok) upload(r.value)
  else console.warn(`item ${r.index} failed:`, r.error)
}

One recipe over many images with bounded concurrency, per-item failure isolation (a broken file never sinks the batch), aggregate progress events, and whole-batch cancellation via signal. Ships as its own tree-shaken entry (imagepipe/batch, ~1 kB).

Live pipelines

import { live } from 'imagepipe/live'

const session = live(video) // an HTMLVideoElement (camera, playback) or canvas
  .pipe(storedRecipe) // the same serialized history your photo editor saved
  .into(canvas) // starts rendering, per frame, in real time

session.update(otherRecipe) // hot-swap the look mid-stream
session.stats // { mode: 'gpu', fps: 60, frames: 1834 }
session.stop()

The recipe that edits a photo, batches an upload, or styles a preset runs on live video unchanged. Rendering is texture-resident: each frame is uploaded once, the recipe's fragment shaders run through ping-pong framebuffers, and the last pass draws straight to the visible canvas — no pixel readback, which is what makes 60 fps possible. Machines without WebGL2 fall back to the same CPU kernels at a lower frame rate with identical output, verified by the cross-path parity suite.

Live recipes are color-only: adjust plus any filter that ships a fragment shader (grayscale, sepia, duotone, blur, vignette, …). Geometry ops throw a descriptive error up front — at pipe(), not per frame.

Tracked face effects

import { live } from 'imagepipe/live'
import { trackFace } from 'imagepipe/track'
import { warp, sticker, stickerSource } from 'imagepipe/effects'

const session = live(video).into(canvas)
const crown = stickerSource(crownPixels)

trackFace(session, myLandmarkProvider, (face) => {
  if (!face) return { version: 1, ops: [] }
  const bigEyes = warp({
    zones: [face.leftEye, face.rightEye].map((eye) => ({
      center: eye,
      radius: face.box[2] * 0.18,
      strength: 0.35,
    })),
  })
  const hat = sticker({
    source: crown,
    at: [face.box[0] + face.box[2] / 2, face.box[1]],
    width: face.box[2] * 1.2,
  })
  return {
    version: 1,
    ops: [bigEyes, hat].map((f) => ({
      op: 'filter' as const,
      params: { name: f.name, options: f.options },
    })),
  }
})

Three pieces, cleanly split: imagepipe/effects renders (warp — radial bulge/pinch zones; sticker — anchored, rotated compositing), imagepipe/track runs the loop (fixed-cadence detection, exponential smoothing, hot-swapping the rebuilt recipe into the session), and a LandmarkProvider supplies the anchors. imagepipe ships no landmark model — detection quality is a model problem, rendering speed is ours. Wrap MediaPipe Face Landmarker, TensorFlow.js, or your own heuristic in a one-function provider and the library does the rest.

Because tracked recipes always carry concrete coordinates, they stay pure JSON: snapshot a tracked frame with history() and it replays pixel-exact anywhere, no model required.

Face-aware cropping

import { enableFaceGravity } from 'imagepipe/face'

enableFaceGravity() // once at startup; ships ~1 kB, tree-shaken if unused

const avatar = await image
  .crop({ aspect: '1:1', gravity: 'face' })
  .resize({ width: 256 })
  .toBlob({ format: 'webp' })

Detection is deterministic and platform-agnostic — a skin-region heuristic with a saliency fallback, no FaceDetector API and no model downloads — so the same input produces the same crop in every browser and serialized histories replay exactly. It is an honest heuristic, not ML: great for portraits and avatars; unusual lighting or stylized art falls back to salient-region framing.

One consequence: the face stage reads skin color, so on grayscale images it degrades to salient-subject framing (measured on our eval set: focal accuracy 9/10 color → 3/10 grayscale, though every produced crop still contained a face). If your chain desaturates, crop with 'face' before applying grayscale() — the crop sees the pixels at its position in the pipeline.

Examples

Complete little apps built on the published package, live from this repo:

  • Avatar Studio — face-aware crops, look presets, size-budgeted WebP export
  • Lookbook — film presets as pure JSON, replayed with pipe()
  • Shrinkwrap — batch upload optimizer with ThumbHash placeholders and dominant colors
  • Collage — a layered document editor: drag to place with layerAt(), per-layer filters and blending, undo as an array of references

Source in examples/, each runnable with npm install && npm run dev. The interactive playground exposes every operation.

Tree-shaking

Every filter is individually importable. If you only use crop and resize, you ship only the core; if you import grayscale, you ship only the grayscale kernel:

import { imagepipe } from 'imagepipe' // core only
import { grayscale } from 'imagepipe/filters' // + grayscale kernel, nothing else

The package is ESM-only with sideEffects: false and per-filter modules, so any modern bundler prunes the rest.

Browser support

Evergreen browsers (Chrome, Edge, Firefox, Safari 16.4+). Baseline execution uses Canvas 2D + typed arrays; WebGL2 (color adjustments and per-pixel filters), Web Workers (heavy pipelines ≥ 512×512 without custom filters), and OffscreenCanvas are detected at runtime and used automatically, falling back gracefully. No polyfills are required or bundled.

Indicative CPU-path timings on a 2000×1500 image (Node 24, M-series): Lanczos resize to 800px ≈ 50 ms, full adjust ≈ 60 ms, three color filters ≈ 25 ms, gaussian blur (r=4) ≈ 310 ms. GPU/worker paths reduce main-thread cost for exactly these heavy cases.

Bundle size

Enforced budgets in CI via size-limit:

| Import | Budget | Measured (brotli) | | ------------------------------------- | ------------ | ----------------- | | Core (imagepipe + defineFilter) | ≤ 10 kB gzip | ~8.6 kB | | Each individual filter | ≤ 2 kB gzip | 0.44–0.52 kB | | All filters together | ≤ 10 kB | ~3 kB | | imagepipe/face, /hash, /palette | ≤ 2 kB each | 1.2–1.6 kB | | imagepipe/layers (own code) | ≤ 4 kB | ~1.6 kB | | imagepipe/live | ≤ 5 kB | ~3.9 kB |

imagepipe/layers is measured as a delta: the size-limit config carries the core baseline it builds on and the layers entry at baseline + 4 kB, so the budget tracks the layers code rather than the engine underneath it.

Core includes the full CPU engine, the WebGL2 renderer, the worker client, the incremental render cache, cancellation, and the overlay compositor. The original 8 kB budget was raised to 10 kB in v0.2 when the cache, overlay, and white-balance landed — measured honestly rather than split into micro-entry-points nobody would import separately. The worker is a separate lazily-loaded artifact.

Re-measure any time with npm run size; CI fails if a budget is exceeded.

When to use imagepipe

| | imagepipe | Fabric.js / Konva | Jimp | sharp | | ---------------------------------------- | --------- | ----------------- | ---------- | --------- | | Runs in browser | ✅ | ✅ | ⚠️ (heavy) | ❌ (Node) | | Zero dependencies | ✅ | ❌ | ❌ | ❌ | | Tree-shakeable ops | ✅ | ❌ | ❌ | — | | Immutable/serializable pipeline | ✅ | ❌ | ❌ | ❌ | | Canvas scene graph / interactive objects | ❌ | ✅ | ❌ | ❌ | | Server-side batch processing | ❌ | ❌ | ✅ | ✅ |

Use imagepipe when you need programmatic image editing in the browser — crop, resize, adjust, filter, export — especially as the engine under your own UI. Use a scene-graph library for interactive canvas apps, and sharp for servers.

Roadmap

  • v0.1 — everything above: geometry, adjustments, filters, serialization, CPU + WebGL2, workers.
  • v0.3imagepipe/layers: multi-image documents, CPU compositing, blend modes, hit testing, versioned document serialization.
  • Later: GPU compositing and affine layer placement, text, drawing/brushes, AI-assisted features, more codecs (AVIF), plugin ecosystem.

Contributing

See CONTRIBUTING.md. Architecture notes live in docs/architecture.md.

Credits

imagepipe's design owes a lot of its ideas and learnings to Javed Ahmed and Junaid Qadir.

License

MIT