Photos taken with a recent phone weigh several megabytes each and have resolutions designed for printing, not for an online catalogue, a technical report or an email attachment. Shrinking them one by one in a graphics program is work best avoided. With Python and the Pillow library you can process an entire folder in a few seconds, producing light, consistent files with no unwanted information.
Installation and script structure
pip install Pillow
The pattern is always the same: list the images in a source folder, process them one at a time and save them to a different destination folder. Originals must never be overwritten: if the chosen quality turns out to be too low, you must be able to start again.
from pathlib import Path
from PIL import Image, ImageOps
source = Path("original_photos")
destination = Path("web_photos")
destination.mkdir(exist_ok=True)
extensions = {".jpg", ".jpeg", ".png", ".tif", ".tiff"}
for file in sorted(source.iterdir()):
if file.suffix.lower() not in extensions:
continue
with Image.open(file) as img:
img = ImageOps.exif_transpose(img)
img.thumbnail((1600, 1600))
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
img.save(destination / f"{file.stem}.jpg", quality=82, optimize=True)
Each line solves a concrete problem, and it is worth looking at them closely.
Keeping the proportions
thumbnail((1600, 1600)) shrinks the image so that neither width nor height exceeds 1600 pixels, keeping the aspect ratio. A 4000×3000 landscape photo becomes 1600×1200; a 3000×4000 portrait becomes 1200×1600. If the image is already smaller, it is not enlarged. That is almost always the behaviour you want.
resize, by contrast, brings the image to exactly the given dimensions and distorts it if the proportions don't match. It is only needed when a precise size is required, and in that case it is better to crop first. ImageOps.fit does both: it crops from the centre and resizes, which is useful for identical square thumbnails.
Photo orientation
Many cameras don't actually rotate the pixels when you shoot in portrait: they save the image in landscape and write a rotation hint in the EXIF metadata. Viewers respect it, but naive processing does not, and the photos come out sideways. ImageOps.exif_transpose applies the indicated rotation to the pixels, so the result is correct everywhere. Forgetting it is the most common mistake in scripts like these.
Quality and format
In practice the JPEG quality parameter ranges from 1 to about 95. Values between 75 and 85 usually offer a good balance between file size and visual quality for photos meant for screens. It is worth testing a few representative images and viewing them at actual size before processing everything.
| Format | Suited to | Notes |
|---|---|---|
| JPEG | Photographs | No transparency, adjustable compression |
| PNG | Graphics, screenshots, logos | Lossless, heavier files for photos |
| WebP | Web images | Supports transparency, files often lighter than JPEG |
JPEG doesn't support transparency: a PNG with an alpha channel must be converted to RGB before saving, otherwise Pillow raises an error. Transparent areas turn black; if you need a white background, paste the image onto a white rectangle before saving. For WebP just change the destination file extension to .webp: Pillow picks the format from the extension.
HEIC photos, common on iPhones, cannot be read by Pillow without an extension: the pillow-heif package adds support.
Metadata: watch out for location
EXIF data can contain much more than orientation: the phone model, the date and time of the shot and, if location was enabled, the GPS coordinates of the place. Publishing a photo of your warehouse or home office online with embedded coordinates is not a good idea. In the JPEG save shown above, Pillow does not write EXIF data unless it is passed explicitly with the exif parameter, so the output images contain none. In my work I still check the result on a sample with a metadata viewer, because it is a check that takes a few seconds.
Names and subfolders
If the images are organised in subfolders, for example one per product, use source.rglob("*") instead of iterdir() and recreate the same structure in the destination:
relative = file.relative_to(source)
output = (destination / relative).with_suffix(".jpg")
output.parent.mkdir(parents=True, exist_ok=True)
Watch out for duplicate names: photo.png and photo.jpg in the same folder would both produce photo.jpg, and the second would overwrite the first. Checking output.exists() before saving prevents the loss.
Speed
For a few hundred images the script takes seconds or minutes, depending on file size. For thousands of photos you can use more than one processor with the concurrent.futures module and a ProcessPoolExecutor, which processes several images in parallel without changing the function's logic.
In short
- Pillow processes whole folders of images without graphics software.
- Use
thumbnailto shrink while keeping proportions andexif_transposefor orientation. - A JPEG quality between 75 and 85 is a good starting point to check by eye.
- Convert to
RGBbefore saving as JPEG and consider WebP for the web. - Check there is no GPS metadata and never overwrite the originals.