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} { 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 }  
45-     else :
46-         # retain the same extension of the original image 
47-         new_filename  =  f"{ filename } { 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}  )
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 } { 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 } { 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