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

lithocinch

v3.1.0

Published

Fast, batch image optimizer built on sharp + svgo. Caps dimensions, normalizes color to sRGB, compresses JPEG/PNG/GIF/WebP/AVIF/TIFF and SVG. Usable as a CLI, a library, or an AWS Lambda.

Downloads

38

Readme

Lithocinch

Perceptually-lossless, best-format image optimization — measured, not guessed. Lithographic quality, cinched.

Most optimizers ship a fixed quality: 82 and hope. Lithocinch measures: it binary-searches the encoder quality per image against a structural-similarity (SSIM) target — landing on the smallest file that’s still perceptually identical to the source — then runs a format shootout (AVIF / WebP / JPEG / PNG) and ships the smallest one the client can actually display.

Results

Real output, verified end-to-end through a Craft CMS site writing to Google Cloud Storage:

| Source | Optimized (800px) | Reduction | |---|---|---| | 1,644 KB PNG | 13 KB AVIF | −99.2%, perceptually lossless |

RESEARCH.md documents why each codec behaves as it does, with measured numbers — the methodology, not marketing.

One engine, four runtimes: CLI · library · AWS Lambda · Craft CMS plugin.


Fast, batch image optimizer built on sharp and svgo. Point it at a folder and it will, for every image in the tree:

  • cap dimensions to a max width/height (default 2560px),
  • normalize color to sRGB (CMYK and oddly-profiled images included),
  • compress JPEG with an adaptive per-file quality search (the jpeg-recompress approach — picks the smallest quality that still looks right by SSIM), plus PNG (libimagequant palette), GIF, WebP, AVIF, TIFF, and SVG,
  • convert real TIFFs — including phone/WhatsApp files mislabeled as .jpg — to JPEG,
  • mirror the folder structure into an output directory, copying any non-image files (PDFs, docs…) straight through,
  • and route anything corrupt to an error folder instead of crashing.

It runs three ways from one codebase: a CLI, an importable library, and an AWS Lambda (S3-triggered). No ImageMagick, no global binaries — sharp ships prebuilt libvips, which is also Lambda-compatible.

This is the 3.x rewrite of a long-lived personal tool (process-images-2.9.js). See NOTES.md for the architecture, the full old-variable → flag mapping, and behavioral differences from 2.x.

Requirements

  • Node.js ≥ 20

Install

# global CLI
npm install -g lithocinch

# or per-project / as a library
npm install lithocinch

Or from a clone:

git clone <repo> lithocinch && cd lithocinch
npm install
npm link        # optional: puts `lithocinch` on your PATH

CLI usage

lithocinch [input-dir] [options]

Run it from (or point it at) the folder of images. By default it writes to a sibling ../img-optimize-output/ folder and skips files already optimized there.

# optimize the current folder with defaults
lithocinch

# a specific folder, custom output, force everything to WebP at 1600px
lithocinch ./photos -o ./web --format webp --max-size 1600

# square-pad product shots onto a white canvas, higher JPEG quality
lithocinch ./products --square --bg-color "#ffffff" --jpeg-quality 90

# see what would happen without writing anything
lithocinch ./photos --dry-run --verbose

Common options

| Flag | Default | Description | |---|---|---| | [input] | . | Input directory (recursive) | | -o, --output <dir> | ../img-optimize-output/ | Output directory | | -s, --suffix <str> | _lco | Inserted before each file's extension | | -m, --max-size <px> | 2560 | Cap width & height (0 / --no-resize to disable) | | -f, --format <fmt> | keep | jpeg/png/webp/avif/gif/tiff, or auto (smallest per image) | | --manifest <path> | — | JSON source → output map; enables --format auto skip-detection | | --webp-mode <m> | auto | auto (classify + shoot out), lossy, lossless, near-lossless | | --emit <formats> | — | Also write sibling formats (e.g. webp,avif) for <picture> | | --square | off | Pad to a max-size square using --bg-color | | --jpeg-target <ssim> | 0.999 | Adaptive JPEG SSIM target (0.999 = jpeg-recompress "low") | | --jpeg-min / --jpeg-max | 30 / 60 | Adaptive JPEG quality search band | | --jpeg-mode <mode> | recompress | recompress (adaptive) or fixed | | --jpeg-quality <n> | 80 | Fixed JPEG quality (only with --jpeg-mode fixed) | | --png-quality <n> | 100 | PNG palette quality search ceiling (libimagequant) | | --png-min-ssim <n> | 0.92 | Palette must clear this SSIM, else fall back to lossless | | --png-engine <e> | auto | auto (shootout) / sharp (libimagequant) / pngquant (binary) | | --gif-engine <e> | auto | auto (lossless gifsicle pass when available) / sharp / gifsicle | | --overwrite | off | Reprocess even if output already exists | | --flatten | off | Write output flat instead of mirroring subfolders | | --no-copy-non-images | — | Don't copy non-image files through | | -c, --concurrency <n> | 16 | Images processed in parallel | | --dry-run | off | Plan only; write nothing | | --json | off | Print summary as JSON |

Run lithocinch --help for the complete list (color, trim, metadata, AVIF, SVG, error-dir, etc.). Every option also exists as a library option key — see the mapping table in NOTES.md.

Legacy suffix (_opt). As of 3.1.0 the default output suffix is _lco (“lithocinch optimized”), replacing the generic _opt that carried over from the original process-images script. To restore the old behavior — e.g. for a tree already built with photo_opt.* names — pass -s _opt (or --suffix _opt), or set suffix: '_opt' when calling the library. -s '' drops the suffix entirely (photo.jpg → photo.avif).

Auto-pick the best format (--format auto)

Let the tool choose the smallest format for each image — it encodes the viable candidates and ships the winner, changing the extension to match:

lithocinch ./images --format auto --manifest ./out/manifest.json
photographic.png  1.27 MB  ->  4.3 KB   .avif   (no longer a bloated PNG)
logo.png          11 KB    ->  64 B     .webp   (lossless)
photo.jpg         700 KB   ->  7.4 KB   .avif
icon-with-alpha   ...      ->  .webp            (alpha preserved; JPEG excluded)
  • Photographic PNGs stop being PNGs — they convert to AVIF/WebP automatically.
  • Tie-break favors compatibility: when two formats are within --format-tolerance (default 3%), the more universally-supported one wins, so a trivial AVIF edge doesn't rewrite every URL to .avif.
  • Restrict the pool for conservative targets: --format-pool webp,jpeg,png drops AVIF.
  • The manifest matters. Because extensions change, --manifest records source → output, format, bytes for every file — feed it to your build/HTML rewrite, and re-runs use it to skip already-done files. Pair with --emit to also write guaranteed fallbacks.

See RESEARCH.md §9 for the candidate-pool and tie-break rules.

Emit <picture> fallbacks alongside originals

--emit writes extra formats next to each optimized image (keeping the primary), so you get a modern-format set without changing the original:

lithocinch ./photos --emit webp,avif
#  photo.jpg  ->  photo_lco.jpg   (primary, optimized)
#               + photo_lco.webp  (content-aware)
#               + photo_lco.avif  (content-aware)

Each sibling uses that codec's content-aware/perceptual settings. The primary format is never duplicated, SVGs are skipped, and re-runs fill in only missing siblings — so adding --emit avif to an already-processed tree just backfills the AVIFs. Pair with <picture>:

<picture>
  <source srcset="photo_lco.avif" type="image/avif">
  <source srcset="photo_lco.webp" type="image/webp">
  <img src="photo_lco.jpg" alt="">
</picture>

Transform service (CMS plugin / Lambda backbone)

Besides batch-optimizing folders, the tool exposes a single-image transform API for on-demand use (a CMS image transform, a build step, an upload hook). The same runTransform() core is reachable three ways — pick per host:

| Transport | Command | Use | |---|---|---| | CLI (stdin→stdout JSON) | lithocinch transform | shell out from PHP/anything; needs Node on the box | | HTTP daemon | lithocinch serve | persistent, warm sharp; POST from localhost | | AWS Lambda | lambda/transform.js | hosts that can't run Node; same logic, Function URL |

Bytes in, bytes out — stateless and storage-agnostic. The caller reads the source (from GCS, S3, local, a Craft Fs…) and writes the results; the service never touches storage. So it works with any backend, including the craftcms/google-cloud filesystem, with no integration.

Request / response contract

// request (image carried as base64 in the `image` field)
{
  "image": "<base64>",
  "ops":    { "width": 800, "height": 600, "fit": "cover",
              "focal": { "x": 0.3, "y": 0.6 }, "maxSize": 2560, "bg": "#fff" },
  "format": "auto",                       // or jpeg|png|webp|avif|keep
  "batch":  [ { "ops": { "width": 800 }, "format": "avif" },   // one source ->
              { "ops": { "width": 800 }, "format": "webp" },   // many outputs
              { "ops": { "width": 800 }, "format": "auto" } ], // (<picture>)
  "defaults": { "perceptualTarget": 0.99, "formatPool": ["avif","webp","jpeg"] }
}
// response
{ "ok": true, "outputs": [
  { "format": "avif", "width": 800, "height": 533, "bytes": 1914,
    "ssim": 0.997, "engine": "avif-lossy", "data": "<base64>" }
]}

fit maps to the usual CMS modes: cover (crop), inside/contain (fit), fill (stretch). focal is {x,y} in 0..1 for a focal-point crop.

Run it

# one-shot, from a file (testing)
lithocinch transform --in photo.jpg --ops '{"width":800}' --format auto --out out.avif

# one-shot, JSON on stdin (the shell-out contract)
echo '{"image":"...","ops":{"width":800},"format":"auto"}' | lithocinch transform

# persistent daemon (warm), optional bearer token
lithocinch serve --port 4111 --token "$PI_TOKEN"
curl -s localhost:4111/transform -H "authorization: Bearer $PI_TOKEN" \
  -d '{"image":"...","ops":{"width":800},"format":"auto"}'

The Lambda (lithocinch/lambda/transform) takes the identical JSON body via a Function URL / API Gateway v2 event and honors PI_TOKEN.

Library usage

import {
  optimizeImageBuffer, // raster bytes -> optimized bytes (pure)
  optimizeSvg,         // svg string   -> optimized string (pure)
  optimizeBytes,       // any image bytes + filename -> { data, outName, ... } (pure)
  optimizeFile,        // one file on disk -> optimized file
  processDirectory,    // whole tree -> mirrored output tree
} from 'lithocinch';

// In-memory, framework-agnostic:
const { data, info } = await optimizeImageBuffer(inputBuffer, {
  maxSize: 1600,
  format: 'webp',
  webpQuality: 82,
});

// Drive a whole folder with progress callbacks:
const stats = await processDirectory(
  { input: './photos', output: './out', maxSize: 2048 },
  { onProgress: (e) => e.type === 'done' && console.log(e.file) }
);
console.log(stats); // { processed, skipped, errors, copied, bytesIn, bytesOut, ... }

AWS Lambda

lambda/handler.js is a ready-to-adapt S3 object-created handler: it reads the uploaded image, optimizes it in memory with the same core, and writes the result to a destination bucket — no /tmp scratch files.

SOURCE_BUCKET ──S3 event──▶ Lambda ──optimizeBytes()──▶ DEST_BUCKET/optimized/...

Environment variables: DEST_BUCKET (required), DEST_PREFIX (default optimized/), plus optional MAX_SIZE, FORMAT, JPEG_QUALITY, … .

Packaging: install sharp for the Lambda platform before zipping (or use a layer):

# arm64 (Graviton) functions
npm install --os=linux --cpu=arm64 sharp
# x86_64 functions
npm install --os=linux --cpu=x64 sharp

The AWS SDK v3 (@aws-sdk/client-s3) is already in the Lambda Node 18+ runtime, so it is not a dependency of this package.

How it handles color (the old "tmp" problem)

Earlier versions kept a temp directory to fix CMYK JPEGs: decode → color- transform to RGB → re-encode to a temp file → re-optimize. That's gone. sharp reads any embedded ICC profile and converts to sRGB in memory in a single pipeline, so CMYK and mis-profiled images come out correct with no scratch files — which is also what makes the Lambda path clean. Use --keep-metadata to re-embed an sRGB ICC profile + EXIF; the default strips metadata for size.

Adaptive JPEG compression (the important part)

JPEG quality is not a fixed number. By default the tool reproduces the jpeg-recompress strategy this project has relied on for years: for each image it binary-searches JPEG quality within a band (default 30–60) and keeps the smallest encoding whose SSIM vs the source still clears a perceptual target (default 0.999 — jpeg-recompress's "low" preset). A flat graphic gets crushed to the floor; a detailed photo settles higher — each file gets its own sweet spot instead of a one-size-fits-all quality.

# loosen or tighten the perceptual target
lithocinch ./photos --jpeg-target 0.997
# widen the quality band
lithocinch ./photos --jpeg-min 35 --jpeg-max 75
# see the chosen quality + SSIM per file
lithocinch ./photos --verbose         # ... scene.jpg -> scene.jpg (685KB -> 30KB q58 ssim0.9951)

The built-in engine (sharp-ssim) needs no native binary, so it runs in Lambda too. For byte-exact parity with the original jpeg-recompress binary:

brew install jpeg-archive   # provides the `jpeg-recompress` binary
lithocinch ./photos --jpeg-engine jpeg-recompress

Its SSIM is the standard Wang metric and is close to — not identical to — JPEG-Archive's; the 30–60 band keeps results tight either way. --jpeg-mode fixed --jpeg-quality 80 switches to a plain single-quality encode. With the real jpeg-recompress on PATH, the default --jpeg-engine auto shoots it out against the built-in and keeps the smaller. See NOTES.md §1e.

WebP (content-aware)

--format webp converts the tree to WebP, and --webp-mode auto (default) picks lossy vs lossless per image by measuring both — because the right mode depends on the content, not the source extension:

| source | → webp | chosen mode | |---|---|---| | photo / JPEG | much smaller | lossy (quality-searched to an SSIM target) | | flat graphic / logo / screenshot | tiny, exact | lossless | | photographic PNG | much smaller | lossy | | animated GIF | smaller, frames kept | animated lossy WebP |

lithocinch ./photos --format webp                 # auto per image
lithocinch ./ui --format webp --webp-mode lossless # force lossless
lithocinch ./photos --format webp --webp-target 0.985  # looser = smaller

A photographic source is detected from the JPEG marker or a high distinct-colour count; graphics fall to lossless (crisp, no ringing). --verbose shows the mode each file got (... via webp-lossy / webp-lossless). See NOTES.md §1h.

AVIF works the same way (--format avif, --avif-mode auto) and is ~3× smaller than WebP on photographs (a 701 KB JPEG → 7.4 KB AVIF vs 24 KB WebP), at the cost of slower encoding. Both share one perceptually-lossless knob, --perceptual-target (SSIM, default 0.99) — raise toward 0.999 for stricter (larger) output, lower for smaller. JPEG keeps its own validated 0.999-in-band.

JPEG XL is intentionally not supported: it isn't in the prebuilt sharp/libvips and Chrome removed it from browsers — so it can't be delivered to most users or run on a bare Lambda. AVIF is the modern format here. Rationale + all per-codec measurements are in RESEARCH.md.

PNG, GIF, and SVG

  • PNG uses sharp's palette: true (libimagequant — the exact library pngquant wraps) at max effort. It runs a perceptual palette search: it keeps the smallest palette that's still perceptually lossless (--png-min-ssim, default 0.92, full-res SSIM), and falls back to lossless for gradients/photos that can't be palettized cleanly. --png-engine pngquant shells out to the real binary for byte-exact 2.9 parity; --png-lossless forces lossless.
  • GIF uses sharp (libimagequant) for pixels, then a lossless gifsicle -O3 pass. --gif-engine auto (default) runs gifsicle when it's installed — best for animations — and falls back to sharp otherwise.
  • Animated GIF/WebP are handled too: --format auto runs a WebP-vs-GIF shootout (SSIM measured across all frames) and keeps the smallest — a 45 KB animated GIF becomes a ~1 KB animated WebP, frames preserved.
  • SVG uses svgo v4 (preset-default + multipass, viewBox preserved).

Opinionated auto engines

By default PNG and GIF use auto: the tool uses the best encoder actually available and keeps the tightest result that still passes the quality bar. With pngquant/gifsicle installed, tiny PNGs and animated GIFs get the specialist tools automatically; without them it falls back to the portable built-ins. --verbose shows which engine won each file (... via pngquant). Pin --png-engine sharp / --gif-engine sharp for byte-stable portable output.

See NOTES.md §1f for the head-to-head audit (sizes + SSIM) against the actual pngquant/gifsicle binaries.

Development

npm test        # node --test (unit tests for the pure core)
npm start -- --help

License

Apache-2.0 © 2026 Danniel T. Gaidula