btk

btk image

Convert, compress and inspect images with Bun.Image. Bun decodes, resizes and encodes the images itself with libjpeg-turbo, libspng and libwebp, plus the OS codecs for AVIF and HEIC. You don't need sharp or ImageMagick.

Commands

CommandWhat it does
btk image <paths...>Convert and compress images. Short for btk image optimize. btk img works too
btk image info <paths...>Show each image's format, dimensions and size
btk image presetsList the presets for --preset, including the ones in your config
btk image --uiOpen the dashboard

optimize

btk image <paths...> writes a WebP at quality 80 next to each image and keeps the original. Paths can be files, folders or quoted globs.

btk image hero.png                                  # hero.webp next to hero.png
btk image ./assets -r --format avif -q 60           # a whole tree, as AVIF
btk image ./photos --preset web --out-dir dist/img  # 1920px wide, q75, into dist/img
btk image "photos/**/*.jpg" -w 1200                 # a glob; shrink to 1200px wide
btk image logo.png --format png --palette --overwrite
btk image ./assets -r --dry-run --json              # plan only, machine-readable

info

btk image info <paths...> reads images without changing them. --placeholder adds a ThumbHash blur placeholder as a data URL.

btk image info hero.png
btk image info ./photos -r
btk image info hero.png --placeholder --json
file                    format  dimensions   MP    size
./hero.png              png     2560 × 1280  3.28  298.8 KB
./photos/beach.jpg      jpeg    1600 × 800   1.28  104.4 KB
./photos/trip/hill.jpg  jpeg    1200 × 600   0.72  63.6 KB

presets

btk image presets lists every preset with its settings and where it comes from: built-in or config.

btk image presets
name       settings            source
web        q75 · fit 1920×∞    built-in
thumbnail  q60 · fit 400×400   built-in
social     q80 · fit 1200×1200 built-in

When to use it

Your site loads slowly because of large photos. Convert them to WebP and cap the width at 1920px. The originals stay untouched, and the table shows how much each file shrank:

btk image ./public/images -r --preset web

Then point your <img> tags or imports at the new .webp files.

Try it before changing anything. A dry run encodes every image and reports the savings without writing a file:

btk image ./public/images -r --preset web --dry-run

Shrink screenshots before you commit them. Keep each file's format and replace it in place. btk skips any file that re-encoding would make larger:

btk image ./docs/assets --format original --overwrite

Screenshots and diagrams with few colors usually get much smaller with --format png --palette, which reduces them to 256 colors.

Make thumbnails for a gallery. Write small copies into a separate folder, mirroring subfolders:

btk image ./photos -r --preset thumbnail --out-dir ./photos/thumbs

Check an image before uploading it, or get a blur placeholder. info shows the format, dimensions and size, and --placeholder adds a tiny ThumbHash data URL you can show while the real image loads:

btk image info hero.jpg --placeholder

Pick files by eye. btk image --ui opens a file browser with a preview, lets you choose several images, adjust the options and watch the run.

Defaults

With the defaults, running btk on a folder never deletes or replaces a file you didn't ask it to:

BehaviorDefault
Output formatWebP, quality 80
Output locationNext to each original (--out-dir mirrors subfolders elsewhere)
ResizingNone. --max-width and --max-height shrink the image to fit inside the box, keep the aspect ratio and never enlarge
Existing filesNever replaced. --overwrite allows it, including optimizing in place
Same format and size, but biggerSkipped, because the original was already smaller. Nothing is written
Earlier outputs (hero.webp next to hero.png)Left alone instead of being processed again as sources
The --out-dir folderNever scanned for inputs, so a second run doesn't write dist/dist/...
Two inputs, one output (a.png and a.jpg both becoming a.webp)The most faithful source wins (PNG, then BMP, TIFF, GIF, HEIC, JPEG, WebP, AVIF); btk skips the other
JPEGProgressive
PNGMaximum zlib compression. --palette reduces it to 256 colors
FoldersTop level only. -r includes subfolders, except hidden folders and node_modules
WritesAtomic: btk writes a temp file, then renames it
ConcurrencyUp to 4 images at once

A failed file never stops the batch. Every file gets a row that says it was converted, skipped with a reason, or failed with a reason and, when there is one, a fix.

Formats

FormatReadWriteNotes
JPEG, PNG, WebP✔✔Everywhere
AVIF✔✔Writing needs macOS on Apple Silicon M3+, or Windows with the AV1 Video Extension
HEIC✔✔macOS, or Windows with the HEIF Image Extension
GIF, BMP✔GIF reads the first frame
TIFF✔macOS and Windows

--format original re-encodes each file in its own format. A file in a read-only format then fails with a hint to pick an output format.

Options

optimize:

FlagConfig keyDefault
-f, --format <webp|avif|jpeg|png|heic|original>formatwebp
-q, --quality <1-100> (jpeg, webp, avif, heic)quality80
--lossless (WebP)losslessfalse
--palette (PNG)palettefalse
-w, --max-width <n>maxWidthnone
--max-height <n>maxHeightnone
-p, --preset <name>presetsnone
-o, --out-dir <dir>outDirnext to the original
-r, --recursiverecursivefalse
--overwriteoverwritefalse
-n, --dry-runfalse

info takes --placeholder and -r, --recursive.

You can also set every config key with an environment variable named BTK_IMAGE_<KEY>, such as BTK_IMAGE_QUALITY=70 or BTK_IMAGE_MAX_WIDTH=1600.

Presets

NameSettings
webq75, fit 1920 wide
thumbnailq60, fit 400×400
socialq80, fit 1200×1200

An explicit flag beats the preset, the preset beats config (env, project and global), and config beats the built-in defaults. So --preset web -q 90 keeps the 1920px limit and uses quality 90, while quality: 90 in config doesn't override the preset.

Add or override presets in config:

export default defineConfig({
  tools: {
    image: {
      format: "avif",
      presets: {
        hero: { maxWidth: 2560, quality: 85 },
        web: { quality: 70, maxWidth: 1600 },
      },
    },
  },
});

Output

ℹ 4 images → webp · q75 · fit 1920×∞
✔ Processed 4 images (156ms)
   file            output                     size  change  note
─  ──────────────  ──────────  ───────────────────  ──────  ─────────────────────────────────
✔  ./hero.png      hero.webp    1.35 MB → 453.2 KB  -67.2%  2400 × 1600 → 1920 × 1280
–  ./icon.webp                                              Output would replace the original
✔  ./photo.jpg     photo.webp  447.8 KB → 245.7 KB  -45.1%  1200 × 800

→ Pass --overwrite to optimize in place, or --out-dir to write elsewhere

✔ Done · 3 converted · 1 skipped · 1.96 MB → 824.5 KB · 1.16 MB (59.0%) saved · 156ms elapsed

Changes over 20% are green, 0 to 20% yellow, and growth red. --json returns the full report with the options, one entry per file and the totals. Each entry has a status, the before and after format, dimensions and bytes, and a reason, code and hint when relevant.

Exit codes

CodeWhen
0Everything converted or was skipped on purpose
1At least one file failed, a path matched nothing, or no images were found
2Bad flags, such as an unknown preset
78Invalid tools.image config
130Cancelled with Ctrl+C (btk still prints the partial report)

Dashboard (btk image --ui)

  1. Browse. A file browser shows only images, with a preview panel for the highlighted one (format, dimensions, megapixels, size). space selects several files, a selects all, and enter continues.
  2. Options. A form built from the same flags as the CLI, prefilled from your config.
  3. Run. Progress for each file as it happens, then the results table and totals. esc cancels a run, and enter returns to the browser with the listing refreshed.

Each screen keeps its state, so esc from Options returns to the same folder with the same selection.

View this page on GitHub

On this page