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."""
1213for unit in ["" ,"K" ,"M" ,"G" ,"T" ,"P" ,"E" ,"Z" ]:
1314if b < factor :
1415return f"{ b :.2f} { unit } { suffix } "
1516b /= factor
1617return 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."""
4831try :
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+
7698args = 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 )