Skip to content

Instantly share code, notes, and snippets.

@benlacey57
Last active May 19, 2025 20:14
Show Gist options
  • Select an option

  • Save benlacey57/816f562d3693358808627c1be9254116 to your computer and use it in GitHub Desktop.

Select an option

Save benlacey57/816f562d3693358808627c1be9254116 to your computer and use it in GitHub Desktop.
Python script to resize and compress images
# Image Compression and Conversion Script
# Author: Ben Lacey
# Version: 1.1
# Website: benlacey.co.uk
# Changes made:
# 1. Added a new `--rename` argument to accept a template string
# 2. Created a `generate_filename_from_template` function to handle template substitutions
# 3. Updated the `process_image` function to include counter and use the template
# 4. Added logging to show renamed filenames
import os
import sys
from datetime import datetime
from PIL import Image
import zipfile
import shutil
def process_images(output_dir, max_width, max_height, quality=70):
"""
Process images from the current directory by resizing and compressing them.
Args:
output_dir (str): Directory to save processed images
max_width (int): Maximum width of output images
max_height (int): Maximum height of output images
quality (int): JPEG compression quality (1-100)
"""
# Get current directory
current_dir = os.getcwd()
# Create output directory if it doesn't exist
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Get list of image files in current directory
valid_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']
image_files = []
for filename in os.listdir(current_dir):
if os.path.isfile(filename): # Make sure it's a file, not a directory
ext = os.path.splitext(filename)[1].lower()
if ext in valid_extensions:
image_files.append(filename)
if not image_files:
print("No image files found in the current directory.")
return []
results = []
# Process each image
for filename in image_files:
input_path = os.path.join(current_dir, filename)
# Skip the output directory itself if it's in the current directory
if os.path.abspath(input_path) == os.path.abspath(output_dir):
continue
# Get original file size
original_size = os.path.getsize(input_path)
# Open and process the image
try:
with Image.open(input_path) as img:
# Convert to RGB if image has an alpha channel (for saving as JPEG)
if img.mode == 'RGBA':
img = img.convert('RGB')
# Calculate new dimensions while maintaining aspect ratio
width, height = img.size
if width > max_width or height > max_height:
# Calculate aspect ratio
aspect_ratio = width / height
if width > height:
new_width = min(width, max_width)
new_height = int(new_width / aspect_ratio)
# Check if height still exceeds max_height
if new_height > max_height:
new_height = max_height
new_width = int(new_height * aspect_ratio)
else:
new_height = min(height, max_height)
new_width = int(new_height * aspect_ratio)
# Check if width still exceeds max_width
if new_width > max_width:
new_width = max_width
new_height = int(new_width / aspect_ratio)
# Resize image
img = img.resize((new_width, new_height), Image.LANCZOS)
# Save with new name and reduced quality
name, ext = os.path.splitext(filename)
output_filename = f"{name}{ext.lower()}"
output_path = os.path.join(output_dir, output_filename)
# Save as JPEG with quality setting
img.save(output_path, 'JPEG', quality=quality, optimize=True)
# Get new file size
new_size = os.path.getsize(output_path)
# Calculate size reduction percentage
size_reduction = (1 - (new_size / original_size)) * 100
result = {
'filename': filename,
'original_size': original_size,
'new_size': new_size,
'size_reduction_percent': size_reduction,
'output_path': output_path
}
results.append(result)
print(f"Processed: {filename}")
print(f" Original size: {original_size / 1024:.2f} KB")
print(f" New size: {new_size / 1024:.2f} KB")
print(f" Reduction: {size_reduction:.2f}%")
except Exception as e:
print(f"Error processing {filename}: {e}")
return results
def create_zip(file_paths, zip_name):
"""Create a zip file containing the processed images"""
with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file_path in file_paths:
zipf.write(file_path, os.path.basename(file_path))
print(f"Created zip archive: {zip_name}")
print(f"Zip size: {os.path.getsize(zip_name) / 1024:.2f} KB")
def main():
# Default parameters
output_dir = "output"
max_width = 1200
max_height = 1200
quality = 70
# Check if we're in a Jupyter notebook by looking at sys.argv[0]
in_jupyter = False
if len(sys.argv) > 0:
if 'jupyter' in sys.argv[0].lower() or '.json' in sys.argv[0].lower():
in_jupyter = True
# Only use command line args if we're not in Jupyter
if not in_jupyter and len(sys.argv) > 1:
try:
output_dir = sys.argv[1]
if len(sys.argv) > 2:
max_width = int(sys.argv[2])
if len(sys.argv) > 3:
max_height = int(sys.argv[3])
if len(sys.argv) > 4:
quality = int(sys.argv[4])
except ValueError as e:
print(f"Error parsing command line arguments: {e}")
print("Using default values instead.")
# Process images
print(f"Processing images from current directory to '{output_dir}'")
print(f"Max dimensions: {max_width}x{max_height}, Quality: {quality}%")
results = process_images(output_dir, max_width, max_height, quality)
if results:
# Create zip file with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
zip_name = f"compressed-images-{timestamp}.zip"
output_files = [result['output_path'] for result in results]
create_zip(output_files, zip_name)
# Print summary
total_original = sum(result['original_size'] for result in results)
total_new = sum(result['new_size'] for result in results)
total_reduction = (1 - (total_new / total_original)) * 100 if total_original > 0 else 0
print("\nSummary:")
print(f"Processed {len(results)} images")
print(f"Total original size: {total_original / 1024:.2f} KB")
print(f"Total new size: {total_new / 1024:.2f} KB")
print(f"Total reduction: {total_reduction:.2f}%")
if __name__ == "__main__":
main()import os
import sys
import argparse
import re
from datetime import datetime
from PIL import Image, ImageFilter, ImageEnhance
import zipfile
import shutil
import logging
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("image_processor.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def parse_arguments():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(description='Process and compress images.')
parser.add_argument('--output', '-o', type=str, default="output",
help='Output directory for processed images')
parser.add_argument('--width', '-w', type=int, default=1200,
help='Maximum width of output images')
parser.add_argument('--height', '-ht', type=int, default=1200,
help='Maximum height of output images')
parser.add_argument('--quality', '-q', type=int, default=70,
help='JPEG quality (1-100)')
parser.add_argument('--format', '-f', type=str, default="JPEG",
choices=['JPEG', 'PNG', 'WEBP'],
help='Output format')
parser.add_argument('--sharpness', '-s', type=float, default=1.0,
help='Sharpness enhancement factor (0.0-2.0)')
parser.add_argument('--contrast', '-c', type=float, default=1.0,
help='Contrast enhancement factor (0.0-2.0)')
parser.add_argument('--brightness', '-b', type=float, default=1.0,
help='Brightness enhancement factor (0.0-2.0)')
parser.add_argument('--filter', type=str, default=None,
choices=['BLUR', 'SHARPEN', 'SMOOTH', 'DETAIL', 'EDGE_ENHANCE'],
help='Apply filter to images')
parser.add_argument('--recursive', '-r', action='store_true',
help='Process images in subdirectories')
parser.add_argument('--workers', type=int, default=4,
help='Number of worker threads')
parser.add_argument('--skip-zip', action='store_true',
help='Skip creating ZIP archive')
# New argument for rename template
parser.add_argument('--rename', type=str, default=None,
help='Rename template (use {original}, {width}, {height}, {date}, {counter}, {extension})')
return parser.parse_args()
def get_image_files(directory, recursive=False):
"""Get list of image files in directory"""
valid_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tiff', '.tif']
image_files = []
if recursive:
for root, _, files in os.walk(directory):
for filename in files:
if os.path.splitext(filename)[1].lower() in valid_extensions:
image_files.append(os.path.join(root, filename))
else:
for filename in os.listdir(directory):
if os.path.isfile(os.path.join(directory, filename)):
if os.path.splitext(filename)[1].lower() in valid_extensions:
image_files.append(os.path.join(directory, filename))
return image_files
def generate_filename_from_template(template, original_filename, img_width, img_height, counter, format_type):
"""Generate a filename based on the provided template"""
if not template:
name, _ = os.path.splitext(original_filename)
return f"{name}"
# Get extension based on format
if format_type == 'JPEG':
extension = 'jpg'
elif format_type == 'PNG':
extension = 'png'
elif format_type == 'WEBP':
extension = 'webp'
else:
extension = os.path.splitext(original_filename)[1].lstrip('.').lower()
# Get original name without extension
original_name = os.path.splitext(os.path.basename(original_filename))[0]
# Replace placeholders
result = template
result = result.replace('{original}', original_name)
result = result.replace('{width}', str(img_width))
result = result.replace('{height}', str(img_height))
result = result.replace('{date}', datetime.now().strftime('%Y%m%d'))
result = result.replace('{counter}', f"{counter:04d}")
result = result.replace('{extension}', extension)
# Replace any invalid characters for filenames
result = re.sub(r'[\\/*?:"<>|]', '_', result)
return result
def process_image(args):
"""Process a single image"""
filepath, output_dir, max_width, max_height, quality, format_type, \
sharpness, contrast, brightness, filter_type, rename_template, counter = args
try:
# Get original file size
original_size = os.path.getsize(filepath)
filename = os.path.basename(filepath)
# Determine output path, keeping directory structure for recursive mode
rel_path = os.path.dirname(filepath).replace(os.getcwd(), '').lstrip(os.sep)
if rel_path:
output_subdir = os.path.join(output_dir, rel_path)
if not os.path.exists(output_subdir):
os.makedirs(output_subdir)
else:
output_subdir = output_dir
# Open and process the image
with Image.open(filepath) as img:
# Convert image mode if needed
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGB')
# Handle EXIF orientation
try:
exif = img._getexif()
if exif:
orientation_key = 274 # EXIF orientation tag
if orientation_key in exif:
orientation = exif[orientation_key]
if orientation == 2:
img = img.transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 3:
img = img.transpose(Image.ROTATE_180)
elif orientation == 4:
img = img.transpose(Image.FLIP_TOP_BOTTOM)
elif orientation == 5:
img = img.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_90)
elif orientation == 6:
img = img.transpose(Image.ROTATE_270)
elif orientation == 7:
img = img.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_270)
elif orientation == 8:
img = img.transpose(Image.ROTATE_90)
except (AttributeError, KeyError, IndexError):
# No EXIF or orientation data
pass
# Calculate new dimensions while maintaining aspect ratio
width, height = img.size
if width > max_width or height > max_height:
# Calculate aspect ratio
aspect_ratio = width / height
if width > height:
new_width = min(width, max_width)
new_height = int(new_width / aspect_ratio)
# Check if height still exceeds max_height
if new_height > max_height:
new_height = max_height
new_width = int(new_height * aspect_ratio)
else:
new_height = min(height, max_height)
new_width = int(new_height * aspect_ratio)
# Check if width still exceeds max_width
if new_width > max_width:
new_width = max_width
new_height = int(new_width / aspect_ratio)
# Resize image with high-quality resampling
img = img.resize((new_width, new_height), Image.LANCZOS)
# Apply filters if specified
if filter_type:
if filter_type == 'BLUR':
img = img.filter(ImageFilter.BLUR)
elif filter_type == 'SHARPEN':
img = img.filter(ImageFilter.SHARPEN)
elif filter_type == 'SMOOTH':
img = img.filter(ImageFilter.SMOOTH)
elif filter_type == 'DETAIL':
img = img.filter(ImageFilter.DETAIL)
elif filter_type == 'EDGE_ENHANCE':
img = img.filter(ImageFilter.EDGE_ENHANCE)
# Apply enhancements
if sharpness != 1.0:
enhancer = ImageEnhance.Sharpness(img)
img = enhancer.enhance(sharpness)
if contrast != 1.0:
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(contrast)
if brightness != 1.0:
enhancer = ImageEnhance.Brightness(img)
img = enhancer.enhance(brightness)
# Generate new filename from template
base_name = generate_filename_from_template(
rename_template, filename, width, height, counter, format_type
)
# Determine file extension based on format
if format_type == 'JPEG':
output_ext = '.jpg'
elif format_type == 'PNG':
output_ext = '.png'
elif format_type == 'WEBP':
output_ext = '.webp'
else:
output_ext = os.path.splitext(filename)[1].lower()
output_filename = f"{base_name}{output_ext}"
output_path = os.path.join(output_subdir, output_filename)
# Save with appropriate settings
if format_type == 'JPEG':
img.save(output_path, format_type, quality=quality, optimize=True, progressive=True)
elif format_type == 'PNG':
img.save(output_path, format_type, optimize=True)
elif format_type == 'WEBP':
img.save(output_path, format_type, quality=quality, method=6)
else:
img.save(output_path, quality=quality, optimize=True)
# Get new file size
new_size = os.path.getsize(output_path)
# Calculate size reduction percentage
size_reduction = (1 - (new_size / original_size)) * 100 if original_size > 0 else 0
return {
'filename': filename,
'new_filename': output_filename,
'original_size': original_size,
'new_size': new_size,
'size_reduction_percent': size_reduction,
'output_path': output_path,
'width': width,
'height': height,
'new_width': img.width,
'new_height': img.height
}
except Exception as e:
logger.error(f"Error processing {filepath}: {e}")
return None
def create_zip(file_paths, zip_name):
"""Create a zip file containing the processed images"""
try:
with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file_path in tqdm(file_paths, desc="Creating ZIP archive"):
arcname = os.path.relpath(file_path, os.path.dirname(zip_name))
zipf.write(file_path, arcname)
logger.info(f"Created zip archive: {zip_name}")
logger.info(f"Zip size: {os.path.getsize(zip_name) / 1024:.2f} KB")
except Exception as e:
logger.error(f"Error creating ZIP file: {e}")
def main():
# Get arguments
args = parse_arguments()
# Current directory
current_dir = os.getcwd()
# Create output directory if it doesn't exist
if not os.path.exists(args.output):
os.makedirs(args.output)
# Get list of image files
image_files = get_image_files(current_dir, args.recursive)
if not image_files:
logger.warning("No image files found in the current directory.")
return
logger.info(f"Found {len(image_files)} image files to process")
logger.info(f"Processing images to '{args.output}'")
logger.info(f"Max dimensions: {args.width}x{args.height}, Quality: {args.quality}%")
if args.rename:
logger.info(f"Using rename template: {args.rename}")
# Prepare arguments for each image
process_args = [
(
file, args.output, args.width, args.height, args.quality,
args.format, args.sharpness, args.contrast, args.brightness,
args.filter, args.rename, i+1
)
for i, file in enumerate(image_files)
]
# Process images in parallel
results = []
with ThreadPoolExecutor(max_workers=args.workers) as executor:
for result in tqdm(
executor.map(process_image, process_args),
total=len(image_files),
desc="Processing images"
):
if result:
results.append(result)
logger.info(f"Processed: {result['filename']}")
if args.rename:
logger.info(f" Renamed to: {result['new_filename']}")
logger.info(f" Original size: {result['original_size'] / 1024:.2f} KB")
logger.info(f" New size: {result['new_size'] / 1024:.2f} KB")
logger.info(f" Reduction: {result['size_reduction_percent']:.2f}%")
logger.info(f" Dimensions: {result['width']}x{result['height']} → {result['new_width']}x{result['new_height']}")
if results:
# Create zip file if not skipped
if not args.skip_zip:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
zip_name = f"compressed-images-{timestamp}.zip"
output_files = [result['output_path'] for result in results]
create_zip(output_files, zip_name)
# Print summary
total_original = sum(result['original_size'] for result in results)
total_new = sum(result['new_size'] for result in results)
total_reduction = (1 - (total_new / total_original)) * 100 if total_original > 0 else 0
logger.info("\nSummary:")
logger.info(f"Processed {len(results)} images")
logger.info(f"Total original size: {total_original / (1024 * 1024):.2f} MB")
logger.info(f"Total new size: {total_new / (1024 * 1024):.2f} MB")
logger.info(f"Total reduction: {total_reduction:.2f}%")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment