diff --git a/README.md b/README.md
index 5451a29..eebe500 100644
--- a/README.md
+++ b/README.md
@@ -1,49 +1,89 @@
+
+
# image-and-video-compressor
-Compress images and videos from the command line or from Node. Parallel by default, honest about what it did, and safe to drive from a script.
+**Compress images and videos from the command line or from Node.**
+Parallel by default, honest about what it did, and safe to drive from a script.
+
+[](https://www.npmjs.com/package/image-and-video-compressor)
+[](https://www.npmjs.com/package/image-and-video-compressor)
+[](https://github.com/rohanpoudel2/image_video_compressor/actions/workflows/ci.yml)
+[](https://nodejs.org)
+[](https://www.typescriptlang.org/)
+[](./LICENSE)
+
+
+
+---
```bash
-npm install -g image-and-video-compressor
-imgvidcompress ./photos
+npx image-and-video-compressor ./photos
```
+That is the whole quick start. No install, no config file, no flags.
+
```
imgvidcompress compressing images and videos
- ✓ hero.jpg 4.2 MB → 388 KB −91.0%
- ✓ team-photo.jpg 2.8 MB → 241 KB −91.4%
- ✓ promo.mp4 18.4 MB → 5.1 MB −72.3%
- ○ icon.png skipped — compressing made it bigger, original kept
+ ✓ graphic-flat.png 976 KB → 64 KB −93.5%
+ ✓ hero-huge.jpg 330 KB → 148 KB −55.3%
+ ✓ portrait-tall.jpg 55 KB → 20 KB −62.9%
+ ✓ clip-1080p-30fps.mp4 4.3 MB → 2.4 MB −44.9%
+ ✓ clip-720p-60fps.mp4 4.4 MB → 2.0 MB −55.0%
+ ○ clip-480p-24fps.mov skipped — compressing made it bigger, original kept
──────────────────────────────────────────────────────────
- Total 25.4 MB → 5.7 MB −19.7 MB (77.6%)
- 3 compressed · 1 skipped · 12.4s
+ Total 14 MB → 7.1 MB −7.4 MB (51.1%)
+ 15 compressed · 1 skipped · 1.2s
```
-## Why v2
-
-v1 worked, but only on the happy path. v2 is a rewrite in TypeScript that fixes the things v1 got wrong and adds what a compressor should have had from the start.
-
-| | v1 | v2 |
-| ---------------------------- | ------------------------------------------------------ | ---------------------------------- |
-| Output piped to a file or CI | **crashed** (`clearLine is not a function`) | works |
-| `--output=.webm` | produced an unplayable file (H.264 in WebM) | VP9/Opus, or AV1 |
-| Video quality | `crf = 100 - quality`, often outside the codec's range | mapped onto each codec's own scale |
-| Frame rate | forced to 30fps, wrecking 24fps and 60fps sources | preserved unless you ask |
-| 500 files | 500 simultaneous processes | bounded worker pool |
-| One corrupt file | killed the whole batch | reported, batch continues |
-| Bytes saved | never shown | per file and in total |
-| Use from a script | `require()` ran the CLI | real library with types |
-
-Full detail in [MIGRATION.md](./MIGRATION.md).
+Real output from `npm run samples && imgvidcompress samples --recursive`. Your numbers will differ; these are the project's own test fixtures.
+
+## What you get
+
+- **One command for both.** Images and videos in the same run, detected by extension and, where that is not enough, by content.
+- **Parallel, but bounded.** A worker pool sized to your CPU count, not one process per file.
+- **It never lies about the result.** If compressing made a file bigger, the original is kept and the file is reported as skipped.
+- **Safe in a pipeline.** `--json` puts exactly one document on stdout and every human-facing byte on stderr. Distinct exit codes, stable error codes.
+- **A real library.** Typed ESM and CJS exports, `AbortSignal` support, progress events. Importing it does not run the CLI.
+- **Honest about your machine.** `formats` reports what your sharp and ffmpeg builds can actually do, verified by encoding, not by a hardcoded list.
+
+## Contents
+
+- [Install](#install)
+- [CLI](#cli)
+ - [Commands](#commands)
+ - [Options](#options)
+ - [Examples](#examples)
+- [Formats](#formats)
+- [File detection](#file-detection)
+- [Node API](#node-api)
+ - [Functions](#functions)
+ - [Options reference](#options-reference)
+ - [Results](#results)
+ - [Progress and cancellation](#progress-and-cancellation)
+ - [Branded types](#branded-types)
+- [Automation and AI agents](#automation-and-ai-agents)
+ - [The `--json` contract](#the---json-contract)
+ - [Exit codes](#exit-codes)
+ - [Error codes](#error-codes)
+- [Recipes](#recipes)
+- [FAQ](#faq)
+- [Coming from v1](#coming-from-v1)
+- [Contributing](#contributing)
+- [License](#license)
## Install
```bash
-npm install -g image-and-video-compressor
+npm install -g image-and-video-compressor # CLI everywhere
+npm install image-and-video-compressor # as a dependency
+npx image-and-video-compressor ./photos # no install at all
```
-Requires **Node 20.11+**. Images work out of the box. Video needs `ffmpeg` on your `PATH`:
+Requires **Node 20.11 or newer**.
+
+Images work out of the box — [sharp](https://sharp.pixelplumbing.com/) ships prebuilt binaries. Video needs `ffmpeg` on your `PATH`:
```bash
brew install ffmpeg # macOS
@@ -51,9 +91,9 @@ sudo apt install ffmpeg # Debian/Ubuntu
winget install Gyan.FFmpeg # Windows
```
-Or point at an existing binary with `--ffmpeg-path` / `FFMPEG_PATH`.
+Or point at a binary you already have with `--ffmpeg-path` or the `FFMPEG_PATH` environment variable. Without ffmpeg the image half still works; only video is unavailable, and the error says so.
-## Usage
+## CLI
```bash
imgvidcompress [options] # auto-detects images and videos
@@ -62,7 +102,60 @@ imgvidcompress video # videos only
imgvidcompress formats # what this build supports
```
-Paths can be files or directories, and your shell's globs work as usual.
+Paths can be files or directories, and your shell's globs work as usual. Output goes to `/compressed` unless you say otherwise, and a recursive run mirrors the input directory structure.
+
+### Commands
+
+| Command | What it does |
+| --------------------------------- | ------------------------------------------------------------------------------------ |
+| `imgvidcompress ` | Compress every image and video found. The default command. |
+| `imgvidcompress image ` | Images only. Ignores video files entirely. |
+| `imgvidcompress video ` | Videos only. |
+| `imgvidcompress formats` | Print this machine's real capabilities. Add `--json` for a machine-readable version. |
+| `imgvidcompress --help` | Full usage, including per-command help such as `image --help`. |
+
+### Options
+
+Shared by `run`, `image` and `video`:
+
+| Option | Default | Description |
+| ----------------------- | --------------------- | ------------------------------------------------------------------ |
+| `-q, --quality <1-100>` | `75` | Higher is better looking. Mapped onto each codec's own scale. |
+| `-t, --to ` | `.webp` / `.mp4` | Output format. Run `formats` to see what is available.† |
+| `-o, --out ` | `/compressed` | Output directory. Structure is mirrored under it. |
+| `-r, --recursive` | off | Descend into subdirectories. |
+| `-c, --concurrency ` | see below | Files processed at once. |
+| `--max-width ` | — | Shrink anything wider. Never enlarges. |
+| `--max-height ` | — | Shrink anything taller. Never enlarges. |
+| `--overwrite` | off | Replace existing output files instead of skipping them. |
+| `--dry-run` | off | Report the plan, write nothing. |
+| `--json` | off | One JSON document on stdout; human output moves to stderr. |
+| `--quiet` | off | Suppress all non-error output. |
+| `--no-color` | — | Disable coloured output. |
+| `--no-skip-larger` | — | Write the output even when it ends up bigger than the source. |
+
+† Images default to WebP, except that a source already in a modern format keeps it — an `.avif` input stays AVIF and a `.webp` input stays WebP rather than being pointlessly transcoded. Videos default to `.mp4`.
+
+Concurrency defaults differ by media kind, because the two libraries behave differently: images get `min(cores, 8)` since sharp parallelises internally through libvips, videos get `cores / 4` since a single ffmpeg process already saturates several cores. Stacking more mostly buys contention.
+
+**Image options**
+
+| Option | Default | Description |
+| ------------------ | ------- | ---------------------------------------------- |
+| `--keep-metadata` | off | Preserve EXIF and ICC instead of stripping it. |
+| `--no-auto-rotate` | — | Do not apply EXIF orientation. |
+
+**Video options**
+
+| Option | Default | Description |
+| ---------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
+| `--codec ` | per container | `libx264`, `libx265`, `libvpx-vp9`, `libvpx`, `libsvtav1`, `libaom-av1`, `mpeg4`, `libtheora`. Must be legal for the container. |
+| `--audio-codec ` | per container | `aac`, `libopus`, or `copy` to pass the original track through. |
+| `--fps ` | source rate | Cap the frame rate. Left alone by default. |
+| `--preset ` | per codec | Encoder speed/efficiency tradeoff. Codec-specific. |
+| `--ffmpeg-path ` | `$FFMPEG_PATH`, then `PATH` | Path to the ffmpeg binary. |
+
+### Examples
```bash
# Everything in a folder, into ./photos/compressed
@@ -79,38 +172,24 @@ imgvidcompress hero.png banner.jpg
# See the plan without touching anything
imgvidcompress ./assets --dry-run
-```
-
-### Options
-| Option | Description |
-| ---------------------------------------- | ----------------------------------------------------------- |
-| `-q, --quality <1-100>` | Higher is better looking. Default `75` |
-| `-t, --to ` | Output format. Default `.webp` for images, `.mp4` for video |
-| `-o, --out ` | Output directory. Default `/compressed` |
-| `-r, --recursive` | Descend into subdirectories |
-| `-c, --concurrency ` | Files at once. Defaults to a value based on CPU count |
-| `--max-width ` / `--max-height ` | Shrink to fit, never enlarge |
-| `--overwrite` | Replace existing output files |
-| `--dry-run` | Report the plan, write nothing |
-| `--json` | JSON report on stdout |
-| `--quiet` / `--no-color` | Quieten or decolour the output |
-| `--no-skip-larger` | Write output even when it ends up bigger than the source |
+# Web-ready video: VP9 in WebM, keeping the source frame rate
+imgvidcompress video ./clips --to .webm --quality 55
-**Images:** `--keep-metadata`, `--no-auto-rotate`
-
-**Video:** `--codec`, `--audio-codec`, `--fps`, `--preset`, `--ffmpeg-path`
+# Re-run over a folder you have already processed
+imgvidcompress ./photos --overwrite
+```
-### Formats
+## Formats
-Support is bounded by what your sharp and ffmpeg builds can actually do — not by a list in this README. Both are build-dependent, so run:
+Support is bounded by what your sharp and ffmpeg builds can actually do, not by a list in this README. Both are build-dependent, so ask the tool:
```bash
imgvidcompress formats # human-readable
imgvidcompress formats --json # machine-readable
```
-**Images.** The writable set is established by encoding a single pixel with each candidate at startup, which is the only check that cannot be wrong: `sharp.format` claims support for formats that then fail at encode time. JPEG 2000, JPEG XL and HEIC are all commonly missing, and a stock macOS build has none of the three. Reads cover everything libvips decodes — SVG, PDF, camera raw — which is a strictly wider set than writes.
+**Images.** The writable set is established at startup by encoding a single pixel with each candidate, which is the only check that cannot be wrong: `sharp.format` claims support for formats that then fail at encode time. JPEG 2000, JPEG XL and HEIC are all commonly missing, and a stock macOS build has none of the three. Reads cover everything libvips decodes, which is a strictly wider set than writes — SVG, PDF, and camera raw (`.arw`, `.cr2`, `.nef`, `.dng`, and friends) come in but never go out.
**Videos.** Two tiers. Curated containers get a verified codec matrix and tuned per-codec flags:
@@ -123,17 +202,21 @@ imgvidcompress formats --json # machine-readable
| `.avi` | H.264, MPEG-4 | MP3 |
| `.ogv` | Theora | Vorbis, Opus |
-Beyond those, **any muxer your ffmpeg reports is a valid `--to`** — a stock Homebrew build carries 184 of them, with around 100 video encoders available to `--codec`. For an uncurated container the muxer's own defaults apply, which are muxable by construction.
+Beyond those, **any muxer your ffmpeg reports is a valid `--to`**. A stock Homebrew build carries 184 of them, with around 100 video encoders available to `--codec`. For an uncurated container the muxer's own defaults apply, which are muxable by construction.
-WebM genuinely cannot carry H.264. For curated containers that restriction is enforced by the compiler, not a runtime check: an impossible pairing is a build error in library code and a clear message on the CLI.
+WebM genuinely cannot carry H.264. For curated containers that restriction is enforced by the compiler rather than by a runtime check: an impossible pairing is a build error in library code and a clear message on the CLI.
Quality maps onto each codec's own scale and direction — CRF counts down, Theora's `-q:v` counts up, MPEG-4 uses qscale — so `--quality 70` means the same thing regardless of codec.
-### File detection
+Streams that the target container cannot carry are reported rather than dropped in silence. Losing a commentary track or a subtitle without a word is worse than refusing outright, because nobody notices until they need it.
-Extension first, since it is free and almost always right. Content sniffing takes over where a name genuinely cannot answer: a file with no extension, an unknown one, or an ISO container where only the brand bytes distinguish an AVIF image from an MP4 video. A merely mislabelled file needs no special handling — sharp and ffmpeg both detect their input's real format. When a decode does fail, the file is sniffed so the error can say "this .jpg is actually an MP4" instead of "unrecognised data".
+## File detection
-## Programmatic API
+Extension first, since it is free and almost always right. Content sniffing takes over where a name genuinely cannot answer: a file with no extension, an unknown one, or an ISO container where only the brand bytes distinguish an AVIF image from an MP4 video.
+
+A merely mislabelled file needs no special handling, because sharp and ffmpeg both detect their input's real format. When a decode does fail, the file is sniffed so the error can say "this .jpg is actually an MP4" instead of "unrecognised data".
+
+## Node API
```ts
import { compressImages, toQuality, toPixels } from "image-and-video-compressor";
@@ -155,18 +238,133 @@ for (const result of report.results) {
}
```
-`compressImages`, `compressVideos` and `compress` always resolve. Per-file problems appear as `status: "failed"` inside `results`; only setup errors (no inputs, a bad option) reject. Pass an `AbortSignal` to cancel.
+Ships as ESM and CommonJS with bundled type declarations. `require()` and `import` both work.
+
+### Functions
+
+| Function | Signature |
+| ------------------------ | --------------------------------------------------------------------------------------------------------------- |
+| `compress` | `(paths: string[], options?: CompressOptions) => Promise` — images and videos, auto-detected |
+| `compressImages` | `(paths: string[], options?: ImageOptions) => Promise` |
+| `compressVideos` | `(paths: string[], options?: VideoOptions) => Promise` |
+| `discoverFiles` | Walk paths and classify what is there, without compressing |
+| `imageCapabilities` | What this sharp build can genuinely read and write |
+| `ffmpegCapabilities` | Muxers, demuxers and encoders this ffmpeg reports |
+| `resolveFfmpeg` | Locate the ffmpeg binary and its version |
+| `probeMedia` | ffprobe a file into typed stream information |
+| `sniff` / `sniffFile` | Identify a file by content rather than name |
+| `toQuality` / `toPixels` | Validating constructors for the branded scalar types |
-Quality and pixel values go through `toQuality()` and `toPixels()`, which validate the range and return branded types. A raw `number` will not typecheck — that is what stops a 1-100 quality being fed to a flag that expects 0-51.
+The three compression functions **always resolve**. Per-file problems appear as `status: "failed"` inside `results`; only setup errors — no inputs, a bad option, ffmpeg missing — reject with a `CompressorError`.
-## Scripting and AI agents
+Format registries (`VIDEO_CONTAINERS`, `VIDEO_CODECS`, `AUDIO_CODECS`, `CURATED_IMAGE_FORMATS`) and the codec-matrix predicates (`isCodecAllowedIn`, `isVideoContainer`, `qualityToCrf`, …) are exported too, so a caller can validate a combination before running one.
-`--json` writes exactly one JSON document to stdout. Every human-facing byte — banner, progress, summary — goes to stderr, so the stream is always safe to parse.
+### Options reference
+
+Shared by every entry point:
+
+| Option | Type | Default |
+| ------------- | -------------------------------- | --------------------- |
+| `quality` | `Quality` | `75` |
+| `outDir` | `string` | `/compressed` |
+| `recursive` | `boolean` | `false` |
+| `concurrency` | `number` | per media kind |
+| `overwrite` | `boolean` | `false` |
+| `dryRun` | `boolean` | `false` |
+| `skipLarger` | `boolean` | `true` |
+| `resize` | `ResizeOptions` | — |
+| `signal` | `AbortSignal` | — |
+| `onProgress` | `(event: ProgressEvent) => void` | — |
+
+`ImageOptions` adds `to`, `autoRotate` (default `true`) and `keepMetadata` (default `false`).
+`VideoOptions` adds `to`, `videoCodec`, `audioCodec`, `fps`, `preset` and `ffmpegPath`.
+`CompressOptions` is the union used by `compress()`, where `to` widens to accept either kind.
+
+### Results
+
+Every file produces one entry in a discriminated union, so a run always yields a complete record of what happened:
+
+```ts
+type JobResult =
+ | ({ status: "compressed" } & CompressedResult)
+ | ({ status: "skipped" } & SkippedResult)
+ | ({ status: "failed" } & FailedResult);
+```
+
+All three carry `kind`, `inputPath` and `outputPath`. Beyond that:
+
+| Status | Additional fields |
+| -------------- | ---------------------------------------------------------------------------------- |
+| `"compressed"` | `inputBytes`, `outputBytes`, `savedBytes`, `savedRatio`, `durationMs`, `warnings?` |
+| `"skipped"` | `inputBytes`, `reason`, `warnings?` |
+| `"failed"` | `error: { code, message, detail? }` |
+
+`savedBytes` is positive when space was saved and negative if the output grew. `savedRatio` is the fraction of the original removed, 0-1.
+
+`isCompressed`, `isSkipped` and `isFailed` are exported as type guards, so consumers never hand-check `status`:
+
+```ts
+import { isFailed, isCompressed } from "image-and-video-compressor";
+
+const failures = report.results.filter(isFailed);
+const bytes = report.results
+ .filter(isCompressed)
+ .reduce((sum, r) => sum + r.savedBytes, 0);
+```
+
+`reason` on a skip is one of `output-larger-than-input`, `output-exists` or `dry-run`.
+
+### Progress and cancellation
+
+```ts
+const controller = new AbortController();
+setTimeout(() => controller.abort(), 30_000);
+
+await compress(["./assets"], {
+ signal: controller.signal,
+ onProgress: (event) => {
+ switch (event.type) {
+ case "run-start":
+ console.log(`${event.total} files`);
+ break;
+ case "job-start":
+ break;
+ case "job-progress":
+ console.log(event.job.inputPath, event.ratio);
+ break;
+ case "job-done":
+ console.log(event.result.status);
+ break;
+ }
+ },
+});
+```
+
+`run-start` arrives once, after discovery and planning, because the total is not knowable before then. `job-progress` carries a 0-1 ratio and is video-only; images complete atomically.
+
+### Branded types
+
+Quality and pixel values go through `toQuality()` and `toPixels()`, which validate the range and return branded types. A raw `number` will not typecheck:
+
+```ts
+compressImages(["./photos"], { quality: 80 }); // ✗ compile error
+compressImages(["./photos"], { quality: toQuality(80) }); // ✓
+```
+
+This is what stops a 1-100 quality value being fed to a flag that expects a 0-51 CRF. `toQuality(150)` throws a `RangeValidationError` at the boundary rather than producing a quietly wrong encode. The project's own `npm run typecheck` asserts these failures with `@ts-expect-error`.
+
+## Automation and AI agents
+
+The tool is built to be driven by something that is not a person: a build script, a CI job, or a coding agent.
+
+### The `--json` contract
+
+`--json` writes exactly one JSON document to stdout. Every human-facing byte — banner, progress, summary, warnings — goes to stderr, so the stream is always safe to parse.
```bash
imgvidcompress ./assets --json | jq '.summary.savedBytes'
-imgvidcompress ./assets --dry-run --json | jq '.results[].outputPath'
-imgvidcompress formats --json | jq '.video.output'
+imgvidcompress ./assets --dry-run --json | jq -r '.results[].outputPath'
+imgvidcompress formats --json | jq '.video.curated[].extension'
```
```jsonc
@@ -175,24 +373,37 @@ imgvidcompress formats --json | jq '.video.output'
"ok": true,
"dryRun": false,
"summary": {
- "totalFiles": 3,
- "compressed": 3,
+ "totalFiles": 2,
+ "compressed": 2,
+ "skipped": 0,
"failed": 0,
- "savedBytes": 20658176,
- "savedRatio": 0.776,
+ "inputBytes": 64931,
+ "outputBytes": 33106,
+ "savedBytes": 31825,
+ "savedRatio": 0.4901,
+ "durationMs": 55,
},
"results": [
{
"status": "compressed",
- "inputPath": "…/hero.jpg",
- "outputPath": "…/compressed/hero.webp",
- "savedRatio": 0.91,
+ "kind": "image",
+ "inputPath": "…/photo-medium.jpg",
+ "outputPath": "…/compressed/photo-medium.webp",
+ "inputBytes": 44331,
+ "outputBytes": 17902,
+ "savedBytes": 26429,
+ "savedRatio": 0.5962,
+ "durationMs": 46,
},
],
}
```
-Exit codes:
+`schemaVersion` is bumped on any breaking change to these shapes, so a consumer can pin behaviour instead of guessing whether a field still means what it did last release. Errors use the same envelope with `ok: false` and an `error` object.
+
+`imgvidcompress formats --json` reports the installation's actual capabilities, including which codecs each container accepts. A caller can discover the constraints instead of hardcoding assumptions that are wrong on some machines.
+
+### Exit codes
| Code | Meaning |
| ---- | ----------------------------------------------------------- |
@@ -202,13 +413,95 @@ Exit codes:
| `3` | ffmpeg not found |
| `4` | No matching input files |
-Error codes in JSON (`FFMPEG_NOT_FOUND`, `ENCODE_FAILED`, `UNSUPPORTED_FORMAT`, …) are stable across releases; the human-readable messages are not. Branch on the code.
+### Error codes
+
+`FFMPEG_NOT_FOUND`, `FFMPEG_FAILED`, `DECODE_FAILED`, `ENCODE_FAILED`, `UNSUPPORTED_FORMAT`, `INPUT_NOT_FOUND`, `PERMISSION_DENIED`, `INVALID_OPTION`, `NO_INPUT_FILES`, `ABORTED`, `UNKNOWN`.
-`imgvidcompress formats --json` reports this build's actual capabilities, including which codecs each container accepts, so a caller can discover the constraints instead of hardcoding them.
+These are stable across releases. The human-readable messages are not — branch on the code.
-## Deprecated v1 commands
+## Recipes
-The v1 commands still work and still write to `optimised_images/` and `optimised_videos/`. They print a deprecation notice and will be removed in v3.
+**Compress assets as part of a build**
+
+```json
+{
+ "scripts": {
+ "build:assets": "imgvidcompress image ./src/assets --recursive --to .webp --out ./dist/assets"
+ }
+}
+```
+
+**Fail CI when an asset is too big to ship**
+
+```bash
+imgvidcompress ./public/img --dry-run --json \
+ | jq -e '[.results[].inputBytes] | max < 500000' \
+ || { echo "An image exceeds 500 KB"; exit 1; }
+```
+
+**Report the saving without writing anything**
+
+```bash
+imgvidcompress ./assets --dry-run --json | jq '.summary'
+```
+
+**Use it from a build script**
+
+```ts
+import { compressImages, toQuality, isFailed } from "image-and-video-compressor";
+
+const report = await compressImages(["./src/assets"], {
+ quality: toQuality(82),
+ to: ".webp",
+ recursive: true,
+ outDir: "./dist/assets",
+});
+
+const failed = report.results.filter(isFailed);
+if (failed.length > 0) process.exit(1);
+```
+
+**Check what the machine supports before offering a format**
+
+```ts
+import { imageCapabilities } from "image-and-video-compressor";
+
+const caps = await imageCapabilities();
+const canAvif = caps.writable.some((f) => f.extensions.includes(".avif"));
+```
+
+## FAQ
+
+**A file came out bigger. Why is it in the output as "skipped"?**
+Because the original was kept. Some inputs — small PNGs, already-optimised WebP, low-detail graphics — do not get smaller when re-encoded. Rather than write a worse file, the tool keeps the original and tells you. Pass `--no-skip-larger` if you want the bigger output anyway, usually because you need the format conversion more than the bytes.
+
+**Where did my files go?**
+`/compressed`, next to the input. Use `--out` to choose. A recursive run mirrors the input tree under the output directory rather than flattening it.
+
+**Does it overwrite my originals?**
+Never. Output always goes to a separate directory, and an existing output file is skipped unless you pass `--overwrite`.
+
+**`FFMPEG_NOT_FOUND`, but ffmpeg is installed.**
+It is not on the `PATH` this process sees, which is common with GUI-launched editors and some CI images. Pass `--ffmpeg-path /full/path/to/ffmpeg`, or set `FFMPEG_PATH` (and `FFPROBE_PATH`, which is derived from it by default). Confirm with `imgvidcompress formats`, which prints the resolved ffmpeg version; `formats --json` carries the full path in `video.ffmpeg`.
+
+**`--to .webm` rejected my `--codec libx264`.**
+WebM cannot carry H.264. Use `libvpx-vp9` or `libsvtav1`, or target `.mp4` instead. `imgvidcompress formats` lists the legal codecs per container for your build.
+
+**Why is `.avif` or `.jxl` missing from `formats`?**
+Your sharp build was compiled without it. The list is produced by actually encoding a pixel with each candidate, so it reflects the binary you have rather than what the package hopes is there.
+
+**Can I compress in place?**
+Not directly, and deliberately so. Write to a directory, verify, then move. A crash mid-encode over the original is not recoverable.
+
+**Is quality 75 the same for JPEG and for H.265?**
+Yes, in the sense that matters: the value is mapped onto each codec's own scale and direction. You do not need to remember that CRF counts down while Theora counts up.
+
+**Does it work when output is piped or redirected?**
+Yes. The progress display degrades to plain append-only lines when stdout is not a TTY, which is also the nicer CI log. (v1 crashed here; see below.)
+
+## Coming from v1
+
+**Nothing you have scripted breaks today.** The v1 commands still work, still write to `optimised_images/` and `optimised_videos/`, and still take the same flags. They print a deprecation notice and will be removed in v3.
```bash
# Still works — prints a notice
@@ -218,9 +511,24 @@ imgvidcompress optimise:image --loadFolder='./photos' --quality=40 --output='.we
imgvidcompress image ./photos --quality 40 --to .webp
```
-Nothing you have scripted needs to change today. They run on the v2 pipeline, so they pick up every fix above while keeping v1's output layout and defaults.
+They run on the v2 pipeline, so they pick up every fix below while keeping v1's output layout and defaults.
+
+| | v1 | v2 |
+| ---------------------------- | ------------------------------------------------------ | ---------------------------------- |
+| Output piped to a file or CI | **crashed** (`clearLine is not a function`) | works |
+| `--output=.webm` | produced an unplayable file (H.264 in WebM) | VP9/Opus, or AV1 |
+| Video quality | `crf = 100 - quality`, often outside the codec's range | mapped onto each codec's own scale |
+| Frame rate | forced to 30fps, wrecking 24fps and 60fps sources | preserved unless you ask |
+| 500 files | 500 simultaneous processes | bounded worker pool |
+| One corrupt file | killed the whole batch | reported, batch continues |
+| Bytes saved | never shown | per file and in total |
+| Use from a script | `require()` ran the CLI | real library with types |
+
+Full detail, including the exact command mapping and changed defaults, is in [MIGRATION.md](./MIGRATION.md).
+
+## Contributing
-## Development
+Issues and pull requests are welcome at [rohanpoudel2/image_video_compressor](https://github.com/rohanpoudel2/image_video_compressor).
```bash
npm install
@@ -228,24 +536,25 @@ npm run build
npm test # builds, then runs the suite
npm run typecheck # also enforces the compile-time guarantees
npm run lint
+npm run format:check
```
-### Trying it locally
+**Trying it locally**
```bash
npm run samples # downloads/generates ~14MB of test media into samples/
npm run build
node dist/cli.js samples --dry-run # see the plan
-node dist/cli.js image samples/images # ~68% on real photos
-node dist/cli.js video samples/videos # ~85% on generated clips
+node dist/cli.js image samples/images # real photos
+node dist/cli.js video samples/videos # generated clips
```
`samples/` is gitignored: photos come from Lorem Picsum (Unsplash-licensed) and videos are generated by ffmpeg's synthetic sources, so both are reproducible rather than committed.
-Video end-to-end tests skip themselves when ffmpeg is missing; the codec matrix, quality mapping and output parsers are pure functions and always run. `npm run typecheck` is a real test here — `test/types.test.ts` uses `@ts-expect-error` to assert that invalid container/codec pairings and unbranded quality values still fail to compile.
+**How the suite is structured.** Video end-to-end tests skip themselves when ffmpeg is missing; the codec matrix, quality mapping and output parsers are pure functions and always run. `npm run typecheck` is a real test here — `test/types.test.ts` uses `@ts-expect-error` to assert that invalid container/codec pairings and unbranded quality values still fail to compile. Capability tests assert that what the tool _claims_ to support matches what the local binaries actually do, so a build missing JPEG XL or HEVC is described accurately rather than optimistically.
-Capability tests assert that what the tool _claims_ to support matches what the local binaries actually do, so a build missing JPEG XL or HEVC is described accurately rather than optimistically.
+CI runs on Node 20, 22 and 24 across Linux, macOS and Windows, with ffmpeg installed on each, and verifies the published tarball imports cleanly from both ESM and CommonJS.
## Credits
@@ -253,4 +562,4 @@ Built on [sharp](https://sharp.pixelplumbing.com/) and [ffmpeg](https://ffmpeg.o
## License
-MIT © Rohan Poudel
+MIT © [Rohan Poudel](https://github.com/rohanpoudel2)
diff --git a/package-lock.json b/package-lock.json
index 679aa22..0ac6c06 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "image-and-video-compressor",
- "version": "2.0.0",
+ "version": "2.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "image-and-video-compressor",
- "version": "2.0.0",
+ "version": "2.0.1",
"license": "MIT",
"dependencies": {
"commander": "^15.0.0",
diff --git a/package.json b/package.json
index be47ba9..7c4c29b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "image-and-video-compressor",
- "version": "2.0.0",
- "description": "Fast, type-safe image and video compression for Node.js \u2014 parallel by default, with a colorful CLI and machine-readable JSON output for AI agents.",
+ "version": "2.0.1",
+ "description": "Fast, type-safe image and video compression for Node.js — parallel by default, with a colorful CLI and machine-readable JSON output for AI agents.",
"author": "Rohan Poudel",
"license": "MIT",
"type": "module",