Bulk update image sizes in a directory
This script takes a specified directory, looks for images based on their file extensions (.png, .jpg, .jpeg, and more), and resizes them to a user-configurable width while maintaining their aspect ratio. The script then saves them to a target directory, rather than overwriting the original file, so you can confirm everything looks good.
Images already at or below the target width are copied unchanged, as are animated GIFs and WebPs, so the output folder is always a complete set. Resized photos are rotated upright according to their EXIF orientation and keep their color profile, but no other EXIF metadata is carried over, which also strips location data. Copied images are byte-for-byte originals, so they keep all of theirs.
Prerequisites
Aside from having Python 3 installed, this script requires the Pillow library, which is a fork of PIL (Python Imaging Library). Install it with pip, using the same Python you’ll run the script with:
python3 -m pip install Pillow
Script
import argparse
import os
import shutil
from PIL import Image, ImageOps
EXTENSIONS = (".png", ".jpg", ".jpeg", ".gif", ".webp")
def resize_images(input_directory, output_directory, new_width, quality=85):
"""
Resize images wider than new_width to new_width, keeping the aspect ratio.
Images that don't need resizing are copied unchanged, so the output
directory always contains a complete set.
"""
os.makedirs(output_directory, exist_ok=True)
for filename in sorted(os.listdir(input_directory)):
if not filename.lower().endswith(EXTENSIONS):
continue
input_path = os.path.join(input_directory, filename)
output_path = os.path.join(output_directory, filename)
with Image.open(input_path) as img:
if getattr(img, "is_animated", False):
shutil.copy2(input_path, output_path)
print(f"Copied (animated): {filename}")
continue
oriented = ImageOps.exif_transpose(img)
if oriented.width <= new_width:
shutil.copy2(input_path, output_path)
print(f"Copied: {filename}")
continue
new_height = round(oriented.height * new_width / oriented.width)
resized = oriented.resize((new_width, new_height), Image.Resampling.LANCZOS)
save_options = {}
icc_profile = img.info.get("icc_profile")
if icc_profile:
save_options["icc_profile"] = icc_profile
if filename.lower().endswith((".jpg", ".jpeg", ".webp")):
save_options["quality"] = quality
if filename.lower().endswith((".jpg", ".jpeg")):
save_options["optimize"] = True
resized.save(output_path, **save_options)
print(f"Resized: {filename}")
print("Processing completed.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Resize images wider than a given width, keeping aspect ratio. "
"Smaller images are copied unchanged."
)
parser.add_argument("input_directory", help="Directory containing the images.")
parser.add_argument("output_directory", help="Directory to save resized or copied images.")
parser.add_argument("new_width", type=int, help="Maximum width in pixels.")
parser.add_argument("--quality", type=int, default=85, help="JPEG/WebP quality, 1-95 (default 85).")
args = parser.parse_args()
resize_images(args.input_directory, args.output_directory, args.new_width, args.quality)
Usage
The command takes three parameters, plus one optional flag:
input_directory: Directory containing the images to resize.output_directory: Directory where resized and copied images will be saved.new_width: Maximum width for the images, in pixels.--quality(optional): JPEG and WebP output quality, from 1 to 95. Defaults to 85.
python3 resize_images.py path/to/input/directory path/to/output/directory 800
python3 resize_images.py path/to/input/directory path/to/output/directory 800 --quality 90
This script can be easily adjusted to support other image file extensions supported by the Python Imaging Library. Reach out if you have any questions or recommendations for improvements.
-
Updated October 2026: images that don’t need resizing are now copied instead of skipped, so the output folder is complete; animated images are copied rather than flattened; photos keep their orientation and color profile; there’s a new
--qualityflag; the script is indented with spaces so it pastes cleanly; and install instructions now use pip. ↩︎