img-subir
v0.0.1
Published
Agent-friendly CLI that optimizes images (PNG/JPEG/WebP/AVIF/GIF/SVG) for websites, READMEs and Subir pages.
Maintainers
Readme
img-subir
Agent-friendly CLI that makes images small enough for websites, READMEs and Subir pages. Point it at a folder of screenshots and it does the right thing: downsizes Retina captures, strips metadata, tries WebP / AVIF / PNG / JPEG, keeps the smallest one that still looks the same, and never writes a file that is bigger than the original.
npx img-subir ./assetsimg-subir v0.1.0 · preset: web
✔ [email protected] 2880×1800 PNG 3.40 MB → [email protected] 1920×1200 q82 148 KB −95.7%
✔ settings.png 1440×900 PNG 1.10 MB → settings.webp 1440×900 q82 92 KB −91.8%
✔ logo.svg SVG 41 KB → logo.svg (svgo) 12 KB −70.7%
· already.webp 320×200 WEBP 200 B kept: no candidate smaller than original
Saved 4.54 MB → 252 KB (−94.6%) in 1.8s · 3 optimized, 1 kept
Originals kept in assets/.img-subir-originals — delete or ignore before upload/commit (or use --no-backup).Why
Screenshots straight out of macOS are 3–5 MB each. Put a few in a plan page, a README or a marketing site and the page is slow, the repo bloats, and storage fills up. img-subir is the one command an agent (or you) runs right before uploading or committing.
Install
npx img-subir <paths> # no install
npm i -g img-subir # or global
pnpm add -D img-subir # or per projectNode ≥ 20. Uses sharp (prebuilt libvips, no system deps) and svgo.
Usage
img-subir <paths...> [options] files, directories (recursive) or globs
-p, --preset <name> web | github | screenshot | og | thumb | lossless (default: web)
-f, --format <fmt> auto | keep | webp | avif | png | jpeg (default: auto)
--kind <kind> auto | ui | photo | graphic (default: auto)
--max-width <px> cap width (overrides preset)
--max-height <px> cap height (overrides preset)
-q, --quality <1-100> starting quality for lossy encoders (overrides preset)
--target-kb <n> binary-search quality until each file is ≤ n KB
-o, --out <dir> write to dir (mirrors structure; kept files are copied too) instead of in place
--suffix <str> insert before the extension, e.g. ".min"
--no-backup don't keep originals in .img-subir-originals/
--no-rename keep the original filename even if the format changes
--rewrite-refs update src/href/url() in html/md/tsx/css that point at renamed files
-n, --dry-run report, write nothing
--json JSON report on stdout, human log on stderr
--fast skip SSIM checks (~3× faster)
-c, --concurrency <n> files in flight (default: cpus − 1)
--quiet / --verbose
img-subir inspect <paths...> metadata, detected kind, and what optimize would do
img-subir presets list presets
img-subir skill print the SKILL.md for agents
img-subir doctor verify sharp and every encoder on this machinePresets
| Preset | For | Max width | Tries | Notes |
| ------------ | ------------------------------------- | --------- | ----------------------- | ----------------------------------------------------------- |
| web | websites, Subir pages, docs | 1920 | webp, avif, png, jpeg | default. AVIF kept only when ≥15% smaller than WebP |
| github | README images, PR screenshots | 1600 | webp, png, jpeg | never AVIF |
| screenshot | crisp UI captures | 1600 | webp, webp-lossless, png | forces kind=ui, higher quality floor |
| og | Open Graph / social cards | 1200×630 | jpeg, png | many crawlers dislike WebP |
| thumb | thumbnails, avatars | 480 | webp, avif | aggressive; pair with --target-kb |
| lossless | pixel-exact | none | png, webp-lossless | no resize, no lossy |
Unknown flags, missing values and malformed config files are errors (exit 2), never silent fallbacks, so an agent typo like --dryrun cannot cause real writes.
Flags beat config beat preset. Config lives in img-subir.config.json, .img-subirrc.json or a "img-subir" key in package.json, found by walking up from the current directory:
{ "preset": "github", "maxWidth": 1400, "rewriteRefs": true }How it decides
- Probe — format, size, alpha, animation. A PNG whose alpha channel is entirely opaque is treated as opaque (this unlocks JPEG and smaller WebP for most macOS screenshots).
- Classify —
ui/photo/graphicfrom cheap pixel statistics (flat runs, distinct colors) plus filename and device-size hints. Override with--kind. - Resize — fit inside the preset's max width/height, never enlarge, Lanczos3. EXIF orientation is applied, colors converted to sRGB, metadata dropped.
- Encode candidates in parallel: lossy WebP, AVIF (skipped above 4 MP), palette PNG, mozjpeg JPEG, plus lossless PNG/WebP for graphics.
- Quality floor — each lossy candidate is compared to the resized source with SSIM (0.985 for UI, 0.96 for photos by default). Candidates below the floor get +6 quality and another try, up to three times.
--fastskips this. - Pick the smallest passing candidate. AVIF must beat the best non-AVIF by 15%. If nothing is smaller than the input (or the gain is under 5%, or under 10% when re-encoding an already-lossy file), the file is kept untouched.
- Write atomically — temp file, move the original to
.img-subir-originals/, rename. A crash never leaves a half-written image.
SVGs go through svgo (viewBox, width/height and ids preserved). Animated GIF/APNG/WebP become animated WebP (lossless under the lossless preset).
Safety rules: an existing file at the output path is never overwritten (the input is kept with a reason), --out may not be an input directory, and when the extension changes the new file is published before the original is moved or deleted.
For agents
img-subir is built to be run blind by a coding agent:
- Non-interactive, no prompts, no colors when stdout isn't a TTY.
--jsonemits a versioned report (schemaVersion: 1).renamed[]lists everya.png → a.webpso the agent can fix references, or pass--rewrite-refsand readrewrittenRefs[].- Every kept file has a
reason; every written file has anactions[]trail. - Exit codes:
0ok ·1a file failed (others still written) ·2bad invocation ·3--target-kbcould not be met within the quality floor (best effort written). img-subir skillprints a ready-to-save SKILL.md for Claude Code, Codex and friends. Drop it in~/.claude/skills/img-subir/.
// npx img-subir ./assets --json
{
"schemaVersion": 1,
"preset": "web",
"files": [
{
"input": { "path": "…/[email protected]", "format": "png", "width": 2880, "height": 1800, "bytes": 3565158, "hasAlpha": true, "animated": false },
"output": { "path": "…/[email protected]", "format": "webp", "width": 1920, "height": 1200, "bytes": 151552, "quality": 82 },
"kind": "ui", "status": "optimized", "savedBytes": 3413606, "savedPercent": 95.7,
"actions": ["drop-unused-alpha", "resize:1920x1200", "strip-metadata", "encode:webp@82"],
"candidates": [{ "format": "webp", "bytes": 151552, "quality": 82, "ssim": 0.9931, "passed": true }, …]
}
],
"totals": { "files": 4, "inputBytes": 4761600, "outputBytes": 258048, "savedPercent": 94.6, "optimized": 3, "kept": 1, "failed": 0, "budgetMissed": 0 },
"renamed": [{ "from": "…/[email protected]", "to": "…/[email protected]" }],
"rewrittenRefs": [],
"backupDirs": ["…/assets/.img-subir-originals"]
}With Subir
npx img-subir ./site --rewrite-refs # shrink assets, fix <img src> in the HTML
rm -rf ./site/.img-subir-originals # once you've eyeballed the page
subir upload ./siteProgrammatic API
import { optimize, inspect, type Report } from "img-subir";
const report: Report = await optimize({ paths: ["./assets"], preset: "github", rewriteRefs: true });
console.log(report.totals.savedPercent);License
MIT
