1
1
import os
2
2
from PIL import Image
3
+ import argparse
4
+ import logging
5
+ from tqdm import tqdm
3
6
7
+ # Configure logging
8
+ logging .basicConfig (level = logging .INFO ,format = '%(asctime)s - %(levelname)s - %(message)s' )
9
+ logger = logging .getLogger (__name__ )
4
10
5
11
def 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."""
12
13
for unit in ["" ,"K" ,"M" ,"G" ,"T" ,"P" ,"E" ,"Z" ]:
13
14
if b < factor :
14
15
return f"{ b :.2f} { unit } { suffix } "
15
16
b /= factor
16
17
return f"{ b :.2f} Y{ suffix } "
17
-
18
18
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."""
48
31
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
+
67
86
if __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
+
76
98
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 )