I've been building Convertify a free image converter for 11 weeks now. Last week I added compression support (JPG, PNG, WebP) using Rust + libvips. Here's what I learned, with actual numbers.
The Stack
- Backend: Rust + Axum
-
Image processing: libvips 8.15.1 via the
libvipscrate - JPEG encoder: libjpeg (bundled with libvips)
- PNG quantization: imagequant (same algorithm as pngquant / TinyPNG)
- PNG parser: libspng (faster than libpng)
One thing worth knowing upfront: libvips does not include mozjpeg by default. You get standard libjpeg. I'll come back to why this matters.
How the API Works
The compression endpoint is the same /api/upload used for conversion — just pass format_to=jpg (or png, webp) with a quality parameter (1–100):
curl -X POST https://convertifyapp.net/api/upload \
-F "files=@photo.jpg" \
-F "format_to=jpg" \
-F "quality=80"
On the Rust side, quality flows into libvips save options via build_output_path, which appends [Q=N] to the output filename — libvips picks it up automatically:
fn build_output_path(base: &str, format: &str, quality: Option<u32>) -> String {
match (format, quality) {
("jpg" | "jpeg" | "webp", Some(q)) => format!("{}[Q={}]", base, q),
("png", Some(q)) => format!("{}[Q={}]", base, q),
_ => base.to_string(),
}
}
Clean. No separate code path for compression vs conversion.
The Benchmarks
Test image: 1920×1080 synthetic photo, 1.5 MB original JPG (Q=95 source).
JPG → JPG (re-compression)
| Quality | Output size | Saved |
|---|---|---|
| Q=90 | 1,172 KB | 22.7% |
| Q=80 | 890 KB | 41.3% |
| Q=70 | 737 KB | 51.4% |
| Q=60 | 627 KB | 58.6% |
| Q=50 | 542 KB | 64.2% |
PNG → PNG (imagequant lossy)
Test image: 1024×768, 678 KB original PNG.
| Quality | Output size | Saved |
|---|---|---|
| Q=90 | 486 KB | 29.8% |
| Q=80 | 441 KB | 36.2% |
| Q=70 | 396 KB | 42.7% |
| Q=60 | 377 KB | 45.5% |
| Q=50 | 348 KB | 49.7% |
JPG → WebP (format switch as compression)
| Quality | Output size | Saved vs original JPG |
|---|---|---|
| Q=90 | 1,120 KB | 26.1% |
| Q=80 | 872 KB | 42.5% |
| Q=70 | 760 KB | 49.9% |
The Surprising Part
WebP at Q=80 saves almost the same as JPG at Q=80 — 42.5% vs 41.3%. Not the dramatic 2× improvement you'd expect from the marketing.
Why? On already-complex photographic content (lots of high-frequency detail), the gap between WebP and JPEG narrows significantly. WebP's advantage is most visible on graphics, flat colors, and images with transparency.
The other surprise: if the source JPG is already compressed at Q=70–75 (common for web photos), re-compressing to the same format gives you almost nothing. You're fighting the JPEG DCT artifacts that are already baked in. This is where converting to WebP actually helps different codec, fresh encode.
Why Not mozjpeg?
mozjpeg typically gives 10–15% better compression than libjpeg at the same quality level. To use it with libvips you need to compile libvips from source with --with-mozjpeg. On Ubuntu:
# Not trivial requires building mozjpeg first
git clone https://github.com/mozilla/mozjpeg.git
cd mozjpeg && cmake -G"Unix Makefiles" && make install
# Then rebuild libvips pointing at mozjpeg
./configure --with-jpeg-includes=/opt/mozjpeg/include \
--with-jpeg-libraries=/opt/mozjpeg/lib
For a production VPS running PM2 + Caddy with system libvips, the operational complexity wasn't worth it. libjpeg at Q=80 already delivers 40%+ savings which covers 90% of use cases.
PNG: imagequant is the real hero
The selected quantisation package: imagequant line in vips --vips-config is significant. This is the same engine behind pngquant and TinyPNG's PNG compression. It works by reducing the color palette to 256 colors (lossy) while preserving perceptual quality.
The Q parameter maps to imagequant's quality range. Q=80 gives ~36% savings with barely visible quality loss on photographic PNGs. For logos and flat graphics the savings are even better since there are fewer unique colors to begin with.
The Sweet Spot
After running these benchmarks, the defaults I settled on for the UI:
- JPG: Q=82 (hits the knee of the quality/size curve, ~40% savings)
- PNG: Q=80 (36% savings, imagequant artifacts not visible at normal viewing)
- WebP: Q=80 (consistent with JPG behavior)
These are close to what Squoosh and Lighthouse recommend. The difference is users can move the slider themselves the before/after size display makes the tradeoff tangible.
What's Next
The compression cluster is live at convertifyapp.net/compress/jpg. Next up: RAW format support (CR2/NEF) for photographers, and eventually mozjpeg as an opt-in for maximum compression.
If you're building something similar libvips in Rust is genuinely great. The libvips crate is well-maintained, the C library is fast (faster than ImageMagick by a wide margin), and imagequant bundled for PNG is a nice bonus.
Building in public week 11 of 52. Follow along if you're into indie dev + Rust + SEO experiments.
Top comments (0)