1+ import sys
2+ import os
3+ import tkinter as tk
4+ from tkinter import filedialog
5+ from PIL import Image
6+
7+ NORMAL_MAX_BYTES = 10 * 1024 * 1024
8+ AGGRESSIVE_MAX_BYTES = 5 * 1024 * 1024
9+ OUTPUT_DIR = os .path .dirname (os .path .abspath (__file__ ))
10+
11+
12+ def get_file_size (path ):
13+ return os .path .getsize (path )
14+
15+
16+ def downscale_image (input_path , max_bytes ):
17+ if not os .path .isfile (input_path ):
18+ print (f"File not found: { input_path } " )
19+ return None
20+
21+ file_size = get_file_size (input_path )
22+ filename = os .path .basename (input_path )
23+ name , ext = os .path .splitext (filename )
24+ output_ext = ext .lower () if ext .lower () in (".jpg" , ".jpeg" , ".png" , ".webp" ) else ".jpg"
25+ output_path = os .path .join (OUTPUT_DIR , f"{ name } _downscaled{ output_ext } " )
26+
27+ img = Image .open (input_path )
28+
29+ if img .mode == "RGBA" and output_ext in (".jpg" , ".jpeg" ):
30+ img = img .convert ("RGB" )
31+ elif img .mode not in ("RGB" , "RGBA" ):
32+ img = img .convert ("RGB" )
33+
34+ if file_size <= max_bytes :
35+ img .save (output_path , quality = 95 )
36+ print (f"Image already under limit. Saved to: { output_path } " )
37+ return output_path
38+
39+ quality = 95
40+ scale = 1.0
41+
42+ img .save (output_path , quality = quality , optimize = True )
43+
44+ while get_file_size (output_path ) > max_bytes and quality > 20 :
45+ quality -= 5
46+ img .save (output_path , quality = quality , optimize = True )
47+
48+ if get_file_size (output_path ) <= max_bytes :
49+ print (f"Downscaled (quality={ quality } ): { output_path } " )
50+ return output_path
51+
52+ while get_file_size (output_path ) > max_bytes and scale > 0.1 :
53+ scale -= 0.1
54+ new_width = int (img .width * scale )
55+ new_height = int (img .height * scale )
56+ resized = img .resize ((new_width , new_height ), Image .LANCZOS )
57+ resized .save (output_path , quality = quality , optimize = True )
58+
59+ print (f"Downscaled (quality={ quality } , scale={ scale :.0%} ): { output_path } " )
60+ return output_path
61+
62+
63+ def pick_images ():
64+ root = tk .Tk ()
65+ root .withdraw ()
66+ root .attributes ('-topmost' , True )
67+ root .update ()
68+ paths = filedialog .askopenfilenames (
69+ title = "Select images to downscale" ,
70+ filetypes = [
71+ ("Image files" , "*.jpg *.jpeg *.png *.webp *.bmp *.tiff" ),
72+ ("All files" , "*.*" ),
73+ ],
74+ )
75+ root .destroy ()
76+ return paths
77+
78+
79+ def pick_mode ():
80+ print ("Select downscale mode:" )
81+ print (" 1) Normal - 10MB or less" )
82+ print (" 2) Aggressive - 5MB or less" )
83+ choice = input ("Enter 1 or 2: " ).strip ()
84+ if choice == "2" :
85+ return AGGRESSIVE_MAX_BYTES
86+ return NORMAL_MAX_BYTES
87+
88+
89+ if __name__ == "__main__" :
90+ max_bytes = pick_mode ()
91+
92+ if len (sys .argv ) >= 2 :
93+ files = sys .argv [1 :]
94+ else :
95+ files = pick_images ()
96+
97+ if not files :
98+ print ("No images selected." )
99+ sys .exit (0 )
100+
101+ for path in files :
102+ downscale_image (path , max_bytes )
0 commit comments