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.
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:
- Downscale so the longest edge is at most 2560 px (configurable). Aspect ratio is preserved; smaller images are never upscaled.
- 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.
- 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. |
-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:
- Discover — walk
--source(recursive by default), collect*.jpg,*.jpeg,*.png(case-insensitive). Non-image files are ignored. - Plan output paths — mirror the source tree under
--target, replacing each file's extension with.webp. In single-file mode,--targetis treated as the output file path. - Resize — if the longest edge exceeds
--max-edge, downscale with Lanczos resampling and the aspect ratio preserved. Smaller images pass through untouched. - Normalise mode for the WebP encoder:
- Opaque sources (JPEG, opaque PNG) →
RGB - Transparent sources (RGBA PNG, palette+transparency) →
RGBA
- Opaque sources (JPEG, opaque PNG) →
- Encode with
format="WEBP", quality=<q>, method=6. EXIF is intentionally not carried over. - 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
--qualityto82or80. Below ~75 you'll start seeing artefacts on smooth gradients (skies, skin). - Smaller files for thumbnails or grid layouts: drop
--max-edge.1920is fine for most blogs;1280is great for non-retina layouts. - No-compromise archive copy: raise
--qualityto92and--max-edgeto3200. 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 commit—ruff check --fix,ruff format,pyright - On
git push—pytest
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_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
Acknowledgements
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file image_cooker-0.1.0.tar.gz.
File metadata
- Download URL: image_cooker-0.1.0.tar.gz
- Upload date:
- Size: 37.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37f822c3435537183b54f17bfa5c1e3c8a8c0930d9c60d2670c50ebd256073f3
|
|
| MD5 |
c17de813da763b550bbf4f312e4071e1
|
|
| BLAKE2b-256 |
73b6b9cc23164a3bdab512c362ddaf06cef1c85cce610d48e9ae0e83ffee6562
|
Provenance
The following attestation bundles were made for image_cooker-0.1.0.tar.gz:
Publisher:
release.yml on vsevolodbazhan/image-cooker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
image_cooker-0.1.0.tar.gz -
Subject digest:
37f822c3435537183b54f17bfa5c1e3c8a8c0930d9c60d2670c50ebd256073f3 - Sigstore transparency entry: 1493490561
- Sigstore integration time:
-
Permalink:
vsevolodbazhan/image-cooker@5304b90d11ee0037082c21e0c94eaed25afe67e6 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/vsevolodbazhan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5304b90d11ee0037082c21e0c94eaed25afe67e6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file image_cooker-0.1.0-py3-none-any.whl.
File metadata
- Download URL: image_cooker-0.1.0-py3-none-any.whl
- Upload date:
- Size: 8.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ec8ee3944618e55175f80ca57fda89c03f5c109ef657beae4690dfe6a8cc006
|
|
| MD5 |
70f7e59bb311d41fb0f0cc5f32378d7d
|
|
| BLAKE2b-256 |
66a550df01fdc695aa666bf89a3d481927b3a5dfe5c38e76543b7330b8616e06
|
Provenance
The following attestation bundles were made for image_cooker-0.1.0-py3-none-any.whl:
Publisher:
release.yml on vsevolodbazhan/image-cooker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
image_cooker-0.1.0-py3-none-any.whl -
Subject digest:
5ec8ee3944618e55175f80ca57fda89c03f5c109ef657beae4690dfe6a8cc006 - Sigstore transparency entry: 1493490959
- Sigstore integration time:
-
Permalink:
vsevolodbazhan/image-cooker@5304b90d11ee0037082c21e0c94eaed25afe67e6 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/vsevolodbazhan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5304b90d11ee0037082c21e0c94eaed25afe67e6 -
Trigger Event:
push
-
Statement type: