Skip to content

Commit 04b12d7

Browse files
authored
Create Image-Watermarking-Tool.py
1 parent 1a98116 commit 04b12d7

1 file changed

Lines changed: 316 additions & 0 deletions

File tree

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
import tkinter as tk
2+
from tkinter import filedialog, messagebox
3+
import ttkbootstrap as tb
4+
from PIL import Image, ImageDraw, ImageFont, ImageTk
5+
import os
6+
7+
# Optional: Drag-and-drop support
8+
try:
9+
from tkinterdnd2 import DND_FILES, TkinterDnD
10+
DND_AVAILABLE = True
11+
except ImportError:
12+
DND_AVAILABLE = False
13+
print("tkinterdnd2 not installed. Drag-and-drop disabled (install via pip install tkinterdnd2)")
14+
15+
# ================= HELPERS =================
16+
def show_error(title, msg):
17+
messagebox.showerror(title, msg)
18+
19+
def show_info(title, msg):
20+
messagebox.showinfo(title, msg)
21+
22+
def add_placeholder(entry, text):
23+
entry.insert(0, text)
24+
entry.config(foreground="grey")
25+
def clear(_):
26+
if entry.get() == text:
27+
entry.delete(0, "end")
28+
entry.config(foreground="black")
29+
def restore(_):
30+
if not entry.get():
31+
entry.insert(0, text)
32+
entry.config(foreground="grey")
33+
entry.bind("<FocusIn>", clear)
34+
entry.bind("<FocusOut>", restore)
35+
36+
# ================= ROOT WINDOW =================
37+
if DND_AVAILABLE:
38+
app = TkinterDnD.Tk()
39+
app.title("Per-Thumbnail Watermark Tool")
40+
app.geometry("1200x600")
41+
else:
42+
app = tb.Window("Per-Thumbnail Watermark Tool", themename="flatly", size=(1200, 600))
43+
44+
# ================= DATA =================
45+
image_data = [] # List of dicts: {"path":..., "position":..., "thumbnail":..., "preview":..., "selected":...}
46+
47+
# ================= SPLIT PANEL =================
48+
left_panel = tb.Labelframe(app, text="Image Preview (Drag & Drop / Browse)", padding=10)
49+
left_panel.pack(side="left", fill="both", expand=True, padx=10, pady=10)
50+
51+
right_panel = tb.Labelframe(app, text="Global Watermark Settings", padding=10)
52+
right_panel.pack(side="right", fill="y", padx=10, pady=10)
53+
54+
# ================= SCROLLABLE GALLERY =================
55+
canvas = tk.Canvas(left_panel)
56+
scrollbar = tk.Scrollbar(left_panel, orient="vertical", command=canvas.yview)
57+
scrollable_frame = tk.Frame(canvas)
58+
scrollable_frame.bind(
59+
"<Configure>",
60+
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
61+
)
62+
canvas.create_window((0,0), window=scrollable_frame, anchor="nw")
63+
canvas.configure(yscrollcommand=scrollbar.set)
64+
canvas.pack(side="left", fill="both", expand=True)
65+
scrollbar.pack(side="right", fill="y")
66+
67+
# ================= IMAGE MANAGEMENT =================
68+
def add_images(paths):
69+
for path in paths:
70+
if os.path.isfile(path) and path not in [d["path"] for d in image_data]:
71+
ext = os.path.splitext(path)[1].lower()
72+
if ext in [".png", ".jpg", ".jpeg", ".bmp", ".gif"]:
73+
image_data.append({
74+
"path": path,
75+
"position": "Bottom-Right",
76+
"thumbnail": None,
77+
"preview": None,
78+
"selected": False
79+
})
80+
update_gallery()
81+
82+
def browse_images():
83+
paths = filedialog.askopenfilenames(filetypes=[("Image Files", "*.png *.jpg *.jpeg *.bmp *.gif")])
84+
add_images(paths)
85+
86+
# ================= DRAG & DROP =================
87+
def drop_files(event):
88+
if DND_AVAILABLE:
89+
# Handle Windows-style paths with braces and spaces
90+
data = event.data
91+
if data.startswith("{") and data.endswith("}"):
92+
files = [f.strip("{}") for f in data.split("} {")]
93+
else:
94+
files = data.split()
95+
add_images(files)
96+
97+
def remove_selected():
98+
global image_data
99+
image_data = [d for d in image_data if not d["selected"]]
100+
update_gallery()
101+
102+
# ================= GLOBAL WATERMARK SETTINGS =================
103+
tb.Label(right_panel, text="Watermark Text").pack(anchor="w", pady=(0,2))
104+
watermark_text = tb.Entry(right_panel, width=40)
105+
watermark_text.pack(anchor="w", pady=(0,5))
106+
add_placeholder(watermark_text, "Enter watermark text")
107+
108+
tb.Label(right_panel, text="Font Size").pack(anchor="w", pady=(5,2))
109+
font_size = tb.Entry(right_panel, width=20)
110+
font_size.pack(anchor="w", pady=(0,5))
111+
add_placeholder(font_size, "36")
112+
113+
tb.Label(right_panel, text="Opacity (0-255)").pack(anchor="w", pady=(5,2))
114+
opacity = tb.Entry(right_panel, width=20)
115+
opacity.pack(anchor="w", pady=(0,5))
116+
add_placeholder(opacity, "128")
117+
118+
tb.Label(right_panel, text="Output Folder").pack(anchor="w", pady=(5,2))
119+
output_path_var = tk.StringVar()
120+
tb.Entry(right_panel, textvariable=output_path_var, width=40).pack(anchor="w", pady=(0,5))
121+
select_btn = tb.Button(right_panel, text="Select Folder", bootstyle="success", command=lambda: output_path_var.set(filedialog.askdirectory()))
122+
select_btn.pack(anchor="w", pady=(0,5))
123+
124+
# ================= THUMBNAIL PREVIEW =================
125+
def generate_preview(image_dict):
126+
try:
127+
img = Image.open(image_dict["path"]).convert("RGBA")
128+
img.thumbnail((150,150))
129+
preview = img.copy()
130+
text = watermark_text.get().strip()
131+
if text and text.lower() != "enter watermark text":
132+
try:
133+
size = int(font_size.get())
134+
except:
135+
size = 36
136+
try:
137+
alpha = int(opacity.get())
138+
if not (0 <= alpha <= 255):
139+
alpha = 128
140+
except:
141+
alpha = 128
142+
watermark_layer = Image.new("RGBA", preview.size, (255,255,255,0))
143+
draw = ImageDraw.Draw(watermark_layer)
144+
try:
145+
font = ImageFont.truetype("arial.ttf", size)
146+
except:
147+
font = ImageFont.load_default()
148+
bbox = draw.textbbox((0,0), text, font=font)
149+
text_width = bbox[2]-bbox[0]
150+
text_height = bbox[3]-bbox[1]
151+
pos = image_dict.get("position","Bottom-Right")
152+
x = y = 0
153+
if pos=="Top-Left": x,y=10,10
154+
elif pos=="Top-Right": x,y=preview.width-text_width-10,10
155+
elif pos=="Bottom-Left": x,y=10,preview.height-text_height-10
156+
elif pos=="Bottom-Right": x,y=preview.width-text_width-10,preview.height-text_height-10
157+
elif pos=="Center": x,y=(preview.width-text_width)//2,(preview.height-text_height)//2
158+
draw.text((x,y), text, fill=(255,255,255,alpha), font=font)
159+
preview = Image.alpha_composite(preview, watermark_layer)
160+
image_dict["preview"] = ImageTk.PhotoImage(preview)
161+
except Exception as e:
162+
print(f"Preview error: {e}")
163+
164+
# ================= GALLERY UPDATE (grid version) =================
165+
def update_gallery():
166+
for widget in scrollable_frame.winfo_children():
167+
widget.destroy()
168+
columns = 5
169+
for idx, d in enumerate(image_data):
170+
generate_preview(d)
171+
lbl_frame = tk.Frame(scrollable_frame, bd=2, relief="groove")
172+
lbl_frame.grid(row=idx//columns, column=idx%columns, padx=5, pady=5)
173+
lbl = tk.Label(lbl_frame, image=d["preview"])
174+
lbl.image = d["preview"]
175+
lbl.pack()
176+
# Position combobox per thumbnail
177+
pos_cb = tb.Combobox(lbl_frame, values=["Top-Left","Top-Right","Bottom-Left","Bottom-Right","Center"],
178+
state="readonly", width=12)
179+
pos_cb.set(d.get("position","Bottom-Right"))
180+
pos_cb.pack(pady=5)
181+
def cb_callback(event, img_dict=d, label=lbl):
182+
img_dict["position"] = pos_cb.get()
183+
generate_preview(img_dict)
184+
label.configure(image=img_dict["preview"])
185+
label.image = img_dict["preview"]
186+
pos_cb.bind("<<ComboboxSelected>>", cb_callback)
187+
# Selection toggle
188+
def toggle_select(e, img_dict=d, frame=lbl_frame):
189+
img_dict["selected"] = not img_dict["selected"]
190+
frame.config(bd=4, relief="sunken" if img_dict["selected"] else "groove")
191+
lbl_frame.bind("<Button-1>", toggle_select)
192+
lbl.bind("<Button-1>", toggle_select)
193+
194+
def process_image(d, text, size, alpha, out_dir):
195+
try:
196+
img = Image.open(d["path"]).convert("RGBA")
197+
watermark_layer = Image.new("RGBA", img.size, (255,255,255,0))
198+
draw = ImageDraw.Draw(watermark_layer)
199+
200+
try:
201+
font = ImageFont.truetype("arial.ttf", size)
202+
except:
203+
font = ImageFont.load_default()
204+
205+
bbox = draw.textbbox((0,0), text, font=font)
206+
text_width = bbox[2] - bbox[0]
207+
text_height = bbox[3] - bbox[1]
208+
209+
pos = d.get("position","Bottom-Right")
210+
if pos=="Top-Left": x,y=10,10
211+
elif pos=="Top-Right": x,y=img.width-text_width-10,10
212+
elif pos=="Bottom-Left": x,y=10,img.height-text_height-10
213+
elif pos=="Bottom-Right": x,y=img.width-text_width-10,img.height-text_height-10
214+
else: x,y=(img.width-text_width)//2,(img.height-text_height)//2
215+
216+
draw.text((x,y), text, fill=(255,255,255,alpha), font=font)
217+
218+
watermarked = Image.alpha_composite(img, watermark_layer)
219+
base_name = os.path.basename(d["path"])
220+
watermarked.convert("RGB").save(os.path.join(out_dir, base_name))
221+
222+
progress_var.set(progress_var.get() + 1)
223+
app.update_idletasks()
224+
225+
except Exception as e:
226+
print(f"Failed: {d['path']} -> {e}")
227+
228+
def reset_progress_ui():
229+
apply_btn.config(state="normal")
230+
progress_bar.pack_forget()
231+
progress_var.set(0)
232+
233+
# ================= APPLY WATERMARK =================
234+
def apply_watermark():
235+
progress_bar.pack(fill="x", pady=(0, 5))
236+
progress_bar["maximum"] = len(image_data)
237+
progress_var.set(0)
238+
select_btn.config(state="disabled")
239+
browse_btn.config(state="disabled")
240+
remove_btn.config(state="disabled")
241+
apply_btn.config(state="disabled")
242+
app.update_idletasks()
243+
244+
out_dir = output_path_var.get().strip()
245+
if not out_dir or not os.path.isdir(out_dir):
246+
show_error("Error", "Please select a valid output folder.")
247+
reset_progress_ui()
248+
return
249+
text = watermark_text.get().strip()
250+
if not text or text.lower()=="enter watermark text":
251+
show_error("Error", "Please enter watermark text.")
252+
return
253+
try:
254+
size = int(font_size.get())
255+
except:
256+
size = 36
257+
try:
258+
alpha = int(opacity.get())
259+
if not (0<=alpha<=255): alpha=128
260+
except:
261+
alpha=128
262+
count=0
263+
for d in image_data:
264+
process_image(d, text, size, alpha, out_dir)
265+
count += 1
266+
show_info("Done", f"Watermarked {count} image(s) successfully.")
267+
select_btn.config(state="normal")
268+
browse_btn.config(state="normal")
269+
remove_btn.config(state="normal")
270+
apply_btn.config(state="normal")
271+
progress_bar.pack_forget()
272+
progress_var.set(0)
273+
274+
# ================= BUTTONS =================
275+
browse_btn= tb.Button(
276+
right_panel,
277+
text="Browse Images",
278+
bootstyle="info",
279+
command=browse_images
280+
)
281+
browse_btn.pack(pady=5, fill="x")
282+
283+
remove_btn = tb.Button(
284+
right_panel,
285+
text="Remove Selected",
286+
bootstyle="danger",
287+
command=remove_selected
288+
)
289+
remove_btn.pack(pady=5, fill="x")
290+
291+
apply_btn = tb.Button(
292+
right_panel,
293+
text="Apply Watermark to All",
294+
bootstyle="success",
295+
command=apply_watermark
296+
)
297+
apply_btn.pack(pady=(10, 5), fill="x")
298+
299+
# Progress bar (hidden initially)
300+
progress_var = tk.IntVar(value=0)
301+
progress_bar = tb.Progressbar(
302+
right_panel,
303+
variable=progress_var,
304+
maximum=100,
305+
mode="determinate"
306+
)
307+
progress_bar.pack(fill="x", pady=(0, 5))
308+
progress_bar.pack_forget() # hide until needed
309+
310+
# ================= DRAG & DROP =================
311+
if DND_AVAILABLE:
312+
left_panel.drop_target_register(DND_FILES)
313+
left_panel.dnd_bind("<<Drop>>", drop_files)
314+
315+
update_gallery()
316+
app.mainloop()

0 commit comments

Comments
 (0)