Skip to content

Commit 5526566

Browse files
committed
Rewrite of batch resizer script
1 parent eb72acf commit 5526566

File tree

2 files changed

+117
-32
lines changed

2 files changed

+117
-32
lines changed

batch_resizer/logger.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import logging
2+
3+
class Logger:
4+
def __init__(self, name):
5+
self.logger = self.create_logger(name)
6+
self.logger.set_verbosity = self._set_verbosity
7+
8+
9+
def _set_verbosity(self, verbosity):
10+
if verbosity == 1:
11+
self.logger.setLevel(logging.INFO)
12+
if verbosity == 3:
13+
self.logger.setLevel(logging.DEBUG)
14+
15+
16+
def _set_format(self, verbosity):
17+
pass
18+
19+
20+
def has_full_date(self):
21+
pass
22+
23+
24+
def create_logger(self, name):
25+
# Create logger for a particular file
26+
logger = logging.getLogger(name)
27+
28+
# Define output format
29+
format = '%(asctime)s [ %(levelname)s ] %(message)s'
30+
31+
# Create handler to print to stdout
32+
stdout_handler = logging.StreamHandler()
33+
stdout_handler.setFormatter(ColorFormat(format))
34+
logger.addHandler(stdout_handler)
35+
36+
return logger
37+
38+
39+
class ColorFormat(logging.Formatter):
40+
'''
41+
Logging colored formatter, adapted from:
42+
https://stackoverflow.com/a/56944256/3638629
43+
'''
44+
45+
gray = '\x1b[38;5;240m'
46+
blue = '\x1b[38;5;39m'
47+
yellow = '\x1b[38;5;220m'
48+
orange = '\x1b[38;5;202m'
49+
red = '\x1b[38;5;160m'
50+
reset = '\x1b[0m'
51+
52+
def __init__(self, format):
53+
super().__init__()
54+
self.fmt = format
55+
self.FORMATS = {
56+
logging.DEBUG: f'{self.gray}{self.fmt}{self.reset}',
57+
logging.INFO: f'{self.blue}{self.fmt}{self.reset}',
58+
logging.WARNING: f'{self.yellow}{self.fmt}{self.reset}',
59+
logging.ERROR: f'{self.orange}{self.fmt}{self.reset}',
60+
logging.CRITICAL: f'{self.red}{self.fmt}{self.reset}',
61+
}
62+
63+
def format(self, record):
64+
log_format = self.FORMATS.get(record.levelno)
65+
formatter = logging.Formatter(log_format)
66+
return formatter.format(record)

batch_resizer/main.py

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,36 @@
77
from PIL import Image
88
from concurrent.futures import ThreadPoolExecutor
99
import argparse
10-
import logging
1110
import pathlib
1211
import sys
1312
import re
1413

14+
from logger import Logger
15+
1516
CURRENT_VERSION = '1.0.0'
1617
VALID_FORMATS = ['.jpg', '.jpeg', '.png', '.webp']
1718
ALPHA_CHANNEL_UNSUPPORTED = ['.jpg', '.jpeg']
1819
FILE_NOT_FOUND_ERR_CODE = 130
1920

21+
log = Logger(__name__).logger
22+
2023
# 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
24+
# TODO: Implement design patterns (strategy?) for additional options
25+
# TODO: Custom help prompt
2326

2427
def main():
2528
args = parse_arguments()
26-
print(args.usage)
27-
return
2829

2930
if args.verbose:
30-
pass
31+
log.set_verbosity(args.verbose)
3132

3233
if not args.output.exists():
3334
create_dir(args.output, parents=args.parents)
3435

3536
image_files = select_images(args.input)
3637

3738
if not image_files:
38-
print('No files found, exiting...')
39+
log.error('No files found, exiting...')
3940
sys.exit(FILE_NOT_FOUND_ERR_CODE)
4041

4142
with ThreadPoolExecutor() as executor:
@@ -69,29 +70,48 @@ def create_dir(target_dir, parents=False):
6970
try:
7071
target_dir.mkdir(parents=parents)
7172
except FileNotFoundError:
72-
print('Could not find missing directories, try running with -p flag.')
73+
log.error('Could not find missing directories, try running with -p flag.')
7374
sys.exit(FILE_NOT_FOUND_ERR_CODE)
7475

7576

76-
def get_filepath(filename, output_dir, matches=0):
77+
def rename_image(img_path, format, output_dir):
7778
'''
7879
Returns a path-like object of the input filename. Verifies if that filename
7980
already exists in output directory, and appends a suffix to avoid accidental
80-
data loss.
81+
data loss. Recursive call, handle with care :)
8182
'''
82-
83-
result = pathlib.Path(output_dir, filename)
8483

85-
if result in output_dir.iterdir():
86-
name, ext = filename.split('.')
84+
filename = pathlib.Path(output_dir, img_path.stem + format.lower())
85+
86+
if filename in output_dir.iterdir():
87+
if '_copy' not in filename.stem:
88+
new_filename = filename.stem + '_copy1'
89+
else:
90+
_, copy_num = filename.stem.split('_copy')
91+
old_version = f'_copy{copy_num}'
92+
new_version = f'_copy{int(copy_num) + 1}'
93+
new_filename = filename.stem.replace(old_version, new_version)
94+
95+
return rename_image(pathlib.Path(new_filename), format, output_dir)
96+
97+
return filename
98+
8799

88-
if matches == 0:
89-
name += '_copy1'
90100

91-
filename = name.replace(f'_copy{matches - 1}', f'_copy{matches}') + f'.{ext}'
92-
result = get_filepath(filename, output_dir, matches=matches+1)
101+
def resize_image(img, new_width, new_height):
102+
'''
103+
Resizes the image to the dimensions provided, preserving aspect-ratio.
104+
'''
105+
w = new_width or img.width
106+
h = new_height or img.height
107+
enlarged = w > img.width or h > img.height
108+
109+
img.thumbnail((w, h))
93110

94-
return result
111+
if enlarged:
112+
log.warning(f'New size ({img.width}x{img.height}) is larger than original ({img.size})')
113+
else:
114+
log.debug(f'Resized to dimensions: {img.width}x{img.height}')
95115

96116

97117
def process_image(img_path, formats=None, width=None, height=None, dest=None):
@@ -100,37 +120,35 @@ def process_image(img_path, formats=None, width=None, height=None, dest=None):
100120
output_formats = formats or [img_path.suffix.lower()]
101121

102122
with Image.open(img_path) as img:
103-
# determine output dimensions
104-
w = width or img.width
105-
h = height or img.height
106123

107-
# resize image object (preserving aspect-ratio)
108-
img.thumbnail((w, h))
124+
# If custom dimensions were provided, resize image object
125+
if width or height:
126+
resize_image(img, width, height)
109127

110128
# Output one file per format
111129
for f in output_formats:
112-
filename = img_path.stem
113130

114-
# If img was resized, append new dimensions to output filename
131+
# If img is being resized, append new dimensions to output filename
115132
if width or height:
116-
filename += f'_{img.width}-{img.height}'
117-
118-
filename += f.lower()
133+
dimensions = f'_{img.width}-{img.height}'
134+
img_path = pathlib.Path(img_path.stem + dimensions)
119135

120136
# avoid overwrite by checking for duplicate filenames
121-
output_file = get_filepath(filename, dest)
137+
output_file = rename_image(img_path, f, dest)
122138

123139
# Remove alpha channel when converting to formats that don't support it
124140
if f in ALPHA_CHANNEL_UNSUPPORTED:
141+
log.debug(f'Removing alpha channel from {img_path}')
125142
img = img.convert('RGB')
126143

127144
# save image
128-
print(output_file)
145+
log.info(f'Saving file as: {output_file.resolve()}')
129146
img.save(output_file)
130147

131148

132149
def parse_arguments():
133150
parser = argparse.ArgumentParser(add_help=False)
151+
134152
parser.add_argument('input',
135153
help='Path to the image file(s) to process',
136154
nargs='+',
@@ -160,7 +178,7 @@ def parse_arguments():
160178
parser.add_argument('-f', '--formats',
161179
help='Transform images to the specified format(s)',
162180
nargs='+',
163-
choices=['.jpg', '.jpeg', '.png', '.webp']
181+
choices=VALID_FORMATS
164182
)
165183
parser.add_argument('-v', '--verbose',
166184
help='Produces additional output as the program runs',
@@ -171,6 +189,7 @@ def parse_arguments():
171189
action='version',
172190
version=f'%(prog)s v{CURRENT_VERSION}'
173191
)
192+
174193
return parser.parse_args()
175194

176195

0 commit comments

Comments
 (0)