DEV Community

Alessio
Alessio

Posted on Edited on Originally published at py-forge.com

Batch-Convert Images to WebP with Python: Resize, Strip EXIF and Keep Transparency

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
======================================================================
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A few things to notice:

  • Subfolders are kept. blog/cover.jpg became blog/cover.webp, so your relative paths still work after you swap the extension.
  • Same name, different format. banner.jpg and banner.png would both become banner.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.webp is 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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, "...": "..."}
  ]
}
Enter fullscreen mode Exit fullscreen mode

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)"
Enter fullscreen mode Exit fullscreen mode

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_saved in the JSON report.
  • It writes optimised files. It doesn't rewrite your HTML/CSS references or generate srcset variants.

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)