Skip to content

Commit 33df1c0

Browse files
authored
Create Image-resizer.py
1 parent f51b3ff commit 33df1c0

1 file changed

Lines changed: 197 additions & 0 deletions

File tree

63-Image-resizer/Image-resizer.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import os
2+
from pathlib import Path
3+
import threading
4+
from concurrent.futures import ThreadPoolExecutor
5+
import tkinter as tk
6+
from tkinter import filedialog, messagebox
7+
from PIL import Image
8+
import ttkbootstrap as tb
9+
from ttkbootstrap.constants import *
10+
import json
11+
12+
# ---------------- CONFIG ---------------- #
13+
CONFIG_FILE = "resizer_config.json"
14+
15+
def load_last_folder():
16+
if os.path.exists(CONFIG_FILE):
17+
try:
18+
with open(CONFIG_FILE, "r") as f:
19+
data = json.load(f)
20+
return data.get("last_folder", "")
21+
except:
22+
return ""
23+
return ""
24+
25+
def save_last_folder(folder):
26+
try:
27+
with open(CONFIG_FILE, "w") as f:
28+
json.dump({"last_folder": folder}, f)
29+
except:
30+
pass
31+
32+
# ---------------- GLOBALS ---------------- #
33+
image_list = []
34+
pending_images = []
35+
stop_flag = False
36+
current_percent = 100 # Live adjustable percentage
37+
38+
# ---------------- FUNCTIONS ---------------- #
39+
def select_folder():
40+
folder = filedialog.askdirectory(title="Select source folder")
41+
if folder:
42+
folder_var.set(folder)
43+
save_last_folder(folder)
44+
load_images(folder)
45+
46+
def load_images(folder):
47+
global image_list
48+
image_list.clear()
49+
for ext in ("*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif"):
50+
image_list.extend(Path(folder).glob(ext))
51+
count_var.set(f"{len(image_list)} images loaded")
52+
53+
def on_percent_change(val):
54+
"""Update the live percentage from the slider"""
55+
global current_percent
56+
try:
57+
current_percent = int(float(val)) # <-- convert float string to int safely
58+
except ValueError:
59+
current_percent = 100 # fallback default
60+
61+
def process_image(img_path, output_folder, overwrite):
62+
"""Process a single image using live current_percent"""
63+
save_path = os.path.join(output_folder, img_path.name)
64+
if os.path.exists(save_path) and not overwrite:
65+
return "skipped"
66+
try:
67+
img = Image.open(img_path)
68+
w = max(1, img.width * current_percent // 100)
69+
h = max(1, img.height * current_percent // 100)
70+
resized = img.resize((w, h), Image.Resampling.LANCZOS)
71+
resized.save(save_path)
72+
return "processed"
73+
except Exception as e:
74+
return f"error: {img_path.name}"
75+
76+
def resize_images_advanced(overwrite=False):
77+
"""Multi-threaded resizing with cancel/resume and live percentage adjustment"""
78+
global pending_images, stop_flag
79+
if not image_list:
80+
messagebox.showwarning("Warning", "No images loaded.")
81+
return
82+
83+
source_folder = folder_var.get()
84+
output_folder = os.path.join(source_folder, "Resized")
85+
os.makedirs(output_folder, exist_ok=True)
86+
87+
if not pending_images:
88+
pending_images = list(image_list)
89+
90+
processed_count = 0
91+
skipped_count = 0
92+
error_list = []
93+
94+
total_images = len(pending_images)
95+
progress_var.set(0)
96+
app.update_idletasks()
97+
98+
max_workers = min(8, os.cpu_count() or 4)
99+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
100+
futures = {}
101+
for img_path in pending_images:
102+
if stop_flag:
103+
break
104+
future = executor.submit(process_image, img_path, output_folder, overwrite)
105+
futures[future] = img_path
106+
107+
for i, (future, img_path) in enumerate(futures.items(), 1):
108+
if stop_flag:
109+
break
110+
result = future.result()
111+
if result == "processed":
112+
processed_count += 1
113+
elif result == "skipped":
114+
skipped_count += 1
115+
else:
116+
error_list.append(result)
117+
progress_var.set(i / total_images * 100)
118+
status_var.set(f"Processing: {i}/{total_images} ({current_percent}%)")
119+
app.update_idletasks()
120+
121+
# Remove processed images from pending list
122+
pending_images = pending_images[i:] if not stop_flag else pending_images[i-1:]
123+
124+
if not stop_flag:
125+
status_var.set("Done")
126+
message = f"Processed {processed_count} images.\nSkipped {skipped_count} images."
127+
if error_list:
128+
message += f"\nErrors: {len(error_list)} (see console)"
129+
print("Errors during processing:")
130+
for err in error_list:
131+
print(err)
132+
message += f"\nSaved in:\n{output_folder}"
133+
messagebox.showinfo("Batch Resize Complete", message)
134+
pending_images = []
135+
progress_var.set(0)
136+
else:
137+
status_var.set("Paused/Cancelled")
138+
messagebox.showinfo("Paused", f"Batch paused. {len(pending_images)} images remaining.")
139+
140+
def start_advanced_resize():
141+
global stop_flag
142+
stop_flag = False
143+
threading.Thread(target=resize_images_advanced, args=(bool(overwrite_var.get()),), daemon=True).start()
144+
145+
def cancel_resize():
146+
global stop_flag
147+
stop_flag = True
148+
149+
def resume_resize():
150+
global stop_flag
151+
if pending_images:
152+
stop_flag = False
153+
threading.Thread(target=resize_images_advanced, args=(bool(overwrite_var.get()),), daemon=True).start()
154+
else:
155+
messagebox.showinfo("Info", "No pending images to resume.")
156+
157+
# ---------------- GUI ---------------- #
158+
app = tb.Window(themename="darkly", title="Professional Live Batch Resizer", size=(600, 450))
159+
160+
folder_var = tk.StringVar(value=load_last_folder())
161+
count_var = tk.StringVar(value="0 images loaded")
162+
percent_var = tk.IntVar(value=current_percent)
163+
overwrite_var = tk.IntVar(value=0)
164+
progress_var = tk.DoubleVar(value=0)
165+
status_var = tk.StringVar(value="Idle")
166+
167+
# Folder selection
168+
tb.Label(app, text="Source Folder:").pack(pady=5)
169+
folder_frame = tb.Frame(app)
170+
folder_frame.pack(fill=tk.X, padx=10)
171+
tb.Entry(folder_frame, textvariable=folder_var, state="readonly").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
172+
tb.Button(folder_frame, text="Browse", bootstyle="primary", command=select_folder).pack(side=tk.LEFT)
173+
tb.Button(folder_frame, text="Load Images", bootstyle="secondary", command=lambda: load_images(folder_var.get())).pack(side=tk.LEFT, padx=5)
174+
175+
# Image count
176+
tb.Label(app, textvariable=count_var, font=("Segoe UI", 10, "bold")).pack(pady=5)
177+
178+
# Resize percentage (live)
179+
tb.Label(app, text="Resize Percentage (30% - 300%)").pack(pady=5)
180+
tb.Scale(app, from_=30, to=300, orient=tk.HORIZONTAL, variable=percent_var, command=on_percent_change).pack(fill=tk.X, padx=20)
181+
182+
# Overwrite checkbox
183+
tb.Checkbutton(app, text="Overwrite existing resized images", variable=overwrite_var, bootstyle="info").pack(pady=5)
184+
185+
# Progress bar and status
186+
tb.Label(app, text="Progress:").pack(pady=5)
187+
tb.Progressbar(app, variable=progress_var, maximum=100).pack(fill=tk.X, padx=20, pady=5)
188+
tb.Label(app, textvariable=status_var).pack(pady=5)
189+
190+
# Buttons
191+
button_frame = tb.Frame(app)
192+
button_frame.pack(pady=10, fill=tk.X, padx=20)
193+
tb.Button(button_frame, text="🚀 Start", bootstyle="success", command=start_advanced_resize).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5)
194+
tb.Button(button_frame, text="⏸ Cancel", bootstyle="warning", command=cancel_resize).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5)
195+
tb.Button(button_frame, text="▶ Resume", bootstyle="info", command=resume_resize).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5)
196+
197+
app.mainloop()

0 commit comments

Comments
 (0)