These are several comments regarding you question:
- Use function parameters instead of global variables.
- Use
pathlib
instead of os
.
- Configuration (
img_folder
, target_extension
and desired_extension
) should not be contained within code. That makes it necessary to modify the code if configuration changes. Instead, you could extract it to a file or command line arguments.
- Do not
sleep
. As a rule of thumb, if you are using the sleep
function, you are probably doing something wrong. In this case, it is adding an unnecessary wait for the user.
- Core logic/business logic should not handle data presentation (
print
in copy_files_per_dim
)
- Call
main
with a if __name__ == "__main__"
clause. This allows your code to be reused elsewhere.
- I would make the deletion of the previously existent files optional. The user of the script may not expect to have their files deleted.
- Use docstrings instead of comments for your functions.
- Use type hints
- Use
logging
instead of print
This is the code I propose:
import logging
from typing import Iterable, NamedTuple
from pathlib import Path
from PIL import Image
# Logging configuration could be extracted to a file, but it is kept here for simplicity of the example
logging.basicConfig(format="[%(levelname)s] - %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
class Dimension(NamedTuple):
width: int
height: int
OTHER_DIMENSION = Dimension(-1, -1)
def convert_images(
src: Path,
target_extension: str,
desired_extension: str,
dimensions: Iterable[Dimension],
delete_previous: bool = False,
) -> None:
"""Converts images to the specified extension and classifies them by size
:param src: The directory in which to look for images
:param target_extension: The extension of the images to convert
:param desired_extension: The desired format of the new images
:param dimensions: The dimensions on which to classify the images. Could be
empty. If an image has a size not specified in dimensions, it will be
moved to a directory named `other`
:param delete_previous: Whether to delete images after conversion
"""
dimensions_map = _create_dimension_map(src, dimensions)
num_successfully_converted_images = 0
for image_to_convert in src.glob(f"*{target_extension}"):
converted_image = convert_image(image_to_convert, desired_extension)
move_according_to_dimension(converted_image, dimensions_map)
if delete_previous:
image_to_convert.unlink(missing_ok=True)
num_successfully_converted_images += 1
logger.info("Successfully converted %d files", num_successfully_converted_images)
def _create_dimension_map(
src: Path, dimensions: Iterable[Dimension]
) -> dict[Dimension, Path]:
"""Creates the dimensions map"""
dimensions_map = {
dimension: src / f"{dimension.width}x{dimension.height}"
for dimension in dimensions
}
dimensions_map[OTHER_DIMENSION] = src / "other"
for dimension_path in dimensions_map.values():
dimension_path.mkdir(parents=True, exist_ok=True)
return dimensions_map
def convert_image(image_path: Path, desired_extension: str) -> Path:
"""Converts a file to a desired format
:param image_path: The path to the image to be converted
:param desired_extension: The desired format. E.g. ".png"
:return: The path of the newly created image in the desired format
"""
logger.debug("Converting %s", image_path)
image = Image.open(image_path).convert("RGB")
new_file = image_path.with_suffix(desired_extension)
image.save(new_file.parent / new_file.stem, new_file.suffix)
logger.debug("Converted %s to %s", image_path, new_file)
return new_file
def move_according_to_dimension(
image_path: Path, dimension_map: dict[Dimension, Path]
) -> Path:
"""Moves `image_path` to the directory indicated by `dimension_map`
:param image_path: The path of the image to move
:param dimension_map: A map of dimensions. Should contain a default
dimension `OTHER_DIMENSION`
:return: The path of the moved file
"""
image_size = Image.open(image_path).size
dest_dir = dimension_map.get(image_size, dimension_map[OTHER_DIMENSION])
dest_path = dest_dir / image_path.name
image_path.rename(dest_path)
logger.debug("Moved %s into %s", image_path, dest_path)
return dest_path
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Converts images to a desired format",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--src",
type=Path,
default=Path(__file__).absolute().parent,
help="Directory from which to process images. Defaults to this file's directory",
)
parser.add_argument(
"--target-format",
type=str,
default=".webp",
help="The extension of the images to process",
)
parser.add_argument(
"--desired-format",
type=str,
default=".png",
help="The format to which to convert images",
)
parser.add_argument(
"--delete-previous",
action="store_true",
help="If set, it will delete processed images",
)
def dimension_parser(dimension_str: str) -> Dimension:
try:
width, height = dimension_str.split("x")
return Dimension(int(width), int(height))
except ValueError:
raise argparse.ArgumentTypeError(
f"Invalid dimension {dimension_str}. Expected format is WIDTHxHEIGHT. (E.g. 1920x1080)"
)
parser.add_argument(
"--dimensions",
nargs="+",
default=(Dimension(1920, 1080), Dimension(1920, 1200)),
type=dimension_parser,
help="The dimensions for which on which to classify images",
)
args = parser.parse_args()
convert_images(
src=args.src,
target_extension=args.target_format,
desired_extension=args.desired_format,
dimensions=args.dimensions,
delete_previous=args.delete_previous,
)
Now this can be used as:
> python convert.py --src "C:/Users/StephenWilliams/OneDrive - CamLink Services/Wallpapers"
[INFO] - Successfully converted 5 files
It also provides a helpful message and default argument values:
> python converty.py --help
usage: convert.py [-h] [--src SRC] [--target-format TARGET_FORMAT] [--desired-format DESIRED_FORMAT] [--delete-previous] [--dimensions DIMENSIONS [DIMENSIONS ...]]
Converts images to a desired format
options:
-h, --help show this help message and exit
--src SRC Directory from which to process images. Defaults to this file's directory (default: ...)
--target-format TARGET_FORMAT
The extension of the images to process (default: .webp)
--desired-format DESIRED_FORMAT
The format to which to convert images (default: .png)
--delete-previous If set, it will delete processed images (default: False)
--dimensions DIMENSIONS [DIMENSIONS ...]
The dimensions for which on which to classify images (default: (Dimension(width=1920, height=1080), Dimension(width=1920, height=1200)))