Unoptimised images are still the most common reason a page is heavier than it needs to be. Think of a 1 MB PNG hero image, or JPEGs straight off a phone with GPS coordinates in the EXIF. You can fix this one file at a time with an online converter, or once with a script that runs over a whole folder in CI.
SlimImage is that script. It's a single Python file built on Pillow that walks a folder, re-encodes images to WebP, JPEG or PNG, optionally downsizes them, strips metadata, uses several threads, and prints a size report. Everything below was run locally on a folder of test images.
Setup
pip install pillow
python3 slimimage.py --input raw_assets --output optimized_assets --format WEBP --quality 80 --max-width 1600
The input folder:
raw_assets/hero.png 1,062,526 bytes (2400×1350)
raw_assets/banner.png 287,539 bytes
raw_assets/banner.jpg 58,391 bytes
raw_assets/logo.png 3,673 bytes (transparent background)
raw_assets/blog/cover.jpg 128,960 bytes
These are generated test images (blurred shapes, roughly photo-like), not real photos, so treat the percentages as an example rather than a promise. Results depend heavily on the content.
The report
======================================================================
SLIMIMAGE EXECUTIVE REPORT
======================================================================
Assets Processed : 5
Assets Succeeded : 5
Assets Failed : 0
Execution Duration : 0.50 s
Pipeline Throughput : 9.98 assets/sec
----------------------------------------------------------------------
Baseline Total Size : 1.5 MB
Optimized Total Size: 103.8 KB
Absolute Data Saved : 1.4 MB
Compression Yield : 93.1% reduction
----------------------------------------------------------------------
FINANCIAL & SUSTAINABILITY IMPACT (PROJECTED)
Est. Annual Bandwidth Savings: $136.30 USD
Est. Annual Carbon Mitigation: 6.41 kg CO2e
======================================================================
The output:
optimized_assets/hero.webp 37,934
optimized_assets/banner.webp 17,808
optimized_assets/banner.png.webp 16,436
optimized_assets/logo.webp 3,814
optimized_assets/blog/cover.webp 30,290
A few things to notice:
-
Subfolders are kept.
blog/cover.jpgbecameblog/cover.webp, so your relative paths still work after you swap the extension. -
Same name, different format.
banner.jpgandbanner.pngwould both becomebanner.webp. SlimImage keeps both: the first takes the plain name, and the other keeps its original extension in the name (banner.png.webp). -
Transparency survives.
logo.webpis RGBA with a fully transparent corner pixel, just like the PNG. - Small files can grow. The 3.6 KB logo came out at 3.8 KB. That's normal for tiny, flat graphics. Keep the original in those cases.
The "financial" lines are projections, not measurements. They multiply the bytes saved by --monthly-views (default 100,000) × 12 and by --cdn-cost-gb (default $0.085/GB), plus a fixed CO₂-per-GB factor. Set those flags to your real traffic if you want to show the numbers to anyone.
Metadata and orientation
Phone photos often store "rotate 90°" in EXIF instead of rotating the pixels, and they can carry camera model and GPS data. I made a JPEG with an orientation tag, a camera model and a GPS block:
in exif {272: 'SecretCam 9000', 274: 6, 34853: 66}
out exif {} size (300, 400)
SlimImage applies the rotation to the pixels first (the 400×300 input came out as 300×400, correctly upright) and then writes the file with no EXIF. Location and device details don't get published by accident.
Other formats and options
# JPEG for maximum compatibility, max 640 px wide
python3 slimimage.py -i raw_assets -o out_jpeg -f JPEG -q 75 --max-width 640
# lossless WebP for UI screenshots and diagrams
python3 slimimage.py -i screenshots -o out_lossless -f WEBP --lossless
# limit both dimensions, 8 worker threads
python3 slimimage.py -i raw_assets -o out --max-width 1920 --max-height 1080 --threads 8
Resizing keeps the aspect ratio and never upscales. Transparent images converted to JPEG are flattened onto a white background, since JPEG has no alpha channel. Supported inputs are .jpg, .jpeg, .png, .webp, .bmp and .tiff. Other files in the folder are ignored.
Using it in CI
--json prints a machine-readable report instead of the table:
{
"total_processed": 1,
"total_succeeded": 1,
"total_failed": 0,
"original_size_bytes": 2647,
"optimized_size_bytes": 300,
"overall_reduction_percent": 88.67,
"items": [
{"source_path": ".../ex/gps.jpg", "target_path": ".../exo2/gps.webp", "bytes_saved": 2347, "...": "..."}
]
}
A typical step converts assets/raw into public/img on every build and fails if total_failed isn't 0:
python3 slimimage.py -i assets/raw -o public/img -f WEBP -q 80 --max-width 1920 --json > slim.json
python3 -c "import json,sys; sys.exit(json.load(open('slim.json'))['total_failed'] > 0)"
Write output to a separate folder from the input, and keep the originals in version control or storage. Re-encoding is lossy at quality < 100, and you'll want the originals if you change settings later.
Limitations
- Output formats are WebP, JPEG and PNG. There's no AVIF output, and GIF/SVG input isn't supported.
- One format per run. To serve WebP with a JPEG fallback, run it twice into two folders.
- No automatic "keep the original if the result is bigger". Check the per-item
bytes_savedin the JSON report. - It writes optimised files. It doesn't rewrite your HTML/CSS references or generate
srcsetvariants.
FAQ
What quality should I use? 75–85 for photos in WebP is a common range. Compare a few images visually before rolling it out.
Is it fast? It uses a thread pool, and Pillow does the heavy lifting in C. The five files above took half a second on one machine. Yours will differ.
Dependencies? Python 3 and Pillow.
SlimImage is a one-time purchase that includes the source and README: get SlimImage ($29). More details are on the SlimImage product page.
Top comments (0)