oi-optimize-images
v3.0.0
Published
πΌοΈ Shrink, convert and resize whole folders of images with one short command β fast, safe, powered by sharp
Maintainers
Readme
Shrink a whole folder of images with one short command.
oi ./imagesOptimized copies land in ./images-oi-out/ β your originals are never
touched. You get told exactly how many bytes you saved. Powered by
sharp.
oi β Optimize Images
βββββββββββββββββββββ
Path: /home/you/site/images
Output: /home/you/site/images-oi-out
Images: 24
Quality: 80
Format: original (keep)
Workers: 8
ββββββββββββββββββββ 100% | 24/24 files
β 24 image(s) optimized
Output: /home/you/site/images-oi-out
Before: 18.4 MB β After: 3.1 MB
Saved: 15.3 MB (83.2%)β¨ Features
- π¦ Bulk by default β point it at a folder, it walks every subfolder too
- π§΅ Uses the whole machine β encodes one image per core, not one at a time
- ποΈ Quality dial β one flag,
1to100, sensible80default - π Format conversion β JPG, PNG, WebP, AVIF, TIFF, GIF
- π Smart resize β fits inside your box and never upscales a small image
- π Live progress bar β plus a before/after savings report at the end
- π‘οΈ Non-destructive by default β output goes to a sibling
-oi-outfolder; add--in-placewhen you want the old overwrite behavior - π Safe writes β each file lands in a temp file and is renamed, so a cancelled run never leaves a half-written image behind
- π Upright photos β bakes in the camera's rotation instead of dropping it
- ποΈ Keeps animations β animated GIF and WebP keep every frame
- βοΈ Never grows a file β if re-encoding would cost bytes, the original size wins instead
- πͺ Keeps going β one broken image is reported and skipped, not fatal
- π mozjpeg encoding β smaller JPEGs than stock at the same quality
- πͺΆ Tiny install β sharp plus cli-progress, nothing else
- π§© Usable as a library β
require("oi-optimize-images")for the same engine without the CLI
π₯ Install
Pick your package manager. All four give you the same oi command:
npm install -g oi-optimize-imagesyarn global add oi-optimize-imagespnpm add -g oi-optimize-imagesbun add -g oi-optimize-imagesπ Or skip installing
npx oi-optimize-images ./images -q 70π Quick start
Squash a folder, keep the formats:
oi ./imagesWrites optimized copies to ./images-oi-out/. ./images itself is untouched.
Squash harder:
oi ./images -q 60Convert everything to WebP:
oi ./images -f webpOverwrite the originals instead of writing copies:
oi ./images --in-placeWrite into a folder you choose:
oi ./images -o ./optimizedMake thumbnails, max 400Γ400:
oi ./images -f webp -s 400x400 -q 75Leave some cores free for everything else:
oi ./images -j 2Just one file:
oi ./pics/photo.jpgWrites to ./pics-oi-out/photo.jpg β a single file follows the same
parent-folder sibling rule as a directory.
βοΈ Options
| Flag | What it does | Default |
|------|--------------|---------|
| -q, --quality <1-100> | ποΈ Output quality | 80 |
| -f, --format <fmt> | π original, jpg, png, webp, avif, tiff, gif | original |
| -s, --size <WxH> | π Fit inside these dimensions, e.g. 800x600 | none |
| -j, --concurrency <n> | π§΅ Images encoded at once | one per core |
| -o, --output <dir> | π Write into <dir> instead of the -oi-out sibling | none |
| --in-place | β»οΈ Overwrite the source files where they are | off |
| -h, --help | π¬ Show help | |
--in-place and -o can't be combined β pick one.
Which quality should I use?
| Value | Good for |
|-------|----------|
| 90-100 | π¨ Photography, print, archival |
| 75-85 | π Web images β the sweet spot |
| 60-75 | β‘ Thumbnails, previews, speed-first pages |
| < 60 | πͺΆ When bytes matter far more than looks |
β‘ Speed
Several images encode at once. On an 8-core laptop, 32 photos at 1600Γ1200 converted to WebP:
| -j 1 | -j 2 | -j 4 | -j 8 (default here) |
|--------|--------|--------|--------------------------|
| 14.6s | 8.7s | 6.2s | 5.0s |
Same bytes out whichever you pick β concurrency only changes how long you wait.
The default reads the cores actually available, so a 2-core VM uses two, and a
container capped at one CPU uses one rather than reaching for the host's total.
Turning -j down is the useful direction, for when you want the machine back.
β οΈ Good to know
- Originals stay put by default. Output goes to a sibling folder named
<input>-oi-outβoi ./imageswrites into./images-oi-out, and a single file likeoi ./pics/photo.jpgwrites into./pics-oi-out/photo.jpg. Use--in-placeto overwrite sources the old way, or-o <dir>to pick the output folder yourself. *-oi-outfolders are skipped on the way in. Pointingoiat a parent folder never re-processes a previous run's own output.- A file that can't be shrunk still lands in the output folder. Out of place, with the format unchanged, a file that would grow is copied over as is instead of being skipped, so the output stays a complete mirror of the input.
-snever enlarges. It uses fit-inside with no upscaling, so an image already smaller than your box is left at its own size.- Two files can't share one output. If
logo.jpgandlogo.pngwould both becomelogo.webp, the run stops before writing anything and tells you which two clash. Convert them separately. - A file already in the target format is skipped, not fought over. If a
folder holds both
logo.jpgandlogo.webpand you convert it to webp, the.jpgconverts and the existing.webpis left alone rather than causing a collision error β the same rule that makes a second--in-placerun over the same folder safe. The summary says how many were left alone. - Re-running costs quality, with
--in-place. Each pass re-encodes, so compressing an already compressed file again degrades it further. A rewrite that would come out bigger is thrown away and the original kept β but that is a size guard, not a quality one. -jreads the cores you actually have. Inside a container limited to one CPU it uses one worker rather than the host's full count.
Supported inputs: .jpg .jpeg .png .webp .avif .tiff .tif .gif
π¦ Use it from Node
The CLI is a thin layer over an API you can call yourself:
const { discoverImages, optimizeImages } = require("oi-optimize-images");
const { root, files } = await discoverImages("./images");
const summary = await optimizeImages(files, { inputRoot: root, quality: 70 });
console.log(`Saved ${summary.originalSize - summary.newSize} bytes`);
console.log(`Output in ${summary.outputDir}`);discoverImages(path) resolves path and returns { root, files }: the
folder the run is rooted at and the absolute path of every image found under
it (a single file's root is its parent folder). *-oi-out folders are
skipped automatically.
By default nothing under root is modified β output goes to the
${root}-oi-out sibling. Pass output: "<dir>" for a folder of your choosing,
or inPlace: true to overwrite sources instead; inputRoot is required
unless inPlace is set. optimizeImages otherwise takes the same tuning
options as the flags (quality, format, size: { width, height },
concurrency) and an optional third argument, { onProgress, onFailure }.
It never prints and never exits β bad input throws a UserError.
summary comes back as
{ total, optimized, copied, skipped, failed, originalSize, newSize, failures, outputDir }.
copied counts files that couldn't be shrunk and were mirrored unchanged;
outputDir is null when inPlace was used.
concurrency defaults to one image per core. QUALITY_MIN/QUALITY_MAX and
CONCURRENCY_MIN/CONCURRENCY_MAX are exported if you want to validate input
before handing it over.
π Migrating from 2.x
- The default output location changed.
oi ./imagesused to overwrite files in./images; it now writes into./images-oi-outand leaves./imagesalone. Add--in-placeto get the old overwrite behavior back, or-o <dir>to pick a different output folder. -d, --delete-originalis gone. Originals are kept by default now, so there's nothing to opt out of deleting.--in-placestill overwrites a source in place, including when converting it to a new format.- API renames.
findImageFilesis nowdiscoverImagesand returns{ root, files }instead of a plain array.optimizeImagestakesinputRoot/output/inPlaceinstead ofdeleteOriginal. The summary gainedcopiedandoutputDirand lostdeleted.optimizeImageandformatBytesare no longer exported. - Node >= 20.12 is now required, up from 20.9.0.
π€ Contributing
bin/oi.js shebang launcher, hands off to the CLI
src/
index.js public API β the root export
formats.js every supported format: extensions + sharp encoder
defaults.js default options, quality and worker ranges
discover.js a path in, { root, files } out β skips *-oi-out folders
plan.js decides each job's output path and collision rules
encode.js one file: resize, encode, atomic write
run.js many files: plan the jobs, spread over workers, tally a summary
pool.js runs N jobs at a time, results in input order
errors.js problems the user can fix
cli/
index.js wires parsing, discovery and reporting together
args.js argv -> options
help.js the --help screen
reporter.js every line the CLI prints
tune-runtime.js sizes the thread pool before sharp loads
test/ one file per module, plus end-to-end CLI testsTwo rules keep it easy to work in:
- Formats live in one place.
src/formats.jsdrives--formatvalidation, file discovery, the extension lookup and the sharp call. Supporting a new format is one entry inFORMATSand nothing else. - The core never prints and never exits. Anything under
src/outsidesrc/cli/throwsUserErrorand returns data.src/cli/reporter.jsowns the terminal, which is what makes the engine importable and testable.
The suite runs on Node's built-in test runner β no framework to install. It generates real images into a temp directory, so it exercises sharp for real rather than mocking it.
npm testEvery user-visible change goes in CHANGELOG.md under
[Unreleased], and moves under a version heading at release time.
π Requirements
Node.js >= 20.12.0
π License
MIT Β© Rubel Hossain
