Python Snippets

Bulk Image Format Converter with Metadata Preservation

This Python script converts images from one format to another in bulk while preserving EXIF metadata and maintaining quality. It’s particularly useful for photographers, content creators, or anyone needing to process large batches of images efficiently.

import os
from pathlib import Path
from PIL import Image, ExifTags
from typing import List, Tuple
import argparse

def convert_images(input_dir: str, output_dir: str, 
                  input_format: str, output_format: str, 
                  quality: int = 95) -> Tuple[int, int]:
    """
    Convert images from one format to another while preserving EXIF metadata.
    
    Args:
        input_dir: Directory containing source images
        output_dir: Directory to save converted images
        input_format: Source format (e.g., 'jpg', 'png')
        output_format: Target format (e.g., 'webp', 'png')
        quality: Output quality (1-100, for formats that support it)
        
    Returns:
        Tuple of (converted_count, error_count)
    """
    input_path = Path(input_dir)
    output_path = Path(output_dir)
    
    # Create output directory if it doesn't exist
    output_path.mkdir(parents=True, exist_ok=True)
    
    # Get all images with specified input format
    image_files = list(input_path.glob(f"*.{input_format.lower()}"))
    image_files.extend(input_path.glob(f"*.{input_format.upper()}"))
    
    converted = 0
    errors = 0
    
    for img_file in image_files:
        try:
            # Open image and preserve EXIF data
            with Image.open(img_file) as img:
                # Handle transparency for formats that support it
                if img.mode in ("RGBA", "LA") and output_format.lower() in ["jpg", "jpeg"]:
                    # Convert RGBA to RGB for JPEG (which doesn't support transparency)
                    background = Image.new("RGB", img.size, (255, 255, 255))
                    background.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None)
                    img = background
                
                # Prepare output filename
                output_filename = img_file.stem + f".{output_format.lower()}"
                output_file = output_path / output_filename
                
                # Handle EXIF data
                exif_data = img.info.get('exif')
                
                # Save with appropriate parameters
                save_kwargs = {}
                if output_format.lower() in ["jpg", "jpeg", "webp"]:
                    save_kwargs['quality'] = quality
                    save_kwargs['optimize'] = True
                
                if exif_data:
                    save_kwargs['exif'] = exif_data
                    
                # Save converted image
                img.save(output_file, **save_kwargs)
                converted += 1
                print(f"Converted: {img_file.name} -> {output_filename}")
                
        except Exception as e:
            print(f"Error converting {img_file.name}: {str(e)}")
            errors += 1
            
    return converted, errors

def main():
    parser = argparse.ArgumentParser(description="Bulk convert image formats while preserving metadata")
    parser.add_argument("input_dir", help="Input directory containing images")
    parser.add_argument("output_dir", help="Output directory for converted images")
    parser.add_argument("input_format", help="Source image format (jpg, png, etc.)")
    parser.add_argument("output_format", help="Target image format (webp, png, etc.)")
    parser.add_argument("--quality", type=int, default=95, help="Output quality (1-100, default: 95)")
    
    args = parser.parse_args()
    
    if not os.path.exists(args.input_dir):
        print(f"Error: Input directory '{args.input_dir}' does not exist")
        return
    
    converted, errors = convert_images(
        args.input_dir,
        args.output_dir,
        args.input_format,
        args.output_format,
        args.quality
    )
    
    print(f"\nConversion complete!")
    print(f"Successfully converted: {converted}")
    print(f"Errors: {errors}")

if __name__ == "__main__":
    # Example usage without command line arguments:
    # convert_images("./photos", "./webp_photos", "jpg", "webp", 90)
    main()

What This Code Does

This script converts batches of images from one format to another while preserving important metadata like EXIF data (camera settings, GPS coordinates, timestamps). It handles common edge cases like transparency conversion when switching to formats that don’t support it (like converting PNG with transparency to JPEG).

Key features:

Why It’s Useful

Modern web development and content creation often requires converting images to more efficient formats like WebP while maintaining quality and metadata. This tool is especially valuable for:

How to Run It

  1. Install dependencies:
    pip install Pillow
    
  2. Command-line usage:
    python image_converter.py ./input_folder ./output_folder jpg webp --quality 90
    
  3. Programmatic usage (uncomment the example in if __name__ == "__main__"):
    convert_images("./photos", "./webp_photos", "jpg", "webp", 90)
    

The script will process all images in the input directory matching the source format and save converted versions in the output directory, reporting conversion statistics when complete.