Skip to content

Commit 9e05bb0

Browse files
committed
updated script made shorter and support for individual file handling, as well as multiple format conversion
1 parent d7950ac commit 9e05bb0

File tree

1 file changed

+95
-161
lines changed

1 file changed

+95
-161
lines changed

batch-resizer.py

100644100755
Lines changed: 95 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,211 +1,145 @@
11
#!/usr/bin/python3
22

33
"""
4-
Process all images inside a directory: resize them, convert to different formats,
5-
remove metadata, add watermarks and compress it all nicely for upload or share
6-
online.
4+
Process images by converting them to different formats, resizing them and store
5+
them as compressed archives for easy uplaod to your cloud. You may provide one
6+
or more images and they'll be processed in parallel for extra speed.
7+
8+
usage: imgtest.py [-h] [-v] [-o [OUTPUT]] [-r WIDTH HEIGHT]
9+
[-f {webp,jpeg,jpg,png} [{webp,jpeg,jpg,png} ...]]
10+
[-z | -t [ARCHIVE]]
11+
input [input ...]
12+
13+
positional arguments:
14+
input A single or multiple image files to process
15+
16+
optional arguments:
17+
-h, --help show this help message and exit
18+
-v, --verbose Writes output to the terminal as the program runs
19+
-o [OUTPUT], --output [OUTPUT]
20+
Directory where processed images will be saved to
21+
-r WIDTH HEIGHT, --resize WIDTH HEIGHT
22+
Pixel width and height of the output image (preserves
23+
aspect-ratio)
24+
-f {webp,jpeg,jpg,png} [{webp,jpeg,jpg,png} ...], --format {webp,jpeg,jpg,png} [{webp,jpeg,jpg,png} ...]
25+
Convert processed file to the selected image format
26+
-z, --zip-archive Store processed images as .zip archive (uncompressed)
27+
-t [ARCHIVE], --tar-archive [ARCHIVE]
28+
Store processed images as .tar.gz archive (compressed)
729
8-
USAGE: resizer.py INPUT_FOLDER [OPTIONS]
9-
10-
TODO: Option to convert to multiple image formats and add support for more formats.
1130
TODO: Option to provide an image to add as a watermark for each image
12-
TODO: Option to preserve original metadata into the processed image
13-
TODO: Option to generate each output image's name using image recognition
1431
"""
1532

1633
from PIL import Image
1734
from itertools import repeat
18-
from pathlib import Path
1935
import concurrent.futures
2036
import argparse
21-
import zipfile
37+
import logging
2238
import tarfile
23-
import hashlib
24-
import shutil
25-
import sys
26-
import os
39+
import zipfile
2740
import re
28-
import logging
29-
30-
def main(args):
41+
import os
3142

43+
def main():
3244
args = parse_arguments()
33-
if args.verbose == 1:
34-
logging.basicConfig(level=logging.INFO)
3545

36-
# What files will be processed
37-
valid_files = re.compile(r'.*\.(jpg|jpeg|png|webp)$', re.IGNORECASE)
38-
images = [file for file in os.listdir(args.input_dir) if valid_files.match(file)]
46+
valid_ext = re.compile(r'.*\.(jpe?g|png|webp)$', re.IGNORECASE)
47+
images = [file for file in args.input if valid_ext.match(file)]
3948

40-
# For logging purposes only
41-
get_images_size = (os.path.getsize(args.input_dir / img) for img in images)
42-
logging.info(f'{len(images)} images found ({round(sum(get_images_size) / 1000000, 2)} Mb...)')
49+
if args.verbose:
50+
logging.basicConfig(level=logging.DEBUG)
51+
print('Dimensions:', args.dimensions)
52+
print('Output:', args.output)
53+
print('Convert to:', args.format)
54+
print(f'Found {len(images)} images:')
4355

44-
# Process each file using multi-processing
56+
# Process each file using multi-processing, returns extensions used
4557
with concurrent.futures.ProcessPoolExecutor() as executor:
46-
executor.map(process_image, images, repeat(args))
58+
formats = executor.map(process_image, images, repeat(args))
4759

48-
# If an archive flag was passed, make an archive for all the files
60+
# Creates an archive with the output images.
4961
if args.archive:
50-
make_archive(args)
51-
52-
# If the delete flag was passed, delete processed files
53-
if args.delete_original:
54-
delete_processed(images, args)
55-
56-
57-
def process_image(file, args):
58-
59-
'''Resizes the image, saves it and if specified it renames it using a hash
60-
function to randomize its name'''
61-
62-
logging.info(f'Processing {file}...')
63-
64-
filename, _ = os.path.splitext(file)
65-
with Image.open(args.input_dir / file) as img:
66-
# Get relevant data for the new image
67-
width = args.dimensions[0] if args.dimensions else img.width
68-
height = args.dimensions[1] if args.dimensions else img.height
69-
format = args.format if args.format else img.format
70-
71-
# Obscure filename with a hashing function
72-
if args.hash_names:
73-
logging.info(f'Hashing filename...')
74-
filename = hashlib.sha1(file.encode('utf-8')).hexdigest()
75-
76-
# Create new image file with specified dimensions
77-
img.thumbnail((width, height))
78-
img.save(f'{args.output_dir / filename}.{format.lower()}', format.upper())
79-
80-
81-
def make_archive(args):
82-
83-
'''Creates an archive of the specified format to store all processed images.'''
84-
85-
archive_location = args.output_dir.parent
62+
make_archive(args, images, next(formats))
8663

87-
# Create archive in tar format
88-
if args.archive == 'gz':
89-
with tarfile.open(archive_location / 'processed_images.tar.gz', 'x:gz') as tar:
90-
tar.add(args.output_dir, arcname='images')
9164

92-
# Create archive in zip format
93-
if args.archive == 'ZIP_STORED':
94-
with zipfile.ZipFile(archive_location / 'processed_images.zip', 'w') as zip:
95-
os.chdir(args.output_dir)
96-
for file in os.listdir(args.output_dir):
97-
zip.write(file)
65+
def make_archive(args, input, formats):
66+
'''Creates an archive of the specified format to store output images.'''
9867

68+
# Store a list of image filenames without their full path or file format
69+
images = [os.path.splitext(i)[0].split(os.path.sep)[-1] for i in input]
9970

100-
# Once archive is created we can delete the output directory
101-
logging.info(f'Archive created, removing output directory...')
102-
shutil.rmtree(args.output_dir)
71+
# Store the file extensions used for this script execution
72+
formats = set([format.replace('jpg', 'jpeg') for format in formats])
10373

74+
archive_name = os.path.join(args.output, f'{args.archive}.tar.gz')
10475

105-
def delete_processed(images, args):
76+
logging.debug(f'Building archive {archive_name}...')
10677

107-
'''Deletes all files that were processed'''
78+
with tarfile.open(archive_name, 'x:gz') as tar:
79+
for img in images:
80+
for ext in formats:
81+
file_to_add = f'{args.output}{os.path.sep}resized-{img}.{ext}'
82+
tar.add(file_to_add, arcname=f'resized-{img}.{ext}')
10883

109-
logging.info(f'Deleting originals...')
110-
for img in images:
111-
os.unlink(args.input_dir / img)
11284

85+
def process_image(image, args):
86+
'''Takes an image file path and process it according to provided arguments'''
11387

114-
def get_input_directory(path):
88+
filename, ext = os.path.splitext(os.path.basename(image))
89+
formats = args.format if args.format else [ext.replace('.', '')]
11590

116-
'''Verifies the path provided and returns it as a Path object'''
91+
# Append short hash to filename to avoid accidental overwrite
92+
filename = f'resized-{filename}'
11793

118-
p = Path(path)
119-
if p.exists() and p.is_dir():
120-
return p.resolve()
121-
else:
122-
msg = f'{path} is not a valid directory or does not exist'
123-
raise argparse.ArgumentTypeError(msg)
94+
logging.debug(f'Processing file {filename}')
12495

96+
with Image.open(image) as img:
97+
# Find width, height and output file format
98+
width = args.dimensions[0] if args.dimensions else img.width
99+
height = args.dimensions[1] if args.dimensions else img.height
125100

126-
def get_output_directory(path):
127-
128-
'''Verifies the path provided and returns it as a Path object. It'll
129-
create any necessary directories if they don't exist'''
101+
# Resize image to specified dimensions
102+
img.thumbnail((width, height))
130103

131-
p = Path(path) / 'processed_images'
132-
if p.exists() and p.is_dir():
133-
return p.resolve()
134-
elif p.exists() and not p.is_dir():
135-
msg = f'{path} is not a valid directory'
136-
raise argparse.ArgumentTypeError(msg)
137-
else:
138-
dir = p.resolve()
139-
os.makedirs(dir)
140-
return dir
104+
# Save image for each specified format
105+
for f in formats:
106+
format = 'JPEG' if f.lower().endswith('jpg') else f.upper()
107+
filepath = f'{os.path.join(args.output, filename)}.{format.lower()}'
108+
img.save(filepath, format)
141109

110+
return formats
142111

143-
def get_watermark_image(path):
144112

145-
'''Verifies the path provided and returns an Image object'''
113+
def parse_arguments():
114+
'''Parses the arguments used to call this script'''
146115

147-
p = Path(path)
148-
if p.exists() and p.is_file():
149-
with Image.open(p.resolve()) as img:
150-
return img
151-
else:
152-
msg = f'{path} is not a valid file or does not exist'
153-
raise argparse.ArgumentTypeError(msg)
116+
parser = argparse.ArgumentParser()
154117

155-
def parse_arguments():
156-
parser = argparse.ArgumentParser(
157-
description='Bulk image processor - resize, convert and organize your images'
118+
parser.add_argument('-v', '--verbose',
119+
help='Writes output to the terminal as the program runs',
120+
action='store_true'
158121
)
159-
parser.add_argument('input_dir',
160-
help='Directory location with all images',
161-
metavar='input_file',
162-
type=get_input_directory
122+
parser.add_argument('input',
123+
help='A single or multiple image files to process',
124+
nargs='+'
163125
)
164126
parser.add_argument('-o', '--output',
165-
help='Choose the location for output images (defaults to current directory)',
166-
dest='output_dir',
167-
metavar='output_dir',
168-
type=get_output_directory,
127+
help='Directory where processed images will be saved to',
128+
nargs='?',
169129
default=os.getcwd()
170130
)
171131
parser.add_argument('-r', '--resize',
172-
help='Resize images to width and height, preserves aspect-ratio',
173-
dest='dimensions',
174-
metavar=('width', 'height'),
132+
help='Pixel width and height of the output image (preserves aspect-ratio)',
133+
nargs=2,
175134
type=int,
176-
nargs=2
135+
metavar=('WIDTH', 'HEIGHT'),
136+
dest='dimensions'
177137
)
178138
parser.add_argument('-f', '--format',
179-
help='Converts image to specified format',
180-
dest='format',
181-
choices=['jpg', 'jpeg', 'png', 'webp']
182-
)
183-
parser.add_argument('-m', '--metadata',
184-
help="Preserves the original image's metadata",
185-
dest='metadata',
186-
action='store_true'
187-
)
188-
parser.add_argument('-u', '--use-hash',
189-
help='Obscure output filenames using a hashing function',
190-
dest='hash_names',
191-
action='store_true'
192-
)
193-
parser.add_argument('-d', '--delete',
194-
help='Deletes original images upon completion',
195-
dest='delete_original',
196-
action='store_true'
197-
)
198-
parser.add_argument('-w', '--watermark',
199-
help='Blends the provided image into the processed output',
200-
dest='watermark',
201-
metavar='image_file',
202-
type=get_watermark_image
203-
)
204-
parser.add_argument('-v', '--verbose',
205-
help='Writes output to the terminal as the program runs',
206-
dest='verbose',
207-
action='count',
208-
default=0
139+
help='Convert processed file to the selected image format',
140+
nargs='+',
141+
action='store',
142+
choices=['webp', 'jpeg', 'jpg', 'png']
209143
)
210144
archive_options = parser.add_mutually_exclusive_group()
211145
archive_options.add_argument('-z', '--zip-archive',
@@ -217,11 +151,11 @@ def parse_arguments():
217151
archive_options.add_argument('-t', '--tar-archive',
218152
help='Store processed images as .tar.gz archive (compressed)',
219153
dest='archive',
220-
action='store_const',
221-
const='gz'
154+
nargs='?',
155+
const='output'
222156
)
223-
224157
return parser.parse_args()
225158

159+
226160
if __name__ == '__main__':
227161
main()

0 commit comments

Comments
 (0)