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

pixelpress

v0.3.0

Published

Bulk image compression and transcoding to AVIF, WebP, JPEG, and PNG — globs, responsive widths, transforms, animated WebP, size budgets, cache, watch, HTML reports, and picture snippets.

Readme

pixelpress

Bulk image compression and transcoding. Point it at a file, a folder, or a glob; get AVIF, WebP, JPEG, or PNG at responsive widths with sensible quality, a cache, a manifest, an HTML report, and ready-to-paste <picture> snippets.

npx pixelpress "public/images/**/*.{jpg,png}" -o dist/images \
  -f avif,webp -w 480,960,1920 --hash --manifest dist/images.json
pixelpress 42 files → /app/dist/images (avif,webp)
  ✓ hero.jpg              1.4 MB → 182.3 KB  87% smaller
  ✓ team/ana.png          420.1 KB → 61.8 KB  85% smaller
  ✓ products/chair.webp   118.4 KB → 44.2 KB  63% smaller

42 files · 126 outputs · 18.2 MB → 3.1 MB (83% smaller) · 4.2s

Node.js 22.5+. One runtime dependency: sharp.

Install

npm install --save-dev pixelpress
# or use it without installing
npx pixelpress --help

Quick start

# One image, three next-gen sizes
pixelpress hero.jpg -o dist -w 480,960,1920

# A whole tree, recursively, keeping folder structure
pixelpress public/images -o dist/images -r

# Every JPEG/PNG below assets/, but skip drafts
pixelpress "assets/**/*.{jpg,png}" -o dist/assets \
  --include "products/**" --exclude "*-draft.*"

# Fit a hard budget per variant, hash filenames, cache the results
pixelpress photos -o dist -r --max-bytes 120kb --hash --cache

# Rebuild on change
pixelpress photos -o dist -r --watch

# Generate markup that points at the output
pixelpress photos -o dist -r -w 480,960,1600 \
  --picture dist/picture.html --public-path /images

# A visual before/after report
pixelpress photos -o dist -r --report dist/report.html

# One image through a pipe
cat photo.jpg | pixelpress - -f webp --stdout > photo.webp

Output mirrors the input by default:

public/images/                 dist/images/
  hero.jpg                       hero.avif
  team/ana.png                   hero.webp
                                 team/ana.avif
                                 team/ana.webp

With --widths, the rendered width is appended: hero-480.avif, hero-960.avif. Widths larger than the source are dropped unless --enlarge is set.

Why pixelpress?

It started as a rewrite of SudiptoImgTranscode — same job, none of the sharp edges: any input format, recursive and glob inputs, per-format quality, responsive widths, a real API, visible failures, alpha handling, EXIF orientation, and tests.

It now borrows the best ideas from the tools people already use: sharp-cli (transforms, globs), imagemin-cli (batch output), Squoosh CLI (per-codec options, suffix, size optimization), vite-imagetools (srcset), and @11ty/eleventy-img (responsive output sets). See COMPARISON.md for the matrix and what was adopted.

CLI reference

Input and bulk

| Option | Default | Description | | --- | --- | --- | | <file\|dir\|glob...> | required | Files, directories, or quoted globs. | | -r, --recursive | off | Include subdirectories of directory inputs. | | --include <glob> | all | Only process matching files; repeatable. | | --exclude <glob> | none | Skip matching files; repeatable. | | --follow-symlinks | off | Follow symlinked files and directories. | | -c, --config <file> | auto | JSON or ESM config. Auto-loads pixelpress.config.json if present. |

Globs are expanded by Node, not the shell, so quote them: "public/**/*.{jpg,png}". Include/exclude patterns match the path relative to the input root and the basename, so --include "*.jpg" catches nested JPEGs too. Patterns are matched with Node's glob semantics (*, **, ?, [...], {a,b}).

With multiple directory/glob roots, outputs are prefixed by each root's basename to avoid collisions. --flat drops the tree and uses basenames (collisions are reported, never silently overwritten).

Output and naming

| Option | Default | Description | | --- | --- | --- | | -o, --out <dir> | pixelpress-out | Output directory. | | -f, --formats <list> | avif,webp | Any of avif,webp,jpeg,png (or jpg). | | -w, --widths <list> | source size | Responsive widths, e.g. 480,960,1920. | | --width <px> | none | Single resize width (not with --widths). | | --height <px> | none | Single resize height (not with --widths). | | --fit <mode> | inside | cover, contain, fill, inside, outside. | | --position <pos> | center | Crop gravity, e.g. attention, entropy, right top. | | --enlarge | off | Allow upscaling. | | --prefix <text> | none | Prepend to filenames. | | --suffix <text> | none | Append to filenames. | | --hash | off | Append an 8-character SHA-256 content hash. | | --flat | off | Flatten the output tree. | | --clean | off | Delete the output directory before writing. | | --preserve-dates | off | Copy source timestamps onto outputs. |

Name order: <prefix><name><suffix>[-<width>][-<hash>].<ext>. For example pre-hero-2x-960-a1b2c3d4.avif. --flat, --prefix, and --suffix never escape the output directory.

Transforms

| Option | Description | | --- | --- | | --rotate <deg> | Rotate by degrees. This overrides EXIF auto-orientation. | | --no-auto-orient | Ignore EXIF orientation (on by default). | | --flip / --flop | Flip vertically / horizontally. | | --grayscale | Convert to grayscale. | | --blur <sigma> | Gaussian blur. | | --sharpen <sigma> | Sharpen; 1 is a good starting point. | | --normalize | Stretch contrast across the full range. | | --gamma <value> | Gamma correction. | | --tint <color> | Tint while preserving luminance. | | --trim <threshold> | Trim uniform borders (1-100). | | --extract <WxH+X+Y> | Crop a region, e.g. 800x600+100+50. | | --extend <edges> | Pad: 10, 10,20, or 10,20,30,40 (top,right,bottom,left). | | --density <dpi> | Rasterization density for SVG input. |

Encoding

| Option | Default | Description | | --- | --- | --- | | -q, --quality <value> | per format | A number 1-100, or avif=50,webp=72. | | --effort <value> | avif 4, webp 4, png 10 | A number or avif=6,webp=4,png=10. | | --lossless | off | Lossless AVIF/WebP/PNG. JPEG is rejected. | | --progressive | on | Progressive JPEG. | | --no-progressive | — | Baseline JPEG. | | --chroma-subsampling <s> | 4:2:0 | 4:2:0, 4:2:2, or 4:4:4 (JPEG). | | --alpha-quality <n> | 100 | WebP alpha quality. | | --palette / --no-palette | auto | Force or disable PNG quantization. | | --compression-level <n> | 9 | PNG zlib level 0-9. | | --max-bytes <size> | none | Lower quality until each variant fits, e.g. 150kb. | | --background <color> | #ffffff | Matte for transparent JPEGs. | | --keep-metadata | off | Keep EXIF/ICC instead of stripping. |

Default quality is AVIF 55, WebP 78, JPEG 80, PNG 80. PNG quantizes to a palette below quality 100; use --quality png=100 for lossless PNG, or --lossless.

--max-bytes is a per variant budget. It first tries your requested quality, then binary-searches down. If even quality 1 cannot fit, that file is reported as failed instead of writing an oversized output. Lossless and --max-bytes cannot be combined.

Workflow

| Option | Default | Description | | --- | --- | --- | | --manifest <file> | none | Write a JSON report of every variant. | | --picture <file> | none | Write <picture> snippets for every input. | | --report <file> | none | Write a self-contained visual HTML report. | | --public-path <path> | / | URL prefix used in snippets. | | --sizes <value> | 100vw | sizes attribute used in snippets. | | --lqip | off | Add a tiny WebP data-URI placeholder + dominant color. | | --cache | on | Reuse outputs from .pixelpress-cache.json. | | --no-cache | — | Disable the cache. | | --force | off | Re-encode even when the cache is valid. | | --fail-fast | off | Stop after the first failed file. | | --concurrency <n> | 4 | Files processed in parallel. | | --dry-run | off | Show the plan; write nothing. | | --stdout | off | Write one encoded image to stdout. | | --json | off | Print the full report as JSON. | | --quiet | off | Only print the summary. | | --verbose | off | Print every variant. | | --watch | off | Re-run when inputs change. | | -h, --help, -v, --version | | |

Some flags take values beginning with - (for example a suffix of -2x); use the --flag=value form, e.g. --suffix=-2x.

Config file

Put defaults in pixelpress.config.json and run pixelpress with no inputs:

{
  "inputs": ["src/images/**/*.{jpg,png}"],
  "outDir": "public/images",
  "formats": ["avif", "webp"],
  "widths": [480, 960, 1920],
  "quality": { "avif": 50, "webp": 72 },
  "recursive": true,
  "cache": true,
  "lqip": true,
  "manifest": "public/images.json",
  "picture": "src/_includes/picture.html",
  "report": "public/images-report.html",
  "publicPath": "/images",
  "sizes": "(max-width: 900px) 100vw, 900px"
}

The file is auto-detected in the current directory. Use --config other.config.json for another path. .mjs and .js configs work too:

export default {
  inputs: ['assets'],
  outDir: 'dist/assets',
  formats: ['webp'],
};

CLI flags override config values. Config keys are the same as the API options. Paths in a config are resolved relative to the current working directory. JSON is recommended for portability.

--watch cannot be combined with --json, --dry-run, or --clean. --dry-run writes neither images nor any requested manifest, picture, or report file.

Caching and watch mode

The CLI enables a cache by default. It lives at <outDir>/.pixelpress-cache.json and stores each input's mtime, size, and outputs, keyed by a fingerprint of all encoding options. Changing quality, widths, transforms, or formats invalidates it automatically. Deleting an output or touching an input re-encodes that file.

pixelpress photos -o dist -r          # first run: encodes
pixelpress photos -o dist -r          # second run: cached
pixelpress photos -o dist -r --force  # ignore the cache
pixelpress photos -o dist -r --no-cache

--watch performs the initial run, then waits for image changes and re-runs with the cache. Output writes are ignored, so it does not loop. Watches are recursive on platforms that support it; on other platforms the input root is watched non-recursively.

Cache invalidation uses mtime + size, not content hashing, so replacing a file with identical mtime and size will be treated as unchanged. Use --force in CI or when in doubt.

Naming, manifests, and <picture>

--hash appends a content hash, which is useful for immutable CDN caching:

pre-hero-2x-960-a1b2c3d4.avif

--manifest writes every input, variant, dimension, byte count, quality, animation frame count, and cache state. --lqip adds placeholder (a base64 WebP data URI) and dominantColor to each file entry. Paths are relative to the current directory.

{
  "version": 2,
  "options": { "formats": ["avif", "webp"], "widths": [480], "...": "..." },
  "files": [
    {
      "input": "public/images/hero.jpg",
      "width": 2400,
      "height": 1600,
      "sourceBytes": 1436544,
      "cached": false,
      "dominantColor": "#6c7a89",
      "placeholder": "data:image/webp;base64,UklGR...",
      "outputs": [
        {
          "format": "avif",
          "path": "dist/images/hero-480.avif",
          "width": 480,
          "height": 320,
          "bytes": 14210,
          "quality": 55
        },
        {
          "format": "webp",
          "path": "dist/images/hero-480.webp",
          "width": 480,
          "height": 320,
          "bytes": 22981,
          "quality": 72
        }
      ]
    }
  ],
  "totals": {
    "files": 1,
    "outputs": 2,
    "cached": 0,
    "sourceBytes": 1436544,
    "outputBytes": 37191,
    "savedBytes": 1399353,
    "savedPercent": 97,
    "skipped": 0,
    "failed": 0,
    "durationMs": 342
  }
}

--picture writes markup like this, choosing the largest JPEG/PNG/WebP as the fallback and adding the other formats as <source>:

<!-- public/images/hero.jpg -->
<picture>
  <source type="image/avif" srcset="/images/hero-480.avif 480w, /images/hero-960.avif 960w" sizes="100vw">
  <source type="image/webp" srcset="/images/hero-480.webp 480w, /images/hero-960.webp 960w" sizes="100vw">
  <img src="/images/hero-960.jpg" srcset="/images/hero-480.jpg 480w, /images/hero-960.jpg 960w" sizes="100vw" width="960" height="640" alt="" loading="lazy" decoding="async">
</picture>

Edit alt afterwards; snippets intentionally start with an empty value.

HTML report

--report writes a self-contained page with every source and output side by side, byte savings, dimensions, quality, frame counts, and cache state. It has no external assets or network requests; open the file directly in a browser.

pixelpress photos -o dist -r --report dist/report.html
open dist/report.html

Previews link to files relative to the report, so the report can live next to the output or elsewhere on disk. --report is skipped by --dry-run and rewritten on every --watch run. It can be set as report in the config file.

stdin and stdout

Use - as the input and --stdout (or -o -) for a single image through a pipe. Logs go to stderr, so stdout is pure image data.

pixelpress photo.jpg -f avif --stdout > photo.avif
cat photo.jpg | pixelpress - -f webp --stdout > photo.webp
curl -s https://example.com/cat.jpg | pixelpress - -f webp -q 60 --stdout > cat.webp

In stream mode, use exactly one input and one format. --formats defaults to webp here. Use --width/--height instead of --widths; all transforms, quality/effort settings, --max-bytes, --lossless, and metadata options apply. --stdout cannot be combined with an output directory (-o other than -), --watch, --clean, --dry-run, --json, --manifest, --picture, or --report.

Animated images

Animated GIF and WebP inputs are supported:

  • WebP output preserves all frames, including resized width variants.
  • AVIF, JPEG, and PNG output use the first frame as a static fallback.
  • <picture> snippets put the animated WebP source first, with a static format as the fallback.
  • --lqip uses the first frame for its placeholder.

Use -f webp when animation must be preserved. Animated AVIF output is not supported by the current sharp/libvips build; it is on the roadmap.

API

import { optimize, transcode } from 'pixelpress';

const report = await optimize(['public/images', 'static/**/*.png'], {
  outDir: 'dist/images',
  formats: ['avif', 'webp', 'jpeg'],
  widths: [480, 960, 1920],
  quality: { avif: 50, webp: 72 },
  recursive: true,
  include: ['*.jpg', '*.png'],
  exclude: ['*-draft.*'],
  cache: true,
  lqip: true,
  concurrency: 8,
});

console.log(report.totals.savedPercent, '% smaller');
console.log(report.totals.cached, 'files reused from cache');

for (const file of report.files) {
  for (const variant of file.outputs) {
    console.log(variant.format, variant.path, variant.width, variant.bytes);
  }
}

Every path in the API result is absolute. The cache is off by default in the API; pass cache: true to enable it. force, clean, failFast, and all transforms accept the same names as the CLI.

Single image, buffer in and buffer out:

import { transcode } from 'pixelpress';

const { data, info } = await transcode('upload.jpg', {
  format: 'avif',
  width: 800,
  quality: 50,
  maxBytes: 90_000,
  grayscale: true,
  keepMetadata: false,
});

await fs.writeFile('upload.avif', data);
// info = { format, width, height, bytes, quality, sourceWidth, sourceHeight }

transcode accepts a filesystem path, Buffer, or Uint8Array and applies the same transforms, format tuning, and size budget as optimize.

Behavior and limits

  • Never overwrites the input. Output that resolves to an input file is refused; --out cannot equal a directory input.
  • No upscaling unless --enlarge. Responsive widths above the source are dropped.
  • Output collisions are errors, not silent overwrites.
  • Per-file failures do not stop the run unless --fail-fast; the CLI exits 1 if anything failed.
  • Metadata is stripped by default (including GPS); pass --keep-metadata to retain EXIF/ICC. EXIF orientation is applied even when metadata is stripped.
  • Transparent JPEGs are flattened onto --background.
  • Animated GIF/WebP inputs preserve frames in WebP; AVIF/JPEG/PNG use the first frame as a static fallback.
  • SVG inputs are rasterized, not minified. Run SVGO first if you need that.
  • --clean is guarded: it refuses the filesystem root, the home directory, the current directory, ancestors of the current directory, and any directory containing an input.
  • No stale cache surprises: the fingerprint covers every option that affects pixels or output paths.

Development

npm install
npm test

PLAN.md has the roadmap; COMPARISON.md shows how the feature set compares with sharp-cli, imagemin-cli, Squoosh CLI, vite-imagetools, and eleventy-img.

License

MIT