Introduction
Resizing fifty photos by hand is an afternoon lost; a script does it in ten seconds. This tool walks a folder, scales every image to a maximum dimension while preserving aspect ratio, optionally converts formats, and reports the space saved. It uses the same Pillow operations from the image processing tutorial — but pointed at automation instead of a UI.
The design goal is safety by default: originals are never touched, output goes to a new folder, and the script tells you exactly what it did.
Features
- Folder-wide processing — every JPG/PNG/WebP in one run.
- Max-dimension scaling — never upscales, always preserves ratio.
- Format conversion — normalize everything to JPG, PNG, or WebP.
- Quality control — trade file size for fidelity.
- Before/after report — count and total size saved.
Prerequisites
- Python 3.8+ — from python.org.
- Dependencies:
pip install pillow
Step 1: Create the Script
Save as bulk_resize.py:
import os
import sys
from PIL import Image
VALID = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
def resize_folder(src, max_dim=1600, out_format=None, quality=85):
out_dir = os.path.join(src, "resized")
os.makedirs(out_dir, exist_ok=True)
files = [f for f in os.listdir(src) if os.path.splitext(f)[1].lower() in VALID]
if not files:
sys.exit("No images found in that folder.")
total_before = total_after = 0
for i, name in enumerate(files, 1):
src_path = os.path.join(src, name)
before = os.path.getsize(src_path)
with Image.open(src_path) as img:
img = img.convert("RGB") if out_format in ("jpg", "webp") else img
img.thumbnail((max_dim, max_dim)) # preserves aspect, never upscales
stem = os.path.splitext(name)[0]
fmt = (out_format or os.path.splitext(name)[1].lstrip(".")).lower()
out_name = f"{stem}.{ 'jpg' if fmt in ('jpeg', 'jpg') else fmt }"
out_path = os.path.join(out_dir, out_name)
save_kwargs = {"quality": quality, "optimize": True} if fmt in ("jpg", "webp") else {}
img.save(out_path, fmt.upper() if fmt != "jpg" else "JPEG", **save_kwargs)
after = os.path.getsize(out_path)
total_before += before
total_after += after
print(f"[{i}/{len(files)}] {name}: {before//1024}KB -> {after//1024}KB")
saved = 100 * (1 - total_after / total_before)
print(f"\nDone: {len(files)} images, {total_before//1024//1024}MB -> {total_after//1024//1024}MB ({saved:.0f}% smaller)")
if __name__ == "__main__":
folder = sys.argv[1] if len(sys.argv) > 1 else "."
resize_folder(folder, max_dim=1600, out_format="jpg", quality=85)
Step 2: Run the Tool
python bulk_resize.py ~/Pictures/camera-dump
Check the resized/ subfolder — originals untouched, copies scaled and converted, with a per-file size report.
How It Works
Image.thumbnail() is the entire resizing strategy: pass it a bounding box and it scales the image to fit within, preserving aspect ratio and never upscaling smaller images. That single method replaces the manual ratio math most tutorials write out — computing scale factors and rounding dimensions is exactly the bug thumbnail exists to prevent. (The image processing tutorial’s resize examples show the manual path; thumbnail is the production shortcut.)
Format conversion rides on one decision: JPEG and WebP can’t store transparency, so images are converted to RGB first — skipping that step is the classic OSError: cannot write mode RGBA as JPEG. The save kwargs differ per format: quality and optimize matter for lossy formats and are ignored by PNG.
The output folder lives inside the source folder, which makes the tool idempotent — running it twice won’t re-process its own output, because resized/ files aren’t in the input scan (they’re in a subdirectory, and os.listdir doesn’t recurse).
Common Errors & Fixes
cannot write mode RGBA as JPEG— transparency;img.convert("RGB")before saving (handled in the code for jpg/webp).OSError: cannot identify image file— a non-image with a valid extension (or a corrupt file); wrap the open in try/except and skip with a warning.- PNG got bigger after resizing — normal for screenshots with flat colors; PNG is lossless, so quality stays but so does size — convert to WebP instead.
- EXIF orientation lost — photos may appear rotated; apply
from PIL import ImageOps; img = ImageOps.exif_transpose(img)after opening.
Key Concepts
thumbnail()— bounded, ratio-preserving, non-upscaling resize.- Mode conversion — RGBA→RGB before lossy formats.
- Idempotent output — writing to a subfolder the input scan ignores.
- Per-format save options — quality applies to lossy, not lossless.
What to Try Next
- Add a watermark pass — paste a logo at fixed opacity before saving.
- Add EXIF stripping for privacy:
img.info = {}before save (or keep it for photos). - Add square-crop mode for e-commerce thumbnails — center-crop after thumbnail.
- Wrap it with the email automation to mail yourself a zipped result.
FAQ
WebP or JPEG for the web?
WebP at quality 80 is typically 25–35% smaller than JPEG at equivalent quality, and every modern browser supports it. JPEG remains the compatibility choice.
Why 1600px maximum?
It covers social media, blog posts, and most screens at 2× DPI. Full-width hero images might want 1920–2400; thumbnails 400–800.
Does resizing lose quality?
Downscaling loses pixels by definition, but at high quality settings the loss is invisible at display size. Upscaling is where quality dies — which is why the tool never does it.