Image conversion: why Rust is 8.5× faster than bash on the same libwebp
A Rust utility for converting between jpg, png and webp — replaces bash + ImageMagick, makes no network calls, runs in single-file mode for embedding into pipelines.
A Rust utility for converting between jpg, png and webp — replaces bash + ImageMagick, makes no network calls, runs in single-file mode for embedding into pipelines.
All hostnames, databases, topics, and credentials in the examples are fictional. Any match with real systems is accidental.
Bash + ImageMagick converts 50 JPGs to WebP in 10.09 seconds — through ~100 processes (one identify per file, one magick per file, plus shell wrappers for grep/sort/rm). The Rust binary does the same work in 1.18 seconds — through 1 process with per-core parallelization. This is not 8.5× magic, it's the absence of ~99 processes' spawn overhead.
fast-image-converter is a self-contained Rust CLI for converting images between jpg, png and webp. It replaces bash + ImageMagick where 100 processes on 50 files stopped being invisible, and it doesn't pretend to solve problems outside its scope — animation, ICC transforms, network sources.
Six pipelines — every combination across the three formats. By default (no flags) it runs jpg → webp.
| Pipeline | Quality honoured |
|---|---|
jpg → webp (default) |
yes |
png → webp |
yes |
webp → jpg |
yes |
png → jpg |
yes |
webp → png (lossless) |
no |
jpg → png (lossless) |
no |
Quality control — --quality <1..100> for lossy pipelines (default 85). Lossless pipelines ignore it.
Resize policies — --resize accepts four shapes:
| Shape | What it does |
|---|---|
none |
no resize |
cap=<W> |
scale the wider side down to ≤ <W> px, keeping aspect ratio |
auto:portrait=<W>,landscape=<H> |
per-orientation caps (v0 baseline: 800/1000) |
fit=<mode> long-edge=<N> |
contain / cover / stretch to a target size |
Two operation modes:
| Mode | What it does |
|---|---|
| Batch | Takes a directory, converts every file in it. By default removes the source after a successful conversion; the --keep-source flag keeps it |
| Single-file | Reads one file from stdin, writes the result to stdout — for embedding into other pipelines |
Minimal example:
# partial — snippet of a CLI invocation, runnable against the built binary.
cat input.jpg | ./target/release/fast-image-converter \
--single-file --output-format webp \
> output.webp
The binary — one self-contained file, ~2.4 MiB stripped, no network calls, no telemetry, no external services. Links against libwebp directly and against MozJPEG statically, so the deploy doesn't depend on system libraries.
Output fidelity — encoded bytes match the libwebp reference encoder within 0.1% tolerance. This is enforced by the golden-batch regression test in the repo (tests/golden_v0.rs), not "close enough".
Full flag list — --help. Full contract (exit codes, channels, edge cases) — docs/integration-contract.md in the repo.
50 files / 3 MB mixed-orientation JPG on a 12-core host with libwebp 1.6.0:
| Pipeline | bash + ImageMagick | fast-image-converter | Speedup |
|---|---|---|---|
jpg → webp (default) |
10.09 s | 1.18 s | 8.5× |
Output bytes match the libwebp reference encoder within 0.1% tolerance — enforced by the golden-batch regression test (tests/golden_v0.rs), not "close enough".
What the numbers depend on:
What the numbers don't depend on: spawn overhead. It's simply gone — and that is the main reason for the speedup.
Re-measure on your host:
time ./target/release/fast-image-converter /path/to/your/jpg-batch
On 500 files and 10k files the numbers will be different, but the order of magnitude holds: one process with parallelization beats a chain of identify + magick per file.
Demo — image-converter.elrise.io: the same binary behind a web wrapper. You can poke at pipelines and resize without building anything.
Repo — github.com/elriseio/fast-image-converter.
What's inside the repo, if you decide to use it:
| Document | Why |
|---|---|
README.md |
Quick start, CLI flags, prereqs, troubleshooting |
docs/integration-contract.md |
Contract for server-side callers: stdin/stdout, exit codes, failure-mode mapping |
docs/RUNBOOK.md |
First-30-minutes triage, build failures, runtime incidents |
docs/architecture.md |
C4-style architecture overview for those who want to understand the internals |
tests/golden_v0.rs |
Golden-batch regression: 10 reference files, re-recorded when libwebp changes |
Build (Rust 1.75+ and libwebp dev headers):
# partial — snippet of CLI commands, runnable on Ubuntu/Debian/Arch/macOS host.
git clone https://github.com/elriseio/fast-image-converter
cd fast-image-converter
cargo build --release
# → target/release/fast-image-converter
What it doesn't do (so you don't waste time evaluating): GIF/TIFF/AVIF (Wave 2 in the Roadmap, not implemented), distributed batch (single-host multi-core), in-process library API (the binary is the only public surface), animated GIF/APNG (first frame only), explicit ICC transforms (passed through image crate defaults).