Skip to content
Eugene Lazutkin edited this page Sep 16, 2026 · 4 revisions

imop

Optimize and convert images for the web. Automatically selects the best available tool for each format with fallback chains.

Usage

imop photo.jpg                # Optimize in place
imop photo.jpg out.jpg        # Optimize to new file
imop photo.jpg photo.webp     # Convert to WebP
imop images/                  # Batch optimize in place
imop images/ output/          # Batch to output dir
imop --to webp images/        # Batch convert to WebP

Commands

Command Description
optimize, o Re-compress in same format (default)
convert, c Convert to format of output file
batch, b Process all images in a directory

Commands can be omitted — imop infers the operation from arguments (file vs directory, same vs different extension).

Options

Option Description
-q, --quality N Quality 0–100 (default: 80)
--lossy Force lossy compression
--lossless Force lossless compression
--best Use slow-but-best compressor (guetzli/zopflipng)
--to EXT Target format for batch conversion
--suffix TAG Append tag to output names
--no-suffix-on-top In batch --to, replace the source extension instead of appending
--include-variants Don't skip pre-generated .webp/.avif/.jxl siblings in batch
--min-ratio PCT Drop output unless at least PCT% smaller; 0 = unconditional (default: 0)
-j, --jobs N Parallel jobs for batch (default: 1, 0 = nproc)
-n, --dry-run Show commands without running

Supported formats and tools

Supported formats: JPEG, PNG, WebP, AVIF, JXL.

Format Mode Tools (preference order)
JPEG lossless jpegoptim
JPEG lossy mozjpeg (cjpeg) > jpegoptim
JPEG best guetzli (very slow, quality ≥ 84)
PNG lossless oxipng > optipng
PNG lossy pngquant + oxipng > pngquant + optipng
PNG best zopflipng (very slow)
WebP lossless source cwebp -lossless
WebP lossy source cwebp -q
AVIF avifenc
JXL cjxl

WebP: re-encoded the way it was encoded

imop photo.webp asks webpinfo (installed with cwebp) whether the bitstream is lossy or lossless and re-encodes it the same way: a lossy file at --quality, a lossless file losslessly. Re-encoding a lossy WebP with -lossless is a format change that always grows the file, which is what happened before 1.4.0 and rolled back every time. --lossy and --lossless override the detection.

JPEG → JXL: lossless transcode by default

Converting a JPEG source to JXL defaults to cjxl's bit-exact lossless transcode — the JPEG's DCT coefficients are repacked, not re-encoded, typically ~20% smaller and byte-reversible with djxl --jpeg_to_pixels=0. --quality is not consulted on this path; pass --lossy to opt into a real re-encode at the requested quality. Non-JPEG sources encode lossily at --quality (or losslessly with --lossless), like WebP/AVIF.

Sources the target encoder can't read

The encoders read JPEG and PNG. cwebp also reads WebP and cjxl reads JXL; neither reads the other's format, and avifenc reads nothing but JPEG, PNG, and Y4M. When the target encoder can't read the source, imop decodes it to a PNG beside the source first (photo.avif.tmp.png, removed afterwards): dwebp for WebP, djxl for JXL, and ImageMagick (magick) for AVIF, with avifdec as the fallback. That covers re-optimizing an AVIF in place and every cross-codec conversion (WebP to AVIF, AVIF to JXL, and so on).

ImageMagick is preferred over avifdec for one reason: avifdec copies the AVIF's colour signalling into a PNG cICP chunk, avifenc's default transfer there is "unspecified", and cjxl 0.12 refuses such a PNG (Getting pixel data failed). Without ImageMagick, AVIF to JXL fails with that error and every other pair works.

Re-encoding a lossy source this way is a generation loss, the same as re-optimizing a JPEG; the in-place rollback below still guards against growth.

Installing tools

brew install jpegoptim mozjpeg oxipng pngquant webp libavif jpeg-xl imagemagick
brew install guetzli zopfli    # slow-but-best compressors (optional)

Batch output naming

In batch --to <ext>, output files default to suffix-on-top — the source extension is preserved and the target is appended:

photo.jpg  →  photo.jpg.webp
icon.png   →  icon.png.avif
scan.jpg   →  scan.jpg.jxl

This matches the cxop family (name.html.br) and the lambda variant probe (name + '.webp'). Pass --no-suffix-on-top to revert to extension-replace (photo.webp).

Skipping pre-generated variants

In batch, imop skips files that look like the output of a prior batch pass — .webp/.avif/.jxl files whose .jpg/.jpeg/.png source exists alongside, in either naming form (foo.jpg.webp next to foo.jpg, or foo.webp next to foo.jpg). Re-encoding a .webp as a .webp source typically grows it; skipping them avoids that whole class of regression.

Pass --include-variants to process them anyway.

In-place rollback

When optimizing in place, imop always snapshots the source before re-encoding and rolls back if the result grew the file — even at the default --min-ratio 0. This makes imop optimize safe to run on directories that mix already-optimized and unoptimized images: bigger results never overwrite the original.

--min-ratio N (N > 0) keeps the source unless the result is at least N% smaller, both in-place and for separate-output. For separate-output (convert or optimize <in> <out>), --min-ratio 0 keeps the output regardless of size — the source is unaffected, so there's no destructive risk to guard against.

Encoder failures

When an encoder or the decode step fails, imop restores an in-place source from its snapshot, removes a partial separate output, prints encoder failed, and exits non-zero; in batch the file counts as failed. Before 1.3.0 a failed encode was reported as 0% saved with exit 0.

Sibling: cxop

For general compression (.gz/.br/.zst sibling variants for HTTP content negotiation), reach for cxop. imop handles raster images (JPEG/PNG/WebP/AVIF/JXL); cxop produces generic compression siblings for anything else worth compressing. Same scaffolding (options.bash, batch + parallel + dry-run), different codecs.

Clone this wiki locally