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