Skip to content

Commit eb72acf

Browse files
committed
Complete rewrite of batch_resizer.py script
1 parent a72e622 commit eb72acf

File tree

2 files changed

+179
-0
lines changed

2 files changed

+179
-0
lines changed

batch_resizer/main.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/python3
2+
3+
""""
4+
Resize and transform images into different image file formats.
5+
"""
6+
7+
from PIL import Image
8+
from concurrent.futures import ThreadPoolExecutor
9+
import argparse
10+
import logging
11+
import pathlib
12+
import sys
13+
import re
14+
15+
CURRENT_VERSION = '1.0.0'
16+
VALID_FORMATS = ['.jpg', '.jpeg', '.png', '.webp']
17+
ALPHA_CHANNEL_UNSUPPORTED = ['.jpg', '.jpeg']
18+
FILE_NOT_FOUND_ERR_CODE = 130
19+
20+
# TODO: Validate files based on mime type rather than file extension
21+
# TODO: Verbosity and console output with logging
22+
# TODO: Implement design patterns (strategy?) for additional options per extension
23+
24+
def main():
25+
args = parse_arguments()
26+
print(args.usage)
27+
return
28+
29+
if args.verbose:
30+
pass
31+
32+
if not args.output.exists():
33+
create_dir(args.output, parents=args.parents)
34+
35+
image_files = select_images(args.input)
36+
37+
if not image_files:
38+
print('No files found, exiting...')
39+
sys.exit(FILE_NOT_FOUND_ERR_CODE)
40+
41+
with ThreadPoolExecutor() as executor:
42+
for img in image_files:
43+
executor.submit(
44+
process_image,
45+
img,
46+
formats=args.formats,
47+
width=args.width,
48+
height=args.height,
49+
dest=args.output
50+
)
51+
52+
53+
def select_images(files):
54+
'''Selects all the valid image files'''
55+
return [f for f in files if is_valid_format(f)]
56+
57+
58+
def is_valid_format(file):
59+
'''Verifies input to be a file, and with a valid extension'''
60+
return file.is_file() and file.suffix.lower() in VALID_FORMATS
61+
62+
63+
def create_dir(target_dir, parents=False):
64+
'''
65+
Creates a directory on input location. Parents option set to True will
66+
create any missing directories.
67+
'''
68+
69+
try:
70+
target_dir.mkdir(parents=parents)
71+
except FileNotFoundError:
72+
print('Could not find missing directories, try running with -p flag.')
73+
sys.exit(FILE_NOT_FOUND_ERR_CODE)
74+
75+
76+
def get_filepath(filename, output_dir, matches=0):
77+
'''
78+
Returns a path-like object of the input filename. Verifies if that filename
79+
already exists in output directory, and appends a suffix to avoid accidental
80+
data loss.
81+
'''
82+
83+
result = pathlib.Path(output_dir, filename)
84+
85+
if result in output_dir.iterdir():
86+
name, ext = filename.split('.')
87+
88+
if matches == 0:
89+
name += '_copy1'
90+
91+
filename = name.replace(f'_copy{matches - 1}', f'_copy{matches}') + f'.{ext}'
92+
result = get_filepath(filename, output_dir, matches=matches+1)
93+
94+
return result
95+
96+
97+
def process_image(img_path, formats=None, width=None, height=None, dest=None):
98+
'''Converts the image format and/or resizes, preserving aspect ratio.'''
99+
100+
output_formats = formats or [img_path.suffix.lower()]
101+
102+
with Image.open(img_path) as img:
103+
# determine output dimensions
104+
w = width or img.width
105+
h = height or img.height
106+
107+
# resize image object (preserving aspect-ratio)
108+
img.thumbnail((w, h))
109+
110+
# Output one file per format
111+
for f in output_formats:
112+
filename = img_path.stem
113+
114+
# If img was resized, append new dimensions to output filename
115+
if width or height:
116+
filename += f'_{img.width}-{img.height}'
117+
118+
filename += f.lower()
119+
120+
# avoid overwrite by checking for duplicate filenames
121+
output_file = get_filepath(filename, dest)
122+
123+
# Remove alpha channel when converting to formats that don't support it
124+
if f in ALPHA_CHANNEL_UNSUPPORTED:
125+
img = img.convert('RGB')
126+
127+
# save image
128+
print(output_file)
129+
img.save(output_file)
130+
131+
132+
def parse_arguments():
133+
parser = argparse.ArgumentParser(add_help=False)
134+
parser.add_argument('input',
135+
help='Path to the image file(s) to process',
136+
nargs='+',
137+
type=pathlib.Path
138+
)
139+
parser.add_argument('-o', '--output-dir',
140+
help="Directory to save processed images. Create dir it if doesn't exist.",
141+
default=pathlib.os.getcwd(),
142+
type=pathlib.Path,
143+
dest='output'
144+
)
145+
parser.add_argument('-p', '--create-parents',
146+
help="Creates missing directories for target output",
147+
action='store_true',
148+
dest='parents'
149+
)
150+
parser.add_argument('-w', '--width',
151+
help='Pixel width of the output image',
152+
type=int,
153+
metavar='width'
154+
)
155+
parser.add_argument('-h', '--height',
156+
help='Pixel height of the output image',
157+
type=int,
158+
metavar='height'
159+
)
160+
parser.add_argument('-f', '--formats',
161+
help='Transform images to the specified format(s)',
162+
nargs='+',
163+
choices=['.jpg', '.jpeg', '.png', '.webp']
164+
)
165+
parser.add_argument('-v', '--verbose',
166+
help='Produces additional output as the program runs',
167+
action='count',
168+
default=0
169+
)
170+
parser.add_argument('--version',
171+
action='version',
172+
version=f'%(prog)s v{CURRENT_VERSION}'
173+
)
174+
return parser.parse_args()
175+
176+
177+
if __name__ == '__main__':
178+
sys.exit(main())

batch_resizer/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pillow == 8.4

0 commit comments

Comments
 (0)