Featured image of post Python script - Convert images to WebP

Python script - Convert images to WebP

Overview

This Python script is designed to process image files by converting them to the WebP format, organizing them into specific folders, and preserving their metadata. It also handles the renaming of files to remove spaces and ensures that the original metadata is preserved in the converted files.

Usage

To use this script, run it from the command line and provide the following arguments:

  • input_dir: The path to the directory containing the images you want to process.
  • output_dir: The path to the directory where the converted images will be saved.
  • --ext: (Optional) A list of image extensions to process. Default is ["all"].
  • --subdir: (Optional) A flag to indicate whether to search for images recursively in subdirectories. Default is False.

Example command:

python image_processing_script.py /path/to/input /path/to/output --ext jpg png --subdir

Features

  • Converts images to WebP format using the cwebp command.
  • Organizes converted images into folders based on conversion options.
  • Renames files to remove spaces.
  • Preserves metadata from the original images in the converted files using exiftool.
  • Provides logging information with timestamps and log levels.

Functions

  • process_single_image: Converts a single image and writes the markdown link to a file.
  • process_images: Processes all image files in the input directory with the specified extensions.
  • expand_image_extensions: Expands a list of image extensions to include common variants.
  • find_image_files: Finds all image files in the given directory with the specified extensions.
  • create_output_directories: Creates the necessary output directories within the base directory.
  • convert_image_with_cwebp: Converts an image using cwebp and saves it to the output directory.
  • build_converted_path: Builds the path for the converted image file.

Requirements

  • Python 3.x
  • The cwebp command-line tool must be installed and available in the system’s PATH.
  • The exiftool command-line tool must be installed and available in the system’s PATH.

Notes

  • Ensure that you have the necessary permissions to read and write files in the specified directories.
  • Always backup your original images before running the script to prevent data loss.
  • The script assumes that the cwebp and exiftool tools are correctly installed and configured on your system.

Code

import argparse
import subprocess
from pathlib import Path
import logging

# Enable logger, print to console with timestamp and log level
logging.basicConfig(
    level=logging.INFO, format="\n%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


def process_single_image(
    image_path: Path,
    output_dir: Path,
) -> None:
    """
    Processes a single image file: converts it, uploads it, and writes the markdown link to a file.

    Args:
    image_path (Path): The path to the input image file.
    output_dir (Path): The directory where the converted images will be saved.

    Returns:
    None
    """
    folder_options = [
        # ("Lossless", ["-z", "8", "-mt"]),
        # ("6MB", ["-size", "6291456", "-pass", "10", "-qrange", "80", "100", "-mt"]),
        ("Web", ["-resize", "1280", "0", "-mt"]),
    ]

    for folder_name, options in folder_options:
        folder_path = output_dir / folder_name
        create_output_directories(output_dir, folder_name)
        convert_image_with_cwebp(image_path, folder_path, options)


def process_images(
    input_dir: Path,
    output_dir: Path,
    extensions: list[str],
    subdir: bool,
) -> None:
    """
    Processes all image files in the input directory with the specified extensions.

    Args:
    input_dir (Path): The directory containing the input image files.
    output_dir (Path): The directory where the converted images and markdown links will be saved.
    extensions (list[str]): A space-separated list of image extensions.
    subdir (bool): Whether to search recursively in subdirectories.
    md_links_file_name (str): The name of the file where the markdown links will be written.
    """
    image_files = find_image_files(input_dir, extensions, subdir)

    for image_path in image_files:
        image_path.rename(image_path.parent / image_path.name.replace(" ", "-"))
        process_single_image(image_path, output_dir)


def expand_image_extensions(extensions: list[str]) -> list[str]:
    """
    Expands a comma-separated list of image extensions to include common variants.

    Args:
    extensions (list[str]): A list of image extensions.

    Returns:
    list[str]: A list of expanded image extensions.
    """
    supported_extensions = ["jpg", "jpeg", "png", "tif", "tiff", "webp"]

    expanded_extensions = []
    for ext in [s.lower() for s in extensions]:
        if ext == "all":
            expanded_extensions = supported_extensions
            break
        elif ext in ["jpg", "jpeg"]:
            expanded_extensions.extend(["jpg", "jpeg"])
        elif ext in ["tif", "tiff"]:
            expanded_extensions.extend(["tif", "tiff"])
        else:
            expanded_extensions.append(ext)

    return [s.lower() for s in expanded_extensions] + [
        s.upper() for s in expanded_extensions
    ]


def find_image_files(
    directory: Path, extensions: list[str], subdir: bool
) -> list[Path]:
    """
    Finds all image files in the given directory with the specified extensions.

    Args:
    directory (Path): The directory to search for image files.
    extensions (list[str]): A list of image file extensions.
    subdir (bool): Whether to search recursively in subdirectories.

    Returns:
    list[Path]: A list of paths to the found image files.
    """
    return sorted(
        [
            file
            for ext in extensions
            for file in directory.rglob(f"*.{ext}")
            if not file.name.startswith(".")
        ]
        if subdir
        else [
            file
            for ext in extensions
            for file in directory.glob(f"*.{ext}")
            if not file.name.startswith(".")
        ]
    )


def create_output_directories(base_dir: Path, *folder_names: str) -> None:
    """
    Creates the necessary output directories within the base directory.

    Args:
    base_dir (Path): The base directory where the output directories will be created.
    *folder_names (str): Variable number of folder names to create.
    """
    for folder_name in folder_names:
        Path(base_dir, folder_name).mkdir(parents=True, exist_ok=True)


def convert_image_with_cwebp(input_path: Path, output_dir: Path, options: list) -> None:
    """
    Converts an image using cwebp and saves it to the output directory.

    Args:
    input_path (Path): The path to the input image file.
    output_dir (Path): The directory where the converted image will be saved.
    options (list): A list of options to pass to the cwebp command.
    """
    output_path = build_converted_path(input_path, output_dir)
    cweb_cmd = [
        "cwebp",
        *options,
        "-metadata",
        "all",
        str(input_path),
        "-o",
        str(output_path),
    ]
    try:
        subprocess.run(cweb_cmd, check=True)
        subprocess.run(
            [
                "exiftool",
                "-TagsFromFile",
                str(input_path),
                "-overwrite_original",
                str(output_path),
            ],
            check=True,
        )
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to convert {input_path}: {e}")


def build_converted_path(input_path: Path, output_dir: Path) -> Path:
    """
    Builds the path for the converted image file.

    Args:
    input_path (Path): The path to the input image file.
    output_dir (Path): The directory where the converted image will be saved.

    Returns:
    Path: The path to the converted image file.
    """
    return output_dir / (input_path.stem + ".webp")


def main() -> None:
    """
    The main function that parses command line arguments and processes images.
    """
    parser = argparse.ArgumentParser(description="Convert and upload images.")
    parser.add_argument("input_dir", type=str, help="Input directory path")
    parser.add_argument("output_dir", type=str, help="Output directory path")
    parser.add_argument(
        "--ext",
        nargs="+",
        default=["all"],
        help="List of input image extensions",
    )
    parser.add_argument(
        "--subdir",
        action="store_false",
        help="Search for images recursively in subdirectories",
    )

    args = parser.parse_args()

    input_dir = Path(args.input_dir)
    output_dir = Path(args.output_dir)

    process_images(
        input_dir, output_dir, expand_image_extensions(args.ext), args.subdir
    )


if __name__ == "__main__":
    main()
Built with Hugo
Theme Stack designed by Jimmy