7
7
from PIL import Image
8
8
from concurrent .futures import ThreadPoolExecutor
9
9
import argparse
10
- import logging
11
10
import pathlib
12
11
import sys
13
12
import re
14
13
14
+ from logger import Logger
15
+
15
16
CURRENT_VERSION = '1.0.0'
16
17
VALID_FORMATS = ['.jpg' , '.jpeg' , '.png' , '.webp' ]
17
18
ALPHA_CHANNEL_UNSUPPORTED = ['.jpg' , '.jpeg' ]
18
19
FILE_NOT_FOUND_ERR_CODE = 130
19
20
21
+ log = Logger (__name__ ).logger
22
+
20
23
# 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
23
26
24
27
def main ():
25
28
args = parse_arguments ()
26
- print (args .usage )
27
- return
28
29
29
30
if args .verbose :
30
- pass
31
+ log . set_verbosity ( args . verbose )
31
32
32
33
if not args .output .exists ():
33
34
create_dir (args .output , parents = args .parents )
34
35
35
36
image_files = select_images (args .input )
36
37
37
38
if not image_files :
38
- print ('No files found, exiting...' )
39
+ log . error ('No files found, exiting...' )
39
40
sys .exit (FILE_NOT_FOUND_ERR_CODE )
40
41
41
42
with ThreadPoolExecutor () as executor :
@@ -69,29 +70,48 @@ def create_dir(target_dir, parents=False):
69
70
try :
70
71
target_dir .mkdir (parents = parents )
71
72
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.' )
73
74
sys .exit (FILE_NOT_FOUND_ERR_CODE )
74
75
75
76
76
- def get_filepath ( filename , output_dir , matches = 0 ):
77
+ def rename_image ( img_path , format , output_dir ):
77
78
'''
78
79
Returns a path-like object of the input filename. Verifies if that filename
79
80
already exists in output directory, and appends a suffix to avoid accidental
80
- data loss.
81
+ data loss. Recursive call, handle with care :)
81
82
'''
82
-
83
- result = pathlib .Path (output_dir , filename )
84
83
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
+
87
99
88
- if matches == 0 :
89
- name += '_copy1'
90
100
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 ))
93
110
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 } ' )
95
115
96
116
97
117
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):
100
120
output_formats = formats or [img_path .suffix .lower ()]
101
121
102
122
with Image .open (img_path ) as img :
103
- # determine output dimensions
104
- w = width or img .width
105
- h = height or img .height
106
123
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 )
109
127
110
128
# Output one file per format
111
129
for f in output_formats :
112
- filename = img_path .stem
113
130
114
- # If img was resized, append new dimensions to output filename
131
+ # If img is being resized, append new dimensions to output filename
115
132
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 )
119
135
120
136
# avoid overwrite by checking for duplicate filenames
121
- output_file = get_filepath ( filename , dest )
137
+ output_file = rename_image ( img_path , f , dest )
122
138
123
139
# Remove alpha channel when converting to formats that don't support it
124
140
if f in ALPHA_CHANNEL_UNSUPPORTED :
141
+ log .debug (f'Removing alpha channel from { img_path } ' )
125
142
img = img .convert ('RGB' )
126
143
127
144
# save image
128
- print ( output_file )
145
+ log . info ( f'Saving file as: { output_file . resolve () } ' )
129
146
img .save (output_file )
130
147
131
148
132
149
def parse_arguments ():
133
150
parser = argparse .ArgumentParser (add_help = False )
151
+
134
152
parser .add_argument ('input' ,
135
153
help = 'Path to the image file(s) to process' ,
136
154
nargs = '+' ,
@@ -160,7 +178,7 @@ def parse_arguments():
160
178
parser .add_argument ('-f' , '--formats' ,
161
179
help = 'Transform images to the specified format(s)' ,
162
180
nargs = '+' ,
163
- choices = [ '.jpg' , '.jpeg' , '.png' , '.webp' ]
181
+ choices = VALID_FORMATS
164
182
)
165
183
parser .add_argument ('-v' , '--verbose' ,
166
184
help = 'Produces additional output as the program runs' ,
@@ -171,6 +189,7 @@ def parse_arguments():
171
189
action = 'version' ,
172
190
version = f'%(prog)s v{ CURRENT_VERSION } '
173
191
)
192
+
174
193
return parser .parse_args ()
175
194
176
195
0 commit comments