11import os
22from PIL import Image
3+ import argparse
4+ import logging
5+ from tqdm import tqdm
36
7+ # Configure logging
8+ logging .basicConfig (level = logging .INFO , format = '%(asctime)s - %(levelname)s - %(message)s' )
9+ logger = logging .getLogger (__name__ )
410
511def get_size_format (b , factor = 1024 , suffix = "B" ):
6- """
7- Scale bytes to its proper byte format
8- e.g:
9- 1253656 => '1.20MB'
10- 1253656678 => '1.17GB'
11- """
12+ """Scale bytes to its proper byte format."""
1213 for unit in ["" , "K" , "M" , "G" , "T" , "P" , "E" , "Z" ]:
1314 if b < factor :
1415 return f"{ b :.2f} { unit } { suffix } "
1516 b /= factor
1617 return f"{ b :.2f} Y{ suffix } "
17-
1818
19-
20- def compress_img (image_name , new_size_ratio = 0.9 , quality = 90 , width = None , height = None , to_jpg = True ):
21- # load the image to memory
22- img = Image .open (image_name )
23- # print the original image shape
24- print ("[*] Image shape:" , img .size )
25- # get the original image size in bytes
26- image_size = os .path .getsize (image_name )
27- # print the size before compression/resizing
28- print ("[*] Size before compression:" , get_size_format (image_size ))
29- if new_size_ratio < 1.0 :
30- # if resizing ratio is below 1.0, then multiply width & height with this ratio to reduce image size
31- img = img .resize ((int (img .size [0 ] * new_size_ratio ), int (img .size [1 ] * new_size_ratio )), Image .LANCZOS )
32- # print new image shape
33- print ("[+] New Image shape:" , img .size )
34- elif width and height :
35- # if width and height are set, resize with them instead
36- img = img .resize ((width , height ), Image .LANCZOS )
37- # print new image shape
38- print ("[+] New Image shape:" , img .size )
39- # split the filename and extension
40- filename , ext = os .path .splitext (image_name )
41- # make new filename appending _compressed to the original file name
42- if to_jpg :
43- # change the extension to JPEG
44- new_filename = f"{ filename } _compressed.jpg"
45- else :
46- # retain the same extension of the original image
47- new_filename = f"{ filename } _compressed{ ext } "
19+ def compress_image (
20+ input_path ,
21+ output_dir = None ,
22+ quality = 85 ,
23+ resize_ratio = 1.0 ,
24+ width = None ,
25+ height = None ,
26+ to_jpg = False ,
27+ preserve_metadata = True ,
28+ lossless = False ,
29+ ):
30+ """Compress an image with advanced options."""
4831 try :
49- # save the image with the corresponding quality and optimize set to True
50- img .save (new_filename , quality = quality , optimize = True )
51- except OSError :
52- # convert the image to RGB mode first
53- img = img .convert ("RGB" )
54- # save the image with the corresponding quality and optimize set to True
55- img .save (new_filename , quality = quality , optimize = True )
56- print ("[+] New file saved:" , new_filename )
57- # get the new image size in bytes
58- new_image_size = os .path .getsize (new_filename )
59- # print the new size in a good format
60- print ("[+] Size after compression:" , get_size_format (new_image_size ))
61- # calculate the saving bytes
62- saving_diff = new_image_size - image_size
63- # print the saving percentage
64- print (f"[+] Image size change: { saving_diff / image_size * 100 :.2f} % of the original image size." )
65-
66-
32+ img = Image .open (input_path )
33+ logger .info (f"[*] Processing: { os .path .basename (input_path )} " )
34+ logger .info (f"[*] Original size: { get_size_format (os .path .getsize (input_path ))} " )
35+
36+ # Resize if needed
37+ if resize_ratio < 1.0 :
38+ new_size = (int (img .size [0 ] * resize_ratio ), int (img .size [1 ] * resize_ratio ))
39+ img = img .resize (new_size , Image .LANCZOS )
40+ logger .info (f"[+] Resized to: { new_size } " )
41+ elif width and height :
42+ img = img .resize ((width , height ), Image .LANCZOS )
43+ logger .info (f"[+] Resized to: { width } x{ height } " )
44+
45+ # Prepare output path
46+ filename , ext = os .path .splitext (os .path .basename (input_path ))
47+ output_ext = ".jpg" if to_jpg else ext
48+ output_filename = f"{ filename } _compressed{ output_ext } "
49+ output_path = os .path .join (output_dir or os .path .dirname (input_path ), output_filename )
50+
51+ # Save with options
52+ save_kwargs = {"quality" : quality , "optimize" : True }
53+ if not preserve_metadata :
54+ save_kwargs ["exif" ] = b"" # Strip metadata
55+ if lossless and ext .lower () in (".png" , ".webp" ):
56+ save_kwargs ["lossless" ] = True
57+
58+ try :
59+ img .save (output_path , ** save_kwargs )
60+ except OSError :
61+ img = img .convert ("RGB" )
62+ img .save (output_path , ** save_kwargs )
63+
64+ logger .info (f"[+] Saved to: { output_path } " )
65+ logger .info (f"[+] New size: { get_size_format (os .path .getsize (output_path ))} " )
66+ except Exception as e :
67+ logger .error (f"[!] Error processing { input_path } : { e } " )
68+
69+ def batch_compress (
70+ input_paths ,
71+ output_dir = None ,
72+ quality = 85 ,
73+ resize_ratio = 1.0 ,
74+ width = None ,
75+ height = None ,
76+ to_jpg = False ,
77+ preserve_metadata = True ,
78+ lossless = False ,
79+ ):
80+ """Compress multiple images."""
81+ if output_dir and not os .path .exists (output_dir ):
82+ os .makedirs (output_dir , exist_ok = True )
83+ for path in tqdm (input_paths , desc = "Compressing images" ):
84+ compress_image (path , output_dir , quality , resize_ratio , width , height , to_jpg , preserve_metadata , lossless )
85+
6786if __name__ == "__main__" :
68- import argparse
69- parser = argparse .ArgumentParser (description = "Simple Python script for compressing and resizing images" )
70- parser .add_argument ("image" , help = "Target image to compress and/or resize" )
71- parser .add_argument ("-j" , "--to-jpg" , action = "store_true" , help = "Whether to convert the image to the JPEG format" )
72- parser .add_argument ("-q" , "--quality" , type = int , help = "Quality ranging from a minimum of 0 (worst) to a maximum of 95 (best). Default is 90" , default = 90 )
73- parser .add_argument ("-r" , "--resize-ratio" , type = float , help = "Resizing ratio from 0 to 1, setting to 0.5 will multiply width & height of the image by 0.5. Default is 1.0" , default = 1.0 )
74- parser .add_argument ("-w" , "--width" , type = int , help = "The new width image, make sure to set it with the `height` parameter" )
75- parser .add_argument ("-hh" , "--height" , type = int , help = "The new height for the image, make sure to set it with the `width` parameter" )
87+ parser = argparse .ArgumentParser (description = "Advanced Image Compressor with Batch Processing" )
88+ parser .add_argument ("input" , nargs = '+' , help = "Input image(s) or directory" )
89+ parser .add_argument ("-o" , "--output-dir" , help = "Output directory (default: same as input)" )
90+ parser .add_argument ("-q" , "--quality" , type = int , default = 85 , help = "Compression quality (0-100)" )
91+ parser .add_argument ("-r" , "--resize-ratio" , type = float , default = 1.0 , help = "Resize ratio (0-1)" )
92+ parser .add_argument ("-w" , "--width" , type = int , help = "Output width (requires --height)" )
93+ parser .add_argument ("-hh" , "--height" , type = int , help = "Output height (requires --width)" )
94+ parser .add_argument ("-j" , "--to-jpg" , action = "store_true" , help = "Convert output to JPEG" )
95+ parser .add_argument ("-m" , "--no-metadata" , action = "store_false" , help = "Strip metadata" )
96+ parser .add_argument ("-l" , "--lossless" , action = "store_true" , help = "Use lossless compression (PNG/WEBP)" )
97+
7698 args = parser .parse_args ()
77- # print the passed arguments
78- print ("=" * 50 )
79- print ("[*] Image:" , args .image )
80- print ("[*] To JPEG:" , args .to_jpg )
81- print ("[*] Quality:" , args .quality )
82- print ("[*] Resizing ratio:" , args .resize_ratio )
83- if args .width and args .height :
84- print ("[*] Width:" , args .width )
85- print ("[*] Height:" , args .height )
86- print ("=" * 50 )
87- # compress the image
88- compress_img (args .image , args .resize_ratio , args .quality , args .width , args .height , args .to_jpg )
99+ input_paths = []
100+ for path in args .input :
101+ if os .path .isdir (path ): input_paths .extend (os .path .join (path , f ) for f in os .listdir (path ) if f .lower ().endswith ((".jpg" ,".jpeg" ,".png" ,".webp" )))
102+ else : input_paths .append (path )
103+ if not input_paths : logger .error ("No valid images found!" ); exit (1 )
104+ batch_compress (input_paths , args .output_dir , args .quality , args .resize_ratio , args .width , args .height , args .to_jpg , args .no_metadata , args .lossless )
0 commit comments