Skip to main content

CLI that prepares web-optimized images: downscales then converts to WebP.

Project description

image-cooker

Bake your blog photos. Downscale once, re-encode as WebP, ship to the web.

CI Python License Built with Typer Image library

image-cooker is a small, opinionated Python CLI that takes the giant JPEG/PNG files your camera produces and turns them into web-optimised WebP images that don't sink your Lighthouse score.

It does one thing — and it does it well: shrink, then re-encode.


Why

Modern cameras output 4000×6000-pixel JPEGs at 5–15 MB each. Browsers ignore the DPI metadata embedded in those files; the only things that matter for web performance are the pixel dimensions and the codec efficiency. So image-cooker only attacks those two levers:

  1. Downscale so the longest edge is at most 2560 px (configurable). Aspect ratio is preserved; smaller images are never upscaled.
  2. Re-encode as WebP at quality 85 (configurable), with EXIF stripped to keep files small and avoid leaking camera metadata.

A real-world photo blog post dropped from 140 MB to 47 MB (-66%) with no perceptible quality loss, just by running the defaults.


Features

  • Single binary CLI powered by Typer — rich --help, validated input, sensible exit codes.
  • JPEG and PNG in, WebP out. Alpha channel preserved automatically for transparent PNGs.
  • Mirrors directory trees. Point it at ~/Pictures/blog/ and get back the same structure under your target folder, every file as .webp.
  • Single-file mode when you just need to cook one photo.
  • Recursive or shallow. Toggle with --recursive / --no-recursive.
  • Lanczos resampling for the downscale step — the same algorithm professional image tools use.
  • Per-file resilience. A bad file is logged and skipped; the rest of the batch keeps going. The process exits non-zero if anything failed.
  • Parallel by default. Uses every CPU core to fan out the batch via a process pool. Tunable with --jobs N, or set to 1 for sequential.
  • Zero hidden state. No config files, no caches, no surprise network calls. What you pass on the command line is what you get.

Quick start

uv run image-cooker --source ~/Pictures/blog --target ./optimised

Output:

~/Pictures/blog/IMG_0001.jpg -> ./optimised/IMG_0001.webp  3888x5175 -> 1923x2560  4.2 MB -> 1.4 MB (-66.7%)
~/Pictures/blog/IMG_0002.jpg -> ./optimised/IMG_0002.webp  6000x4000 -> 2560x1707  6.1 MB -> 2.0 MB (-67.2%)
...

42 converted, 0 failed. Total: 140.0 MB -> 47.0 MB (-66.4%)

Installation

From source (current)

git clone https://github.com/vsevolodbazhan/image-cooker.git
cd image-cooker
uv sync

Then run via uv run image-cooker ... or activate the venv and use the image-cooker console script directly.

Requirements

  • Python 3.10+
  • uv (recommended), or any PEP 517–compatible installer (pip, pipx, hatch, …)

Usage

Usage: image-cooker [OPTIONS]

  Prepare web-optimized WebP images from JPEG/PNG sources.
Option Default Description
--source PATH (required) Source file or directory containing JPEG/PNG images.
--target PATH (required) Output file (when source is a file) or directory (when source is a directory).
--max-edge INT 2560 Cap on the longest edge in pixels. Aspect ratio is preserved; smaller images are not upscaled.
--quality INT 85 WebP encoder quality, 1–100.
--recursive / --no-recursive recursive Recurse into subdirectories when source is a directory.
--jobs INT, -j INT host CPU count Number of worker processes. 1 disables parallelism.
-h, --help Show the help message and exit.

Examples

Optimise a whole blog photo folder, recursively:

uv run image-cooker --source ~/Pictures/blog --target ~/Sites/cooked

Cook a single hero image with custom settings:

uv run image-cooker \
  --source hero.jpg \
  --target hero.webp \
  --max-edge 1920 \
  --quality 88

Build a smaller thumbnail set in a sibling directory:

uv run image-cooker \
  --source ~/Pictures/blog \
  --target ./thumbs \
  --max-edge 960 \
  --quality 75

Process only the top level (no recursion):

uv run image-cooker --source ./photos --target ./out --no-recursive

How it works

The pipeline is deliberately small and easy to reason about:

  1. Discover — walk --source (recursive by default), collect *.jpg, *.jpeg, *.png (case-insensitive). Non-image files are ignored.
  2. Plan output paths — mirror the source tree under --target, replacing each file's extension with .webp. In single-file mode, --target is treated as the output file path.
  3. Resize — if the longest edge exceeds --max-edge, downscale with Lanczos resampling and the aspect ratio preserved. Smaller images pass through untouched.
  4. Normalise mode for the WebP encoder:
    • Opaque sources (JPEG, opaque PNG) → RGB
    • Transparent sources (RGBA PNG, palette+transparency) → RGBA
  5. Encode with format="WEBP", quality=<q>, method=6. EXIF is intentionally not carried over.
  6. Report — one line per file plus a final summary.

That's it. There is no caching, no parallelism, no hidden config — the code is short enough to read in a sitting.


Tuning guide

The defaults (--max-edge 2560, --quality 85) target high-quality photo blogs on retina displays. If you want to push further:

  • Smaller files, same look: drop --quality to 82 or 80. Below ~75 you'll start seeing artefacts on smooth gradients (skies, skin).
  • Smaller files for thumbnails or grid layouts: drop --max-edge. 1920 is fine for most blogs; 1280 is great for non-retina layouts.
  • No-compromise archive copy: raise --quality to 92 and --max-edge to 3200. Files get noticeably bigger but quality is effectively transparent.

Development

git clone https://github.com/vsevolodbazhan/image-cooker.git
cd image-cooker
uv sync
uv run pre-commit install   # one-time: wire up commit + push hooks
uv run pytest

Quality gates run automatically:

  • On git commitruff check --fix, ruff format, pyright
  • On git pushpytest

To run them manually: uv run pre-commit run --all-files.

Project layout:

image-cooker/
├── pyproject.toml              # PEP 621 + PEP 735 dep groups
├── uv.lock                     # reproducible env
├── src/image_cooker/
│   ├── __init__.py
│   ├── __main__.py             # python -m image_cooker
│   ├── cli.py                  # Typer command, orchestration
│   └── processor.py            # discovery + image transform
└── tests/
    ├── test_cli.py             # CLI integration tests
    └── test_processor.py       # pipeline unit tests

The whole project is intentionally tiny — fewer than 250 lines of Python — and fully covered by pytest. Contributions welcome.


License

MIT


Acknowledgements

  • Pillow for the imaging primitives
  • Typer for the delightful CLI
  • uv for fast, reproducible Python envs

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

image_cooker-0.2.0.tar.gz (37.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

image_cooker-0.2.0-py3-none-any.whl (8.5 kB view details)

Uploaded Python 3

File details

Details for the file image_cooker-0.2.0.tar.gz.

File metadata

  • Download URL: image_cooker-0.2.0.tar.gz
  • Upload date:
  • Size: 37.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for image_cooker-0.2.0.tar.gz
Algorithm Hash digest
SHA256 0495f8ba962c2c65d82efe565309bb322478bbf21497abb1cba29863951e314f
MD5 c0d27f29513bda48159dee705741ef33
BLAKE2b-256 b7157206906cb770e4eed8fdc1704a9fd66a7170e2a1fc41e80c8f11bd208df3

See more details on using hashes here.

Provenance

The following attestation bundles were made for image_cooker-0.2.0.tar.gz:

Publisher: release.yml on vsevolodbazhan/image-cooker

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file image_cooker-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: image_cooker-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 8.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for image_cooker-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 db29ac3efbf167652530396d2694ca4aadf0729ebff1d65bca0ff095451d1fd7
MD5 782c968b0e4d4239e4a6a2360f7d2af6
BLAKE2b-256 7dfc8dc4d2555afa7be00c1b4bb719b16efd5ede69b2906e0cfb939686db33bd

See more details on using hashes here.

Provenance

The following attestation bundles were made for image_cooker-0.2.0-py3-none-any.whl:

Publisher: release.yml on vsevolodbazhan/image-cooker

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page