|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import sqlite3 |
| 4 | +from threading import Thread |
| 5 | +from PIL import Image, ImageTk |
| 6 | + |
| 7 | +try: |
| 8 | + from PIL import ImageResampling |
| 9 | + RESAMPLE = ImageResampling.LANCZOS |
| 10 | +except: |
| 11 | + RESAMPLE = Image.LANCZOS |
| 12 | + |
| 13 | +import ttkbootstrap as tb |
| 14 | +from ttkbootstrap.constants import * |
| 15 | +from tkinter import filedialog, messagebox, Listbox, Canvas, Scrollbar |
| 16 | +from tkinterdnd2 import TkinterDnD, DND_FILES |
| 17 | + |
| 18 | +# ---------------- APP INFO ---------------- |
| 19 | +APP_NAME = "ImageConvert PRO" |
| 20 | +APP_VERSION = "1.1" |
| 21 | + |
| 22 | +# ---------------- PATH ---------------- |
| 23 | +BASE_DIR = os.path.dirname(sys.argv[0]) |
| 24 | +DB_NAME = os.path.join(BASE_DIR, "snapconvert.db") |
| 25 | +OUTPUT_DIR = os.path.join(BASE_DIR, "converted") |
| 26 | + |
| 27 | +# ---------------- DATABASE ---------------- |
| 28 | +def init_db(): |
| 29 | + conn = sqlite3.connect(DB_NAME) |
| 30 | + c = conn.cursor() |
| 31 | + c.execute("""CREATE TABLE IF NOT EXISTS history( |
| 32 | + id INTEGER PRIMARY KEY, |
| 33 | + name TEXT, |
| 34 | + original TEXT, |
| 35 | + converted TEXT)""") |
| 36 | + conn.commit() |
| 37 | + conn.close() |
| 38 | + |
| 39 | +def insert_db(name, orig, conv): |
| 40 | + conn = sqlite3.connect(DB_NAME) |
| 41 | + c = conn.cursor() |
| 42 | + c.execute("INSERT INTO history(name, original, converted) VALUES(?,?,?)",(name,orig,conv)) |
| 43 | + conn.commit() |
| 44 | + conn.close() |
| 45 | + |
| 46 | +def fetch_db(): |
| 47 | + conn = sqlite3.connect(DB_NAME) |
| 48 | + c = conn.cursor() |
| 49 | + c.execute("SELECT name, original, converted FROM history ORDER BY id DESC") |
| 50 | + rows = c.fetchall() |
| 51 | + conn.close() |
| 52 | + return rows |
| 53 | + |
| 54 | +def clear_history(): |
| 55 | + conn = sqlite3.connect(DB_NAME) |
| 56 | + c = conn.cursor() |
| 57 | + c.execute("DELETE FROM history") |
| 58 | + conn.commit() |
| 59 | + conn.close() |
| 60 | + |
| 61 | +# ---------------- ABOUT ---------------- |
| 62 | +def show_about(): |
| 63 | + messagebox.showinfo( |
| 64 | + f"About {APP_NAME}", |
| 65 | + f"{APP_NAME} v{APP_VERSION}\n\n" |
| 66 | + "Professional Image Converter\n\n" |
| 67 | + "© 2026 Mate Technologies\n" |
| 68 | + "https://matetools.gumroad.com" |
| 69 | + ) |
| 70 | + |
| 71 | +# ---------------- WORKER ---------------- |
| 72 | +def worker(images, fmt, out, quality, resize, keep, progress, finish): |
| 73 | + os.makedirs(out, exist_ok=True) |
| 74 | + total = len(images) |
| 75 | + count = 0 |
| 76 | + |
| 77 | + for i, path in enumerate(images): |
| 78 | + try: |
| 79 | + with Image.open(path) as img: |
| 80 | + if resize > 0: |
| 81 | + img = img.resize((resize, resize), RESAMPLE) |
| 82 | + |
| 83 | + if fmt == "JPEG" and img.mode in ("RGBA","P"): |
| 84 | + img = img.convert("RGB") |
| 85 | + |
| 86 | + name = os.path.splitext(os.path.basename(path))[0] |
| 87 | + if not keep: |
| 88 | + name += f"_{i+1}" |
| 89 | + |
| 90 | + out_path = os.path.join(out, f"{name}.{fmt.lower()}") |
| 91 | + |
| 92 | + c = 1 |
| 93 | + while os.path.exists(out_path): |
| 94 | + out_path = os.path.join(out, f"{name}_{c}.{fmt.lower()}") |
| 95 | + c += 1 |
| 96 | + |
| 97 | + params = {"quality": quality} if fmt == "JPEG" else {} |
| 98 | + img.save(out_path, fmt, **params) |
| 99 | + |
| 100 | + insert_db(name, path, out_path) |
| 101 | + count += 1 |
| 102 | + |
| 103 | + except Exception as e: |
| 104 | + print("Error:", e) |
| 105 | + |
| 106 | + progress(int((i+1)/total*100)) |
| 107 | + |
| 108 | + finish(count) |
| 109 | + |
| 110 | +# ---------------- APP ---------------- |
| 111 | +class App: |
| 112 | + def __init__(self): |
| 113 | + self.root = TkinterDnD.Tk() |
| 114 | + self.root.title(APP_NAME) |
| 115 | + self.root.geometry("1200x750") |
| 116 | + self.style = tb.Style("darkly") |
| 117 | + |
| 118 | + self.images = [] |
| 119 | + self.thumbs = [] |
| 120 | + |
| 121 | + self.create_menu() |
| 122 | + self.build_ui() |
| 123 | + self.load_history() |
| 124 | + |
| 125 | + self.root.drop_target_register(DND_FILES) |
| 126 | + self.root.dnd_bind("<<Drop>>", self.drop) |
| 127 | + |
| 128 | + # MENU |
| 129 | + def create_menu(self): |
| 130 | + menubar = tb.Menu(self.root) |
| 131 | + help_menu = tb.Menu(menubar, tearoff=0) |
| 132 | + help_menu.add_command(label="About", command=show_about) |
| 133 | + menubar.add_cascade(label="Help", menu=help_menu) |
| 134 | + self.root.config(menu=menubar) |
| 135 | + |
| 136 | + # UI |
| 137 | + def build_ui(self): |
| 138 | + main = tb.Frame(self.root) |
| 139 | + main.pack(fill=BOTH, expand=True) |
| 140 | + |
| 141 | + # LEFT |
| 142 | + left = tb.Frame(main, width=250, padding=5) |
| 143 | + left.pack(side=LEFT, fill=Y) |
| 144 | + |
| 145 | + tb.Label(left, text="📂 Files", font=("Arial", 12)).pack(pady=5) |
| 146 | + |
| 147 | + self.listbox = Listbox(left, bg="#1e1e1e", fg="white") |
| 148 | + self.listbox.pack(fill=BOTH, expand=True, pady=5) |
| 149 | + |
| 150 | + tb.Button(left, text="Add Images", command=self.add_files, bootstyle=SUCCESS).pack(fill=X, pady=2) |
| 151 | + tb.Button(left, text="Add Folder", command=self.add_folder, bootstyle=INFO).pack(fill=X, pady=2) |
| 152 | + tb.Button(left, text="Remove", command=self.remove_selected, bootstyle=DANGER).pack(fill=X, pady=2) |
| 153 | + tb.Button(left, text="Clear All", command=self.clear_all, bootstyle=SECONDARY).pack(fill=X, pady=2) |
| 154 | + |
| 155 | + # CENTER |
| 156 | + center = tb.Frame(main, padding=5) |
| 157 | + center.pack(side=LEFT, fill=BOTH, expand=True) |
| 158 | + |
| 159 | + self.canvas = Canvas(center, bg="#121212") |
| 160 | + self.scroll = Scrollbar(center, command=self.canvas.yview) |
| 161 | + |
| 162 | + self.inner = tb.Frame(self.canvas) |
| 163 | + |
| 164 | + self.inner.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) |
| 165 | + |
| 166 | + self.canvas.create_window((0,0), window=self.inner, anchor="nw") |
| 167 | + self.canvas.configure(yscrollcommand=self.scroll.set) |
| 168 | + |
| 169 | + self.canvas.pack(side=LEFT, fill=BOTH, expand=True) |
| 170 | + self.scroll.pack(side=RIGHT, fill=Y) |
| 171 | + |
| 172 | + # RIGHT |
| 173 | + right = tb.Frame(main, width=260, padding=10) |
| 174 | + right.pack(side=RIGHT, fill=Y) |
| 175 | + |
| 176 | + tb.Label(right, text="⚙ Settings", font=("Arial", 12)).pack(pady=5) |
| 177 | + |
| 178 | + tb.Label(right, text="Format").pack(anchor="w") |
| 179 | + self.format = tb.Combobox(right, values=["PNG","JPEG","WEBP","BMP","TIFF"]) |
| 180 | + self.format.current(0) |
| 181 | + self.format.pack(fill=X, pady=5) |
| 182 | + |
| 183 | + tb.Label(right, text="JPEG Quality").pack(anchor="w") |
| 184 | + self.quality = tb.Spinbox(right, from_=10, to=100) |
| 185 | + self.quality.set(90) |
| 186 | + self.quality.pack(fill=X, pady=5) |
| 187 | + |
| 188 | + tb.Label(right, text="Resize (px)").pack(anchor="w") |
| 189 | + self.resize = tb.Spinbox(right, from_=0, to=5000) |
| 190 | + self.resize.set(0) |
| 191 | + self.resize.pack(fill=X, pady=5) |
| 192 | + |
| 193 | + self.keep = tb.Checkbutton(right, text="Keep original filename") |
| 194 | + self.keep.invoke() |
| 195 | + self.keep.pack(pady=5) |
| 196 | + |
| 197 | + tb.Separator(right).pack(fill=X, pady=10) |
| 198 | + |
| 199 | + tb.Button(right, text="🚀 Convert", command=self.convert, bootstyle=WARNING).pack(fill=X, pady=5) |
| 200 | + |
| 201 | + self.progress = tb.Progressbar(right) |
| 202 | + self.progress.pack(fill=X, pady=5) |
| 203 | + |
| 204 | + self.status = tb.Label(right, text="Ready") |
| 205 | + self.status.pack(pady=5) |
| 206 | + |
| 207 | + tb.Separator(right).pack(fill=X, pady=10) |
| 208 | + |
| 209 | + tb.Button(right, text="🧹 Delete History", command=self.delete_history, bootstyle=DANGER).pack(fill=X, pady=5) |
| 210 | + |
| 211 | + # HISTORY TABLE |
| 212 | + self.table = tb.Treeview(self.root, columns=("n","o","c"), show="headings") |
| 213 | + self.table.heading("n", text="Name") |
| 214 | + self.table.heading("o", text="Original") |
| 215 | + self.table.heading("c", text="Converted") |
| 216 | + self.table.pack(fill=BOTH, expand=True) |
| 217 | + |
| 218 | + # FUNCTIONS |
| 219 | + def drop(self, e): |
| 220 | + self.add_images(self.root.tk.splitlist(e.data)) |
| 221 | + |
| 222 | + def add_files(self): |
| 223 | + self.add_images(filedialog.askopenfilenames()) |
| 224 | + |
| 225 | + def add_folder(self): |
| 226 | + folder = filedialog.askdirectory() |
| 227 | + imgs = [] |
| 228 | + for r,_,f in os.walk(folder): |
| 229 | + for x in f: |
| 230 | + if x.lower().endswith(("png","jpg","jpeg","bmp","gif")): |
| 231 | + imgs.append(os.path.join(r,x)) |
| 232 | + self.add_images(imgs) |
| 233 | + |
| 234 | + def add_images(self, paths): |
| 235 | + for p in paths: |
| 236 | + if p not in self.images: |
| 237 | + self.images.append(p) |
| 238 | + self.listbox.insert(END, os.path.basename(p)) |
| 239 | + self.render_gallery() |
| 240 | + |
| 241 | + def remove_selected(self): |
| 242 | + sel = list(self.listbox.curselection()) |
| 243 | + sel.reverse() |
| 244 | + for i in sel: |
| 245 | + self.images.pop(i) |
| 246 | + self.listbox.delete(i) |
| 247 | + self.render_gallery() |
| 248 | + |
| 249 | + def clear_all(self): |
| 250 | + self.images = [] |
| 251 | + self.listbox.delete(0, END) |
| 252 | + for w in self.inner.winfo_children(): |
| 253 | + w.destroy() |
| 254 | + |
| 255 | + def render_gallery(self): |
| 256 | + for w in self.inner.winfo_children(): |
| 257 | + w.destroy() |
| 258 | + self.thumbs.clear() |
| 259 | + |
| 260 | + cols = 4 |
| 261 | + for i, path in enumerate(self.images[:50]): |
| 262 | + try: |
| 263 | + img = Image.open(path) |
| 264 | + img.thumbnail((150,150)) |
| 265 | + tkimg = ImageTk.PhotoImage(img) |
| 266 | + self.thumbs.append(tkimg) |
| 267 | + |
| 268 | + frame = tb.Frame(self.inner) |
| 269 | + frame.grid(row=i//cols, column=i%cols, padx=10, pady=10) |
| 270 | + |
| 271 | + tb.Label(frame, image=tkimg).pack() |
| 272 | + tb.Label(frame, text=os.path.basename(path), wraplength=140).pack() |
| 273 | + except: |
| 274 | + pass |
| 275 | + |
| 276 | + def convert(self): |
| 277 | + if not self.images: |
| 278 | + messagebox.showwarning("No images", "Add images first") |
| 279 | + return |
| 280 | + |
| 281 | + fmt = self.format.get() |
| 282 | + q = int(self.quality.get()) |
| 283 | + r = int(self.resize.get()) |
| 284 | + keep = self.keep.instate(["selected"]) |
| 285 | + |
| 286 | + def prog(v): |
| 287 | + self.root.after(0, lambda: ( |
| 288 | + self.progress.config(value=v), |
| 289 | + self.status.config(text=f"{v}%") |
| 290 | + )) |
| 291 | + |
| 292 | + def done(c): |
| 293 | + self.root.after(0, lambda: ( |
| 294 | + self.status.config(text=f"Done: {c} images"), |
| 295 | + self.progress.config(value=0), |
| 296 | + self.load_history() |
| 297 | + )) |
| 298 | + |
| 299 | + Thread(target=worker, args=(self.images,fmt,OUTPUT_DIR,q,r,keep,prog,done), daemon=True).start() |
| 300 | + |
| 301 | + def delete_history(self): |
| 302 | + if messagebox.askyesno("Confirm", "Delete all history?"): |
| 303 | + clear_history() |
| 304 | + self.load_history() |
| 305 | + |
| 306 | + def load_history(self): |
| 307 | + for i in self.table.get_children(): |
| 308 | + self.table.delete(i) |
| 309 | + for row in fetch_db(): |
| 310 | + self.table.insert("",END,values=row) |
| 311 | + |
| 312 | + def run(self): |
| 313 | + self.root.mainloop() |
| 314 | + |
| 315 | +# RUN |
| 316 | +if __name__ == "__main__": |
| 317 | + init_db() |
| 318 | + App().run() |
0 commit comments