-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbatch_resize.py
More file actions
155 lines (122 loc) · 5.13 KB
/
batch_resize.py
File metadata and controls
155 lines (122 loc) · 5.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import os
import sys
import threading
import re
from PIL import Image, ImageOps
# === USER SETTINGS ===
RENAME_FILES = True # ← Set to False to disable filename sanitization
RENAMED_LOG_PATH = "renamed_files.log"
# === Global Counters ===
total_original_size = 0
total_new_size = 0
lock = threading.Lock()
# === Rename Log List ===
renamed_files_log = []
def sanitize_filename(filepath):
"""Renames the file to remove unsafe URL characters and return the new path."""
directory, original_filename = os.path.split(filepath)
name, ext = os.path.splitext(original_filename)
# Replace spaces with dashes and remove unsafe characters
safe_name = re.sub(r'[^A-Za-z0-9\-_.]', '', name.replace(' ', '-'))
safe_filename = f"{safe_name}{ext.lower()}"
new_filepath = os.path.join(directory, safe_filename)
# Avoid overwriting another file
counter = 1
while os.path.exists(new_filepath) and new_filepath != filepath:
safe_filename = f"{safe_name}-{counter}{ext.lower()}"
new_filepath = os.path.join(directory, safe_filename)
counter += 1
if new_filepath != filepath:
os.rename(filepath, new_filepath)
renamed_files_log.append(f"{original_filename} → {os.path.basename(new_filepath)}")
print(f"🔤 Renamed: {original_filename} → {os.path.basename(new_filepath)}")
return new_filepath
def resize_image(image_path, max_width):
"""Resizes and compresses an image while maintaining orientation and format."""
global total_original_size, total_new_size
try:
if RENAME_FILES:
image_path = sanitize_filename(image_path)
original_size = os.path.getsize(image_path)
with Image.open(image_path) as img:
img = ImageOps.exif_transpose(img) # ✅ Preserve correct orientation
width, height = img.size
orig_format = img.format
if width > max_width:
new_height = int((max_width / width) * height)
img = img.resize((max_width, new_height), Image.LANCZOS)
resized = True
else:
resized = False
if resized:
save_args = {"optimize": True}
if orig_format in ["JPEG", "JPG"]:
save_args["quality"] = 85
save_args["format"] = "JPEG"
elif orig_format == "PNG":
save_args["format"] = "PNG"
img.save(image_path, **save_args)
new_size = os.path.getsize(image_path)
with lock:
total_original_size += original_size
total_new_size += new_size
print(f"✔ Resized & optimized: {image_path}")
else:
print(f"🔄 Skipped (no resize needed): {image_path}")
except Exception as e:
print(f"❌ Error processing {image_path}: {e}")
def collect_images_recursively(directory, supported_extensions):
image_paths = []
for root, _, files in os.walk(directory):
for f in files:
if f.lower().endswith(supported_extensions):
image_paths.append(os.path.join(root, f))
return image_paths
def write_rename_log():
"""Writes renamed files to a log file."""
if renamed_files_log:
with open(RENAMED_LOG_PATH, "w", encoding="utf-8") as f:
for line in renamed_files_log:
f.write(f"{line}\n")
print(f"🗒️ Rename log saved to: {RENAMED_LOG_PATH}")
def process_directory(directory, max_width):
if not os.path.isdir(directory):
print(f"❌ Error: Directory '{directory}' not found.")
return
supported_extensions = (".jpg", ".jpeg", ".png")
images = collect_images_recursively(directory, supported_extensions)
if not images:
print("⚠ No valid images found in the specified directory or subdirectories.")
return
print(f"📷 Processing {len(images)} images...")
threads = []
for image in images:
t = threading.Thread(target=resize_image, args=(image, max_width))
t.start()
threads.append(t)
for t in threads:
t.join()
if total_original_size > 0:
saved_bytes = total_original_size - total_new_size
saved_kb = saved_bytes / 1024
percent_saved = (saved_bytes / total_original_size * 100)
print("✅ Batch resizing and optimization completed.")
print(f"📊 Total size before: {total_original_size / 1024:.2f} KB")
print(f"📉 Total size after: {total_new_size / 1024:.2f} KB")
print(f"💾 Total saved: {saved_kb:.2f} KB ({percent_saved:.1f}%)")
else:
print("🔍 No images were resized or optimized.")
if RENAME_FILES:
write_rename_log()
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python batch_resize.py <directory> <max_width>")
sys.exit(1)
source_directory = sys.argv[1]
try:
max_width = int(sys.argv[2])
except ValueError:
print("❌ Error: max_width must be an integer.")
sys.exit(1)
sys.stdout.reconfigure(encoding='utf-8')
process_directory(source_directory, max_width)