kopok122

The Architect Image Tools v1

Aug 23rd, 2025
25
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 142.20 KB | None | 0 0
  1. --- Merged Files Compilation ---
  2. Source Directory: C:\Scripts\Random_Scripts\New folder
  3. Generated on: 2025-08-24 10:29:53
  4. ================================================================================
  5.  
  6. --- START: enhanced_ocr_filtered.py ---
  7. --------------------------------------------------------------------------------
  8. import os
  9. import sys
  10. import json
  11. import re
  12. import pytesseract
  13. from PIL import Image
  14. from PIL.ExifTags import TAGS
  15. import tkinter as tk
  16. from tkinter import scrolledtext, messagebox, filedialog, ttk, Listbox, Checkbutton
  17. import winreg
  18. import threading
  19. from difflib import SequenceMatcher
  20. import subprocess
  21. import string
  22.  
  23. # --- New Dependency ---
  24. try:
  25. import piexif
  26. except ImportError:
  27. # This will be handled in the main execution block
  28. pass
  29. # --------------------
  30.  
  31. # Configuration
  32. SETTINGS_FILE = "C:\\Scripts\\Saved_json\\merged_ocr_settings.json"
  33. DEFAULT_SETTINGS = {
  34. "custom_text": "",
  35. "tesseract_path": r"C:\\Program Files\\Tesseract-OCR\\tesseract.exe",
  36. "output_filename": "combined_text.txt",
  37. "custom_separator": "////",
  38. "merge_custom_name": "",
  39. "delete_txt_after_merge": False,
  40. "similarity_threshold": 0.8,
  41. "metadata_prefix": "",
  42. "delete_txt_after_merge_metadata": False,
  43. "include_filename_in_ocr_output": False,
  44. "custom_text_templates": { # NEW: Template system
  45. "Basic Info": "Date: \nLocation: \nNotes: ",
  46. "Document Analysis": "Document Type: \nKey Points: \nAction Items: \nDate Processed: ",
  47. "Receipt/Invoice": "Vendor: \nAmount: \nDate: \nCategory: \nPayment Method: ",
  48. "Meeting Notes": "Meeting: \nAttendees: \nKey Decisions: \nAction Items: \nNext Steps: ",
  49. "Research Notes": "Source: \nTopic: \nKey Findings: \nRelevance: \nFollow-up: ",
  50. "Photo Documentation": "Subject: \nDate Taken: \nLocation: \nDescription: \nContext: ",
  51. "Medical Records": "Patient: \nDate: \nProvider: \nDiagnosis: \nTreatment: \nNotes: ",
  52. "Legal Documents": "Document Type: \nParties: \nDate: \nKey Terms: \nStatus: \nNotes: "
  53. },
  54. "selected_template": "Basic Info", # NEW: Remember last selected template
  55. # NEW: Text filtering settings
  56. "enable_text_filtering": True,
  57. "min_word_length": 2,
  58. "max_gibberish_ratio": 0.4,
  59. "remove_single_chars": True,
  60. "min_sentence_words": 3,
  61. "filter_excessive_punctuation": True,
  62. "preserve_numbers": True,
  63. "preserve_currency": True
  64. }
  65.  
  66. class MergedOCRApp:
  67. def __init__(self, root):
  68. self.root = root
  69. self.root.title("Enhanced OCR & File Merger with Text Filtering")
  70. self.root.geometry("700x1050") # Increased height for filtering options
  71.  
  72. # Processing control variables for OCR
  73. self.is_processing_ocr = False
  74. self.stop_processing_ocr = False
  75. self.ocr_processing_thread = None
  76.  
  77. # Processing control variables for Metadata Embedding
  78. self.is_processing_metadata = False
  79. self.stop_processing_metadata = False
  80. self.metadata_processing_thread = None
  81.  
  82. # Ensure settings path exists
  83. os.makedirs(os.path.dirname(SETTINGS_FILE), exist_ok=True)
  84. self.settings = self.load_settings()
  85.  
  86. # Check for Tesseract path validity
  87. tesseract_path = self.settings.get("tesseract_path", DEFAULT_SETTINGS["tesseract_path"])
  88. if os.path.exists(tesseract_path):
  89. pytesseract.pytesseract.tesseract_cmd = tesseract_path
  90. else:
  91. messagebox.showwarning("Tesseract Not Found", f"Tesseract executable not found at:\n{tesseract_path}\nPlease correct the path in the settings file:\n{SETTINGS_FILE}")
  92.  
  93. self.folder_path = tk.StringVar()
  94. self.create_gui()
  95.  
  96. if len(sys.argv) > 1 and os.path.isdir(sys.argv[1]):
  97. self.folder_path.set(sys.argv[1])
  98.  
  99. # Auto-save settings after GUI loaded
  100. self.root.after(1000, self.auto_save_settings)
  101.  
  102. def load_settings(self):
  103. try:
  104. if os.path.exists(SETTINGS_FILE):
  105. with open(SETTINGS_FILE, 'r') as f:
  106. loaded_settings = json.load(f)
  107. # Ensure all default keys exist
  108. for key, value in DEFAULT_SETTINGS.items():
  109. if key not in loaded_settings:
  110. loaded_settings[key] = value
  111. return loaded_settings
  112. except Exception as e:
  113. print(f"Error loading settings: {e}")
  114. return DEFAULT_SETTINGS.copy()
  115.  
  116. def _write_settings_to_file(self):
  117. try:
  118. with open(SETTINGS_FILE, 'w') as f:
  119. json.dump(self.settings, f, indent=4)
  120. except Exception as e:
  121. messagebox.showerror("Error", f"Failed to save settings: {e}")
  122.  
  123. def auto_save_settings(self):
  124. try:
  125. self.settings["custom_text"] = self.custom_text.get("1.0", tk.END).strip()
  126. self.settings["output_filename"] = self.output_name_var.get()
  127. self.settings["custom_separator"] = self.separator_var.get()
  128. self.settings["merge_custom_name"] = self.merge_custom_name_var.get()
  129. self.settings["delete_txt_after_merge"] = self.delete_txt_var.get()
  130. self.settings["similarity_threshold"] = self.similarity_scale.get()
  131. self.settings["metadata_prefix"] = self.metadata_prefix_var.get()
  132. self.settings["delete_txt_after_merge_metadata"] = self.delete_txt_metadata_var.get()
  133. self.settings["include_filename_in_ocr_output"] = self.include_filename_ocr_var.get()
  134. # NEW: Save selected template
  135. if hasattr(self, 'template_var'):
  136. self.settings["selected_template"] = self.template_var.get()
  137. # NEW: Save text filtering settings
  138. if hasattr(self, 'enable_filtering_var'):
  139. self.settings["enable_text_filtering"] = self.enable_filtering_var.get()
  140. self.settings["min_word_length"] = self.min_word_length_var.get()
  141. self.settings["max_gibberish_ratio"] = self.gibberish_ratio_scale.get()
  142. self.settings["remove_single_chars"] = self.remove_single_chars_var.get()
  143. self.settings["min_sentence_words"] = self.min_sentence_words_var.get()
  144. self.settings["filter_excessive_punctuation"] = self.filter_punctuation_var.get()
  145. self.settings["preserve_numbers"] = self.preserve_numbers_var.get()
  146. self.settings["preserve_currency"] = self.preserve_currency_var.get()
  147.  
  148. self._write_settings_to_file()
  149. except Exception as e:
  150. print(f"Auto-save failed: {e}")
  151. self.root.after(5000, self.auto_save_settings) # Repeat every 5 seconds
  152.  
  153. def save_settings(self):
  154. self.settings["custom_text"] = self.custom_text.get("1.0", tk.END).strip()
  155. self.settings["output_filename"] = self.output_name_var.get()
  156. self.settings["custom_separator"] = self.separator_var.get()
  157. self.settings["merge_custom_name"] = self.merge_custom_name_var.get()
  158. self.settings["delete_txt_after_merge"] = self.delete_txt_var.get()
  159. self.settings["similarity_threshold"] = self.similarity_scale.get()
  160. self.settings["metadata_prefix"] = self.metadata_prefix_var.get()
  161. self.settings["delete_txt_after_merge_metadata"] = self.delete_txt_metadata_var.get()
  162. self.settings["include_filename_in_ocr_output"] = self.include_filename_ocr_var.get()
  163. # NEW: Save selected template
  164. if hasattr(self, 'template_var'):
  165. self.settings["selected_template"] = self.template_var.get()
  166. # NEW: Save text filtering settings
  167. if hasattr(self, 'enable_filtering_var'):
  168. self.settings["enable_text_filtering"] = self.enable_filtering_var.get()
  169. self.settings["min_word_length"] = self.min_word_length_var.get()
  170. self.settings["max_gibberish_ratio"] = self.gibberish_ratio_scale.get()
  171. self.settings["remove_single_chars"] = self.remove_single_chars_var.get()
  172. self.settings["min_sentence_words"] = self.min_sentence_words_var.get()
  173. self.settings["filter_excessive_punctuation"] = self.filter_punctuation_var.get()
  174. self.settings["preserve_numbers"] = self.preserve_numbers_var.get()
  175. self.settings["preserve_currency"] = self.preserve_currency_var.get()
  176.  
  177. self._write_settings_to_file()
  178. messagebox.showinfo("Saved", "Settings saved successfully!")
  179.  
  180. # NEW: Text filtering and cleaning methods
  181. def is_gibberish_word(self, word):
  182. """Check if a word is likely gibberish based on character patterns"""
  183. if not word or len(word) < 2:
  184. return True
  185.  
  186. # Preserve numbers and currency
  187. if self.settings.get("preserve_numbers", True) and word.replace('.', '').replace(',', '').isdigit():
  188. return False
  189. if self.settings.get("preserve_currency", True) and any(c in word for c in '$€£¥₹'):
  190. return False
  191.  
  192. # Check for excessive special characters
  193. special_chars = sum(1 for c in word if c in '!@#$%^&*()_+-=[]{}|;:,.<>?')
  194. if special_chars > len(word) * 0.5:
  195. return True
  196.  
  197. # Check for excessive non-alphabetic characters
  198. alpha_chars = sum(1 for c in word if c.isalpha())
  199. if alpha_chars < len(word) * 0.5 and len(word) > 3:
  200. return True
  201.  
  202. # Check for repeating patterns
  203. if len(set(word)) < len(word) * 0.3 and len(word) > 3:
  204. return True
  205.  
  206. # Check for random character sequences
  207. consonant_clusters = re.findall(r'[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ]{4,}', word)
  208. if consonant_clusters:
  209. return True
  210.  
  211. return False
  212.  
  213. def calculate_gibberish_ratio(self, text):
  214. """Calculate the ratio of gibberish words in the text"""
  215. words = text.split()
  216. if not words:
  217. return 0
  218.  
  219. gibberish_count = sum(1 for word in words if self.is_gibberish_word(word.strip(string.punctuation)))
  220. return gibberish_count / len(words)
  221.  
  222. def clean_and_filter_text(self, text):
  223. """Clean and filter OCR text to remove gibberish"""
  224. if not self.settings.get("enable_text_filtering", True):
  225. return text
  226.  
  227. if not text or not text.strip():
  228. return ""
  229.  
  230. # Split into sentences
  231. sentences = re.split(r'[.!?]+', text)
  232. cleaned_sentences = []
  233.  
  234. for sentence in sentences:
  235. sentence = sentence.strip()
  236. if not sentence:
  237. continue
  238.  
  239. # Split into words
  240. words = sentence.split()
  241. cleaned_words = []
  242.  
  243. for word in words:
  244. # Remove excessive punctuation if enabled
  245. if self.settings.get("filter_excessive_punctuation", True):
  246. # Keep reasonable punctuation but remove excessive
  247. word = re.sub(r'[^\w\s\$€£¥₹.,!?-]{2,}', '', word)
  248. word = re.sub(r'[.,!?-]{3,}', '.', word)
  249.  
  250. # Remove single characters if enabled (except meaningful ones)
  251. if self.settings.get("remove_single_chars", True):
  252. if len(word.strip(string.punctuation)) == 1:
  253. if word.lower() not in ['a', 'i', '&', '$', '€', '£', '¥', '₹'] and not word.isdigit():
  254. continue
  255.  
  256. # Check minimum word length
  257. min_length = self.settings.get("min_word_length", 2)
  258. if len(word.strip(string.punctuation)) < min_length:
  259. # Allow short words that are numbers or common short words
  260. clean_word = word.strip(string.punctuation).lower()
  261. if not (clean_word.isdigit() or clean_word in ['a', 'i', 'is', 'it', 'in', 'on', 'at', 'to', 'of', 'or', 'so', 'my', 'we', 'he', 'me']):
  262. continue
  263.  
  264. # Check if word is gibberish
  265. if not self.is_gibberish_word(word.strip(string.punctuation)):
  266. cleaned_words.append(word)
  267.  
  268. # Check minimum words per sentence
  269. min_sentence_words = self.settings.get("min_sentence_words", 3)
  270. if len(cleaned_words) >= min_sentence_words:
  271. cleaned_sentence = ' '.join(cleaned_words)
  272.  
  273. # Final gibberish ratio check for the sentence
  274. max_gibberish_ratio = self.settings.get("max_gibberish_ratio", 0.4)
  275. if self.calculate_gibberish_ratio(cleaned_sentence) <= max_gibberish_ratio:
  276. cleaned_sentences.append(cleaned_sentence)
  277.  
  278. # Join sentences back together
  279. result = '. '.join(cleaned_sentences)
  280. if result and not result.endswith('.'):
  281. result += '.'
  282.  
  283. # Clean up extra spaces
  284. result = re.sub(r'\s+', ' ', result).strip()
  285.  
  286. return result
  287.  
  288. # NEW: Template management methods
  289. def load_template(self):
  290. """Load selected template into custom text field"""
  291. selected_template = self.template_var.get()
  292. templates = self.settings.get("custom_text_templates", DEFAULT_SETTINGS["custom_text_templates"])
  293.  
  294. if selected_template in templates:
  295. self.custom_text.delete("1.0", tk.END)
  296. self.custom_text.insert("1.0", templates[selected_template])
  297. elif selected_template == "-- Custom --":
  298. # Keep current text when "Custom" is selected
  299. pass
  300. else:
  301. # Fallback to first available template
  302. first_template = list(templates.keys())[0]
  303. self.template_var.set(first_template)
  304. self.custom_text.delete("1.0", tk.END)
  305. self.custom_text.insert("1.0", templates[first_template])
  306.  
  307. def save_as_template(self):
  308. """Save current custom text as a new template"""
  309. current_text = self.custom_text.get("1.0", tk.END).strip()
  310. if not current_text:
  311. messagebox.showwarning("Empty Text", "Cannot save empty text as template.")
  312. return
  313.  
  314. # Simple dialog to get template name
  315. template_name = tk.simpledialog.askstring("Save Template",
  316. "Enter template name:",
  317. initialvalue="My Template")
  318. if template_name:
  319. # Ensure templates dict exists
  320. if "custom_text_templates" not in self.settings:
  321. self.settings["custom_text_templates"] = {}
  322.  
  323. self.settings["custom_text_templates"][template_name] = current_text
  324. self.template_var.set(template_name)
  325.  
  326. # Update combobox values
  327. templates = list(self.settings["custom_text_templates"].keys())
  328. templates.append("-- Custom --")
  329. self.template_combo['values'] = templates
  330.  
  331. self._write_settings_to_file()
  332. messagebox.showinfo("Saved", f"Template '{template_name}' saved successfully!")
  333.  
  334. def delete_template(self):
  335. """Delete the currently selected template"""
  336. selected_template = self.template_var.get()
  337. if selected_template == "-- Custom --":
  338. messagebox.showwarning("Cannot Delete", "Cannot delete the Custom option.")
  339. return
  340.  
  341. if selected_template not in self.settings.get("custom_text_templates", {}):
  342. messagebox.showwarning("Not Found", "Selected template not found.")
  343. return
  344.  
  345. if messagebox.askyesno("Confirm Delete", f"Delete template '{selected_template}'?"):
  346. del self.settings["custom_text_templates"][selected_template]
  347.  
  348. # Update combobox values
  349. templates = list(self.settings["custom_text_templates"].keys())
  350. templates.append("-- Custom --")
  351. self.template_combo['values'] = templates
  352.  
  353. # Select first available template or custom
  354. if templates:
  355. self.template_var.set(templates[0] if templates[0] != "-- Custom --" else "-- Custom --")
  356. else:
  357. self.template_var.set("-- Custom --")
  358.  
  359. self._write_settings_to_file()
  360. messagebox.showinfo("Deleted", f"Template '{selected_template}' deleted successfully!")
  361.  
  362. def create_gui(self):
  363. notebook = ttk.Notebook(self.root)
  364. notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
  365.  
  366. self.create_ocr_tab(notebook)
  367. self.create_merge_tab(notebook)
  368. self.create_metadata_tab(notebook)
  369.  
  370. def create_ocr_tab(self, notebook):
  371. ocr_frame = tk.Frame(notebook)
  372. notebook.add(ocr_frame, text="OCR Processor")
  373.  
  374. main_frame = tk.Frame(ocr_frame, padx=10, pady=10)
  375. main_frame.pack(fill=tk.BOTH, expand=True)
  376.  
  377. # NEW: Text Filtering Settings Frame
  378. filtering_frame = tk.LabelFrame(main_frame, text="Text Filtering Settings", padx=5, pady=5)
  379. filtering_frame.pack(fill=tk.X, pady=(0, 5))
  380.  
  381. # Enable filtering checkbox
  382. self.enable_filtering_var = tk.BooleanVar(value=self.settings.get("enable_text_filtering", True))
  383. tk.Checkbutton(filtering_frame, text="Enable OCR text filtering (removes gibberish)",
  384. variable=self.enable_filtering_var).pack(anchor=tk.W)
  385.  
  386. # Filtering options in a grid
  387. filter_options_frame = tk.Frame(filtering_frame)
  388. filter_options_frame.pack(fill=tk.X, pady=5)
  389.  
  390. # Row 1
  391. row1 = tk.Frame(filter_options_frame)
  392. row1.pack(fill=tk.X, pady=2)
  393.  
  394. tk.Label(row1, text="Min word length:").pack(side=tk.LEFT)
  395. self.min_word_length_var = tk.IntVar(value=self.settings.get("min_word_length", 2))
  396. tk.Spinbox(row1, from_=1, to=5, textvariable=self.min_word_length_var, width=5).pack(side=tk.LEFT, padx=5)
  397.  
  398. tk.Label(row1, text="Min words per sentence:").pack(side=tk.LEFT, padx=(20, 0))
  399. self.min_sentence_words_var = tk.IntVar(value=self.settings.get("min_sentence_words", 3))
  400. tk.Spinbox(row1, from_=1, to=10, textvariable=self.min_sentence_words_var, width=5).pack(side=tk.LEFT, padx=5)
  401.  
  402. # Row 2 - Gibberish ratio slider
  403. row2 = tk.Frame(filter_options_frame)
  404. row2.pack(fill=tk.X, pady=2)
  405.  
  406. tk.Label(row2, text="Max gibberish ratio (0.0-1.0):").pack(side=tk.LEFT)
  407. self.gibberish_ratio_scale = tk.Scale(row2, from_=0.0, to=1.0, resolution=0.1, orient=tk.HORIZONTAL,
  408. length=200, tickinterval=0.2)
  409. self.gibberish_ratio_scale.set(self.settings.get("max_gibberish_ratio", 0.4))
  410. self.gibberish_ratio_scale.pack(side=tk.LEFT, padx=5)
  411.  
  412. # Row 3 - Checkboxes
  413. row3 = tk.Frame(filter_options_frame)
  414. row3.pack(fill=tk.X, pady=2)
  415.  
  416. self.remove_single_chars_var = tk.BooleanVar(value=self.settings.get("remove_single_chars", True))
  417. tk.Checkbutton(row3, text="Remove single chars", variable=self.remove_single_chars_var).pack(side=tk.LEFT)
  418.  
  419. self.filter_punctuation_var = tk.BooleanVar(value=self.settings.get("filter_excessive_punctuation", True))
  420. tk.Checkbutton(row3, text="Filter excessive punctuation", variable=self.filter_punctuation_var).pack(side=tk.LEFT, padx=(10, 0))
  421.  
  422. # Row 4 - Preserve options
  423. row4 = tk.Frame(filter_options_frame)
  424. row4.pack(fill=tk.X, pady=2)
  425.  
  426. self.preserve_numbers_var = tk.BooleanVar(value=self.settings.get("preserve_numbers", True))
  427. tk.Checkbutton(row4, text="Preserve numbers", variable=self.preserve_numbers_var).pack(side=tk.LEFT)
  428.  
  429. self.preserve_currency_var = tk.BooleanVar(value=self.settings.get("preserve_currency", True))
  430. tk.Checkbutton(row4, text="Preserve currency symbols", variable=self.preserve_currency_var).pack(side=tk.LEFT, padx=(10, 0))
  431.  
  432. # NEW: Template selection section
  433. template_frame = tk.LabelFrame(main_frame, text="Custom Text Templates", padx=5, pady=5)
  434. template_frame.pack(fill=tk.X, pady=(0, 10))
  435.  
  436. # Template selection row
  437. template_select_frame = tk.Frame(template_frame)
  438. template_select_frame.pack(fill=tk.X, pady=2)
  439.  
  440. tk.Label(template_select_frame, text="Template:").pack(side=tk.LEFT)
  441.  
  442. # Get templates and add Custom option
  443. templates = list(self.settings.get("custom_text_templates", DEFAULT_SETTINGS["custom_text_templates"]).keys())
  444. templates.append("-- Custom --")
  445.  
  446. self.template_var = tk.StringVar(value=self.settings.get("selected_template", "Basic Info"))
  447. self.template_combo = ttk.Combobox(template_select_frame, textvariable=self.template_var,
  448. values=templates, state="readonly", width=20)
  449. self.template_combo.pack(side=tk.LEFT, padx=5)
  450. self.template_combo.bind('<<ComboboxSelected>>', lambda e: self.load_template())
  451.  
  452. # Template action buttons
  453. tk.Button(template_select_frame, text="Load", command=self.load_template).pack(side=tk.LEFT, padx=2)
  454. tk.Button(template_select_frame, text="Save As...", command=self.save_as_template).pack(side=tk.LEFT, padx=2)
  455. tk.Button(template_select_frame, text="Delete", command=self.delete_template).pack(side=tk.LEFT, padx=2)
  456.  
  457. # Custom text area
  458. tk.Label(main_frame, text="Custom Text to Include in Each Entry:").pack(anchor=tk.W)
  459. self.custom_text = scrolledtext.ScrolledText(main_frame, height=6)
  460. self.custom_text.pack(fill=tk.BOTH, expand=True)
  461. self.custom_text.insert(tk.END, self.settings.get("custom_text", ""))
  462.  
  463. # Filename inclusion checkbox
  464. self.include_filename_ocr_var = tk.BooleanVar(value=self.settings.get("include_filename_in_ocr_output", False))
  465. tk.Checkbutton(main_frame, text="Include image filename at the top of OCR text", variable=self.include_filename_ocr_var).pack(anchor=tk.W, pady=5)
  466.  
  467. tk.Label(main_frame, text="Custom Separator:").pack(anchor=tk.W, pady=(10, 0))
  468. separator_frame = tk.Frame(main_frame)
  469. separator_frame.pack(fill=tk.X, pady=2)
  470.  
  471. self.separator_var = tk.StringVar(value=self.settings.get("custom_separator", "////"))
  472. tk.Entry(separator_frame, textvariable=self.separator_var, width=15).pack(side=tk.LEFT)
  473. tk.Button(separator_frame, text="////", command=lambda: self.separator_var.set("////")).pack(side=tk.LEFT, padx=5)
  474. tk.Button(separator_frame, text="---", command=lambda: self.separator_var.set("---")).pack(side=tk.LEFT, padx=2)
  475. tk.Button(separator_frame, text="===", command=lambda: self.separator_var.set("===")).pack(side=tk.LEFT, padx=2)
  476.  
  477. tk.Label(main_frame, text="Output Filename:").pack(anchor=tk.W, pady=(10, 0))
  478. self.output_name_var = tk.StringVar(value=self.settings.get("output_filename", "combined_text.txt"))
  479. tk.Entry(main_frame, textvariable=self.output_name_var).pack(fill=tk.X)
  480.  
  481. tk.Label(main_frame, text="Folder to Process:").pack(anchor=tk.W, pady=(10, 0))
  482. folder_frame = tk.Frame(main_frame)
  483. folder_frame.pack(fill=tk.X, pady=5)
  484. tk.Entry(folder_frame, textvariable=self.folder_path).pack(side=tk.LEFT, fill=tk.X, expand=True)
  485. tk.Button(folder_frame, text="Browse", command=self.select_and_process_folder).pack(side=tk.RIGHT, padx=5)
  486.  
  487. button_frame = tk.Frame(main_frame)
  488. button_frame.pack(fill=tk.X, pady=10)
  489. tk.Button(button_frame, text="Save Settings", command=self.save_settings).pack(side=tk.LEFT, padx=5)
  490.  
  491. self.process_btn = tk.Button(button_frame, text="Process Folder", command=self.manual_process_folder)
  492. self.process_btn.pack(side=tk.LEFT, padx=5)
  493.  
  494. self.stop_btn = tk.Button(button_frame, text="Stop Processing", command=self.stop_ocr_process, state=tk.DISABLED)
  495. self.stop_btn.pack(side=tk.LEFT, padx=5)
  496.  
  497. progress_frame = tk.Frame(main_frame)
  498. progress_frame.pack(fill=tk.X, pady=5)
  499.  
  500. tk.Label(progress_frame, text="Progress:").pack(anchor=tk.W)
  501. self.ocr_progress_var = tk.StringVar(value="Ready")
  502. tk.Label(progress_frame, textvariable=self.ocr_progress_var, fg="blue").pack(anchor=tk.W)
  503.  
  504. self.ocr_progress_bar = ttk.Progressbar(progress_frame, mode='determinate')
  505. self.ocr_progress_bar.pack(fill=tk.X, pady=2)
  506.  
  507. self.ocr_current_file_var = tk.StringVar(value="")
  508. tk.Label(progress_frame, textvariable=self.ocr_current_file_var, fg="green", wraplength=580).pack(anchor=tk.W)
  509.  
  510. ctx_frame = tk.Frame(main_frame)
  511. ctx_frame.pack(fill=tk.X, pady=5)
  512. tk.Label(ctx_frame, text="Context Menu:").pack(side=tk.LEFT, padx=5)
  513. tk.Button(ctx_frame, text="Install", command=self.install_context_menu).pack(side=tk.LEFT, padx=5)
  514. tk.Button(ctx_frame, text="Uninstall", command=self.uninstall_context_menu).pack(side=tk.LEFT, padx=5)
  515.  
  516. def create_merge_tab(self, notebook):
  517. merge_frame = tk.Frame(notebook)
  518. notebook.add(merge_frame, text="Text File Merger")
  519.  
  520. main_frame = tk.Frame(merge_frame, padx=10, pady=10)
  521. main_frame.pack(fill=tk.BOTH, expand=True)
  522.  
  523. instructions = "Merge numbered text files (e.g., 1.txt, 02.txt, name03.txt).\nSelect folder containing numbered .txt files to combine them."
  524. tk.Label(main_frame, text=instructions, justify=tk.LEFT, wraplength=600).pack(anchor=tk.W, pady=(0, 10))
  525.  
  526. tk.Label(main_frame, text="Custom Name/Prefix for Merged Content (optional):").pack(anchor=tk.W)
  527. self.merge_custom_name_var = tk.StringVar(value=self.settings.get("merge_custom_name", ""))
  528. tk.Entry(main_frame, textvariable=self.merge_custom_name_var).pack(fill=tk.X, pady=2)
  529.  
  530. self.delete_txt_var = tk.BooleanVar(value=self.settings.get("delete_txt_after_merge", False))
  531. tk.Checkbutton(main_frame, text="Delete original .txt files after merging", variable=self.delete_txt_var).pack(anchor=tk.W, pady=5)
  532.  
  533. tk.Label(main_frame, text="Output Filename for Merged File:").pack(anchor=tk.W, pady=(10, 0))
  534. self.merge_output_var = tk.StringVar(value="merged_text_files.txt")
  535. tk.Entry(main_frame, textvariable=self.merge_output_var).pack(fill=tk.X, pady=2)
  536.  
  537. tk.Label(main_frame, text="Folder Containing Text Files:").pack(anchor=tk.W, pady=(10, 0))
  538. merge_folder_frame = tk.Frame(main_frame)
  539. merge_folder_frame.pack(fill=tk.X, pady=5)
  540. self.merge_folder_path = tk.StringVar()
  541. tk.Entry(merge_folder_frame, textvariable=self.merge_folder_path).pack(side=tk.LEFT, fill=tk.X, expand=True)
  542. tk.Button(merge_folder_frame, text="Browse", command=self.select_merge_folder).pack(side=tk.RIGHT, padx=5)
  543.  
  544. tk.Label(main_frame, text="Preview of files to merge:").pack(anchor=tk.W, pady=(10, 0))
  545. self.merge_preview = Listbox(main_frame, height=8)
  546. self.merge_preview.pack(fill=tk.BOTH, expand=True, pady=2)
  547.  
  548. merge_button_frame = tk.Frame(main_frame)
  549. merge_button_frame.pack(fill=tk.X, pady=10)
  550. tk.Button(merge_button_frame, text="Scan Files", command=self.scan_merge_files).pack(side=tk.LEFT, padx=5)
  551. tk.Button(merge_button_frame, text="Merge Files", command=self.merge_text_files).pack(side=tk.LEFT, padx=5)
  552.  
  553. def create_metadata_tab(self, notebook):
  554. metadata_frame = tk.Frame(notebook)
  555. notebook.add(metadata_frame, text="JPG + TXT Merger")
  556.  
  557. main_frame = tk.Frame(metadata_frame, padx=10, pady=10)
  558. main_frame.pack(fill=tk.BOTH, expand=True)
  559.  
  560. instructions = "Embed text into JPEG EXIF metadata. Originals are NOT modified.\nModified files are saved to a '_processed_jpegs' subfolder.\n• Exact match: this.jpg + this.txt\n• Similar names: photo01.jpg + photo01_notes.txt\n\nRequires ExifTool (exiftool.exe) in your system PATH for best compatibility with Windows Explorer."
  561. tk.Label(main_frame, text=instructions, justify=tk.LEFT, wraplength=600).pack(anchor=tk.W, pady=(0, 10))
  562.  
  563. tk.Label(main_frame, text="Name Similarity Threshold (0.5-1.0):").pack(anchor=tk.W)
  564. self.similarity_scale = tk.Scale(main_frame, from_=0.5, to=1.0, resolution=0.05, orient=tk.HORIZONTAL, length=300)
  565. self.similarity_scale.set(self.settings.get("similarity_threshold", 0.8))
  566. self.similarity_scale.pack(anchor=tk.W, pady=2)
  567.  
  568. tk.Label(main_frame, text="Custom Prefix for Embedded Text (optional):").pack(anchor=tk.W)
  569. self.metadata_prefix_var = tk.StringVar(value=self.settings.get("metadata_prefix", ""))
  570. tk.Entry(main_frame, textvariable=self.metadata_prefix_var).pack(fill=tk.X, pady=2)
  571.  
  572. self.delete_txt_metadata_var = tk.BooleanVar(value=self.settings.get("delete_txt_after_merge_metadata", False))
  573. tk.Checkbutton(main_frame, text="Delete .txt files after embedding into JPEGs", variable=self.delete_txt_metadata_var).pack(anchor=tk.W, pady=5)
  574.  
  575. tk.Label(main_frame, text="Folder Containing JPG and TXT Files:").pack(anchor=tk.W, pady=(10, 0))
  576. metadata_folder_frame = tk.Frame(main_frame)
  577. metadata_folder_frame.pack(fill=tk.X, pady=5)
  578. self.metadata_folder_path = tk.StringVar()
  579. tk.Entry(metadata_folder_frame, textvariable=self.metadata_folder_path).pack(side=tk.LEFT, fill=tk.X, expand=True)
  580. tk.Button(metadata_folder_frame, text="Browse", command=self.select_metadata_folder).pack(side=tk.RIGHT, padx=5)
  581.  
  582. tk.Label(main_frame, text="Preview JPG-TXT matches:").pack(anchor=tk.W, pady=(10, 0))
  583. self.metadata_preview = scrolledtext.ScrolledText(main_frame, height=10)
  584. self.metadata_preview.pack(fill=tk.BOTH, expand=True, pady=2)
  585.  
  586. metadata_button_frame = tk.Frame(main_frame)
  587. metadata_button_frame.pack(fill=tk.X, pady=10)
  588. tk.Button(metadata_button_frame, text="Scan Matches", command=self.scan_metadata_matches).pack(side=tk.LEFT, padx=5)
  589.  
  590. self.embed_btn = tk.Button(metadata_button_frame, text="Embed Text to JPEGs", command=self.start_embedding_process)
  591. self.embed_btn.pack(side=tk.LEFT, padx=5)
  592.  
  593. self.stop_metadata_btn = tk.Button(metadata_button_frame, text="Stop Embedding", command=self.stop_metadata_process, state=tk.DISABLED)
  594. self.stop_metadata_btn.pack(side=tk.LEFT, padx=5)
  595.  
  596. metadata_progress_frame = tk.Frame(main_frame)
  597. metadata_progress_frame.pack(fill=tk.X, pady=5)
  598.  
  599. tk.Label(metadata_progress_frame, text="Embed Progress:").pack(anchor=tk.W)
  600. self.metadata_progress_var = tk.StringVar(value="Ready")
  601. tk.Label(metadata_progress_frame, textvariable=self.metadata_progress_var, fg="blue").pack(anchor=tk.W)
  602.  
  603. self.metadata_progress_bar = ttk.Progressbar(metadata_progress_frame, mode='determinate')
  604. self.metadata_progress_bar.pack(fill=tk.X, pady=2)
  605.  
  606. self.metadata_current_file_var = tk.StringVar(value="")
  607. tk.Label(metadata_progress_frame, textvariable=self.metadata_current_file_var, fg="green", wraplength=580).pack(anchor=tk.W)
  608.  
  609. def select_and_process_folder(self):
  610. folder = filedialog.askdirectory(title="Select Folder with Images")
  611. if folder: self.folder_path.set(folder)
  612.  
  613. def select_merge_folder(self):
  614. folder = filedialog.askdirectory(title="Select Folder with Text Files to Merge")
  615. if folder:
  616. self.merge_folder_path.set(folder)
  617. self.scan_merge_files()
  618.  
  619. def select_metadata_folder(self):
  620. folder = filedialog.askdirectory(title="Select Folder with JPG and TXT Files")
  621. if folder:
  622. self.metadata_folder_path.set(folder)
  623. self.scan_metadata_matches()
  624.  
  625. def manual_process_folder(self):
  626. folder = self.folder_path.get()
  627. if not os.path.isdir(folder):
  628. messagebox.showwarning("Invalid", "Please select a valid folder.")
  629. return
  630. if self.is_processing_ocr:
  631. messagebox.showwarning("Processing", "Already processing. Please wait or stop current process.")
  632. return
  633. self.ocr_processing_thread = threading.Thread(target=self.process_folder, args=(folder,))
  634. self.ocr_processing_thread.daemon = True
  635. self.ocr_processing_thread.start()
  636.  
  637. def stop_ocr_process(self):
  638. self.stop_processing_ocr = True
  639. self.ocr_progress_var.set("Stopping...")
  640. self.update_ocr_ui_state(False)
  641.  
  642. def stop_metadata_process(self):
  643. self.stop_processing_metadata = True
  644. self.metadata_progress_var.set("Stopping...")
  645. self.update_metadata_ui_state(False)
  646.  
  647. def update_ocr_ui_state(self, processing):
  648. self.is_processing_ocr = processing
  649. state = tk.DISABLED if processing else tk.NORMAL
  650. stop_state = tk.NORMAL if processing else tk.DISABLED
  651. self.process_btn.config(state=state)
  652. self.stop_btn.config(state=stop_state)
  653. if not processing and self.stop_processing_ocr:
  654. self.ocr_progress_var.set("Process stopped by user")
  655. self.ocr_current_file_var.set("")
  656. self.ocr_progress_bar['value'] = 0
  657.  
  658. def update_metadata_ui_state(self, processing):
  659. self.is_processing_metadata = processing
  660. state = tk.DISABLED if processing else tk.NORMAL
  661. stop_state = tk.NORMAL if processing else tk.DISABLED
  662. self.embed_btn.config(state=state)
  663. self.stop_metadata_btn.config(state=stop_state)
  664. if not processing:
  665. if self.stop_processing_metadata:
  666. self.metadata_progress_var.set("Process stopped by user")
  667. self.metadata_current_file_var.set("")
  668. self.metadata_progress_bar['value'] = 0
  669. else:
  670. self.metadata_progress_var.set("Ready")
  671. self.metadata_current_file_var.set("")
  672. self.metadata_progress_bar['value'] = 0
  673.  
  674. def extract_number_from_filename(self, filename):
  675. name = os.path.splitext(filename)[0]
  676. numbers = re.findall(r'\d+', name)
  677. return int(numbers[-1]) if numbers else float('inf')
  678.  
  679. def scan_merge_files(self):
  680. folder = self.merge_folder_path.get()
  681. if not os.path.isdir(folder): return
  682.  
  683. txt_files = [f for f in os.listdir(folder) if f.lower().endswith('.txt') and re.search(r'\d+', f)]
  684. txt_files.sort(key=self.extract_number_from_filename)
  685.  
  686. self.merge_preview.delete(0, tk.END)
  687. if txt_files:
  688. for txt_file in txt_files: self.merge_preview.insert(tk.END, txt_file)
  689. else:
  690. self.merge_preview.insert(tk.END, "No numbered text files found")
  691.  
  692. def merge_text_files(self):
  693. folder = self.merge_folder_path.get()
  694. if not os.path.isdir(folder):
  695. messagebox.showwarning("Invalid", "Please select a valid folder.")
  696. return
  697.  
  698. files_to_merge = [self.merge_preview.get(i) for i in range(self.merge_preview.size()) if self.merge_preview.get(i) != "No numbered text files found"]
  699. if not files_to_merge:
  700. messagebox.showwarning("No Files", "No files to merge.")
  701. return
  702.  
  703. try:
  704. output_path = os.path.join(folder, self.merge_output_var.get())
  705. custom_name = self.merge_custom_name_var.get().strip()
  706. separator = self.separator_var.get()
  707.  
  708. merged_content = []
  709. for idx, txt_file in enumerate(files_to_merge, start=1):
  710. file_path = os.path.join(folder, txt_file)
  711. try:
  712. with open(file_path, 'r', encoding='utf-8') as f:
  713. content = f.read().strip()
  714. entry_parts = [separator, f"{idx:03d}", txt_file]
  715. if custom_name: entry_parts.append(custom_name)
  716. if content: entry_parts.append(content)
  717. merged_content.append('\n'.join(entry_parts))
  718. except Exception as e:
  719. merged_content.append(f"{separator}\n{idx:03d}\n{txt_file}\nRead Error: {e}")
  720.  
  721. with open(output_path, 'w', encoding='utf-8') as f: f.write('\n\n'.join(merged_content))
  722.  
  723. deleted_files_count = 0
  724. if self.delete_txt_var.get():
  725. for txt_file in files_to_merge:
  726. try:
  727. os.remove(os.path.join(folder, txt_file))
  728. deleted_files_count += 1
  729. except Exception as e:
  730. print(f"Could not delete {txt_file}: {e}")
  731.  
  732. success_msg = f"Merged {len(files_to_merge)} files into {self.merge_output_var.get()}"
  733. if deleted_files_count > 0: success_msg += f"\nDeleted {deleted_files_count} original files"
  734.  
  735. messagebox.showinfo("Success", success_msg)
  736. self.scan_merge_files()
  737.  
  738. except Exception as e:
  739. messagebox.showerror("Error", f"Failed to merge files: {e}")
  740.  
  741. def calculate_similarity(self, name1, name2):
  742. name1 = os.path.splitext(name1)[0].lower()
  743. name2 = os.path.splitext(name2)[0].lower()
  744. return SequenceMatcher(None, name1, name2).ratio()
  745.  
  746. def scan_metadata_matches(self):
  747. folder = self.metadata_folder_path.get()
  748. if not os.path.isdir(folder): return []
  749.  
  750. jpg_files = [f for f in os.listdir(folder) if f.lower().endswith(('.jpg', '.jpeg'))]
  751. txt_files = [f for f in os.listdir(folder) if f.lower().endswith('.txt')]
  752.  
  753. matches = []
  754. threshold = self.similarity_scale.get()
  755.  
  756. used_txt_files = set()
  757.  
  758. for jpg_file in jpg_files:
  759. jpg_base = os.path.splitext(jpg_file)[0].lower()
  760.  
  761. # Prioritize exact match
  762. exact_match_found = False
  763. for txt_file in txt_files:
  764. if txt_file in used_txt_files: continue
  765. txt_base = os.path.splitext(txt_file)[0].lower()
  766. if jpg_base == txt_base:
  767. matches.append((jpg_file, txt_file, 1.0, "Exact"))
  768. used_txt_files.add(txt_file)
  769. exact_match_found = True
  770. break
  771.  
  772. if exact_match_found: continue
  773.  
  774. # Find best similar match if no exact one was found
  775. best_match = None
  776. best_similarity = 0
  777. for txt_file in txt_files:
  778. if txt_file in used_txt_files: continue
  779. similarity = self.calculate_similarity(jpg_file, txt_file)
  780. if similarity >= threshold and similarity > best_similarity:
  781. best_similarity = similarity
  782. best_match = txt_file
  783.  
  784. if best_match:
  785. matches.append((jpg_file, best_match, best_similarity, "Similar"))
  786. used_txt_files.add(best_match)
  787.  
  788. self.metadata_preview.delete('1.0', tk.END)
  789. if matches:
  790. preview_text = f"Found {len(matches)} JPG-TXT matches to process:\n\n"
  791. for jpg, txt, sim, match_type in matches:
  792. preview_text += f"• {jpg} → {txt} ({match_type}: {sim:.2f})\n"
  793. else:
  794. preview_text = "No matching JPG-TXT pairs found.\nTry lowering the similarity threshold or check filenames."
  795.  
  796. self.metadata_preview.insert('1.0', preview_text)
  797. return matches
  798.  
  799. def start_embedding_process(self):
  800. folder = self.metadata_folder_path.get()
  801. if not os.path.isdir(folder):
  802. messagebox.showwarning("Invalid", "Please select a valid folder.")
  803. return
  804.  
  805. if self.is_processing_metadata:
  806. messagebox.showwarning("Processing", "Already embedding. Please wait.")
  807. return
  808.  
  809. self.metadata_processing_thread = threading.Thread(target=self.embed_text_to_jpegs, args=(folder,))
  810. self.metadata_processing_thread.daemon = True
  811. self.metadata_processing_thread.start()
  812.  
  813. def embed_text_to_jpegs(self, folder):
  814. """Embed text content into JPEG EXIF metadata using piexif and exiftool."""
  815. try:
  816. self.stop_processing_metadata = False
  817. self.root.after(0, lambda: self.update_metadata_ui_state(True))
  818.  
  819. matches = self.scan_metadata_matches() # Rescan to get fresh list
  820. if not matches:
  821. self.root.after(0, lambda: messagebox.showwarning("No Matches", "No JPG-TXT pairs found to process."))
  822. self.root.after(0, lambda: self.update_metadata_ui_state(False))
  823. return
  824.  
  825. # Create output directory
  826. output_folder = os.path.join(folder, "_processed_jpegs")
  827. os.makedirs(output_folder, exist_ok=True)
  828.  
  829. success_count = 0
  830. deleted_files_count = 0
  831. error_log = []
  832. prefix = self.metadata_prefix_var.get().strip()
  833.  
  834. total_files_to_process = len(matches)
  835. self.root.after(0, lambda: self.metadata_progress_bar.config(maximum=total_files_to_process))
  836. self.root.after(0, lambda: self.metadata_progress_var.set(f"Embedding text into {total_files_to_process} JPEGs..."))
  837.  
  838. for idx, (jpg_file, txt_file, _, _) in enumerate(matches, start=1):
  839. if self.stop_processing_metadata:
  840. self.root.after(0, lambda: self.metadata_progress_var.set("Processing stopped"))
  841. break
  842.  
  843. self.root.after(0, lambda f=jpg_file, i=idx, t=total_files_to_process: self.metadata_current_file_var.set(f"({i}/{t}) Processing: {f}"))
  844. self.root.after(0, lambda p=idx: self.metadata_progress_bar.config(value=p))
  845.  
  846. jpg_path = os.path.join(folder, jpg_file)
  847. txt_path = os.path.join(folder, txt_file)
  848. output_path = os.path.join(output_folder, jpg_file) # Path for the new, processed JPG
  849. temp_jpg_path_for_piexif = os.path.join(output_folder, f"temp_{jpg_file}")
  850.  
  851. try:
  852. # Read text content
  853. with open(txt_path, 'r', encoding='utf-8') as f:
  854. text_content = f.read().strip()
  855. if not text_content:
  856. error_log.append(f"Skipped {jpg_file}: Associated text file {txt_file} is empty.")
  857. continue
  858.  
  859. # Combine prefix and text
  860. final_content = f"{prefix}\n{text_content}" if prefix else text_content
  861.  
  862. # --- PART 1: Use piexif to write to ImageDescription and save to a temporary file ---
  863. # This ensures we get all original EXIF data and can modify it with piexif.
  864. # We save to a temp path first, then exiftool will process this temp file.
  865.  
  866. # Load exif data safely
  867. exif_dict = piexif.load(jpg_path) # Load from original JPG
  868.  
  869. # Embed text into ImageDescription field
  870. exif_dict["0th"][piexif.ImageIFD.ImageDescription] = final_content.encode("utf-8")
  871.  
  872. # Get the raw exif bytes
  873. exif_bytes = piexif.dump(exif_dict)
  874.  
  875. # Save image to a temporary file in the output folder with updated EXIF
  876. img = Image.open(jpg_path)
  877. img.save(temp_jpg_path_for_piexif, exif=exif_bytes)
  878.  
  879. # --- PART 2: Use exiftool to write to Windows/Explorer-compatible Comment tag ---
  880. # And save the final output to the intended output_path.
  881. try:
  882. subprocess.run([
  883. "exiftool",
  884. f"-Comment={final_content}",
  885. "-overwrite_original",
  886. temp_jpg_path_for_piexif
  887. ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  888.  
  889. # After exiftool overwrites the temp file, rename it to the final output_path
  890. if os.path.exists(output_path):
  891. os.remove(output_path)
  892. os.rename(temp_jpg_path_for_piexif, output_path)
  893.  
  894. except subprocess.CalledProcessError as sub_e:
  895. error_msg = f"Exiftool error for {jpg_file}: {sub_e.stderr.decode('utf-8', errors='ignore').strip()}"
  896. error_log.append(error_msg)
  897. print(error_msg)
  898. # Clean up temp file if exiftool failed
  899. if os.path.exists(temp_jpg_path_for_piexif):
  900. os.remove(temp_jpg_path_for_piexif)
  901. continue
  902. except FileNotFoundError:
  903. error_msg = f"ExifTool not found. Please ensure 'exiftool.exe' is in your system PATH to embed Windows Comment for {jpg_file}. Only Exif ImageDescription was written."
  904. error_log.append(error_msg)
  905. print(error_msg)
  906. # If exiftool isn't found, we've still written the ImageDescription with piexif
  907. # so we should move the temp file to the final output path.
  908. if os.path.exists(temp_jpg_path_for_piexif):
  909. if os.path.exists(output_path): os.remove(output_path)
  910. os.rename(temp_jpg_path_for_piexif, output_path)
  911.  
  912. # Count it as partially successful (piexif written)
  913. success_count += 1
  914. continue # Move to next file, having embedded what we could
  915.  
  916. success_count += 1
  917.  
  918. # Delete txt file if requested
  919. if self.delete_txt_metadata_var.get():
  920. try:
  921. os.remove(txt_path)
  922. deleted_files_count += 1
  923. except Exception as e:
  924. error_log.append(f"Could not delete {txt_file}: {e}")
  925.  
  926. except Exception as e:
  927. error_msg = f"Error processing {jpg_file} + {txt_file}: {e}"
  928. error_log.append(error_msg)
  929. print(error_msg)
  930. # Clean up temp file if an error occurred before exiftool or piexif save
  931. if os.path.exists(temp_jpg_path_for_piexif):
  932. os.remove(temp_jpg_path_for_piexif)
  933. continue
  934.  
  935. if not self.stop_processing_metadata and success_count > 0:
  936. summary_message = f"Successfully embedded text into {success_count} JPEG file(s).\nSaved in '{output_folder}'."
  937. if deleted_files_count > 0:
  938. summary_message += f"\nDeleted {deleted_files_count} text file(s)."
  939. if error_log:
  940. summary_message += f"\n\nEncountered {len(error_log)} error(s):\n" + "\n".join(error_log)
  941. self.root.after(0, lambda: messagebox.showwarning("Completed with Errors", summary_message))
  942. else:
  943. self.root.after(0, lambda: messagebox.showinfo("Success", summary_message))
  944.  
  945. self.root.after(0, lambda: self.metadata_progress_bar.config(value=total_files_to_process))
  946. self.root.after(0, lambda: self.metadata_progress_var.set("Complete!"))
  947. self.root.after(0, lambda: self.metadata_current_file_var.set(""))
  948.  
  949. elif self.stop_processing_metadata:
  950. self.root.after(0, lambda: self.metadata_current_file_var.set(f"Processed {success_count} files before stopping."))
  951.  
  952. self.root.after(0, lambda: self.scan_metadata_matches()) # Refresh preview
  953.  
  954. except Exception as e:
  955. self.root.after(0, lambda: messagebox.showerror("Error", f"Embedding process failed: {e}"))
  956. self.root.after(0, lambda: self.metadata_progress_var.set("Error!"))
  957.  
  958. finally:
  959. self.root.after(0, lambda: self.update_metadata_ui_state(False))
  960. self.stop_processing_metadata = False
  961.  
  962. def process_folder(self, folder_path):
  963. try:
  964. self.stop_processing_ocr = False
  965. self.root.after(0, lambda: self.update_ocr_ui_state(True))
  966.  
  967. jpg_files = sorted([f for f in os.listdir(folder_path) if f.lower().endswith(('.jpg', '.jpeg'))])
  968. if not jpg_files:
  969. self.root.after(0, lambda: messagebox.showinfo("No Images", "No JPG/JPEG images found."))
  970. return
  971.  
  972. total_files = len(jpg_files)
  973. self.root.after(0, lambda: self.ocr_progress_bar.config(maximum=total_files))
  974. self.root.after(0, lambda: self.ocr_progress_var.set(f"Processing {total_files} images..."))
  975.  
  976. combined_entries = []
  977. custom_text = self.settings.get("custom_text", "").strip()
  978. separator = self.settings.get("custom_separator", "////")
  979. include_filename = self.settings.get("include_filename_in_ocr_output", False)
  980.  
  981. for idx, jpg_file in enumerate(jpg_files, start=1):
  982. if self.stop_processing_ocr:
  983. self.root.after(0, lambda: self.ocr_progress_var.set("Processing stopped"))
  984. break
  985.  
  986. self.root.after(0, lambda f=jpg_file, i=idx, t=total_files: self.ocr_current_file_var.set(f"({i}/{t}) Processing: {f}"))
  987. self.root.after(0, lambda p=idx: self.ocr_progress_bar.config(value=p))
  988.  
  989. try:
  990. image_path = os.path.join(folder_path, jpg_file)
  991. ocr_text = self.process_image(image_path)
  992.  
  993. entry_parts = [separator, f"{idx:03d}"]
  994.  
  995. # Add filename if enabled
  996. if include_filename:
  997. entry_parts.append(jpg_file)
  998. else:
  999. entry_parts.append(os.path.splitext(jpg_file)[0])
  1000.  
  1001. if custom_text: entry_parts.append(custom_text)
  1002. if ocr_text: entry_parts.append(ocr_text)
  1003.  
  1004. combined_entries.append('\n'.join(entry_parts))
  1005.  
  1006. except Exception as e:
  1007. entry_parts = [separator, f"{idx:03d}"]
  1008. if include_filename:
  1009. entry_parts.append(jpg_file)
  1010. else:
  1011. entry_parts.append(os.path.splitext(jpg_file)[0])
  1012. if custom_text: entry_parts.append(custom_text)
  1013. entry_parts.append(f"OCR Error: {e}")
  1014. combined_entries.append('\n'.join(entry_parts))
  1015.  
  1016. if not self.stop_processing_ocr and combined_entries:
  1017. self.root.after(0, lambda: self.ocr_progress_var.set("Writing output file..."))
  1018. self.root.after(0, lambda: self.ocr_current_file_var.set("Saving results..."))
  1019.  
  1020. output_path = os.path.join(folder_path, self.output_name_var.get())
  1021. try:
  1022. with open(output_path, 'w', encoding='utf-8') as f:
  1023. f.write('\n\n'.join(combined_entries))
  1024.  
  1025. self.root.after(0, lambda: self.ocr_progress_bar.config(value=total_files))
  1026. self.root.after(0, lambda: self.ocr_progress_var.set("Complete!"))
  1027. self.root.after(0, lambda: self.ocr_current_file_var.set(f"Saved: {self.output_name_var.get()}"))
  1028. self.root.after(0, lambda: messagebox.showinfo("Complete", f"Processed {len(combined_entries)} images."))
  1029. except Exception as e:
  1030. self.root.after(0, lambda: messagebox.showerror("Error", f"Failed to write output: {e}"))
  1031.  
  1032. elif self.stop_processing_ocr:
  1033. self.root.after(0, lambda: self.ocr_current_file_var.set(f"Processed {len(combined_entries)} files before stopping."))
  1034.  
  1035. except Exception as e:
  1036. self.root.after(0, lambda: messagebox.showerror("Error", f"Processing failed: {e}"))
  1037. finally:
  1038. self.root.after(0, lambda: self.update_ocr_ui_state(False))
  1039. self.stop_processing_ocr = False
  1040.  
  1041. def process_image(self, image_path):
  1042. try:
  1043. from numpy import array as np_array
  1044. import cv2
  1045.  
  1046. img = Image.open(image_path)
  1047.  
  1048. if img.mode == 'RGB':
  1049. img_array = np_array(img)
  1050. hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV)
  1051. h, s, v = cv2.split(hsv)
  1052. s = cv2.add(s, 40)
  1053. hsv = cv2.merge([h, s, v])
  1054. img_array = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
  1055. img = Image.fromarray(img_array)
  1056.  
  1057. if img.mode != 'L': img = img.convert('L')
  1058.  
  1059. text = pytesseract.image_to_string(img)
  1060.  
  1061. # NEW: Apply text filtering if enabled
  1062. cleaned_text = self.clean_and_filter_text(' '.join(text.split()))
  1063.  
  1064. return cleaned_text
  1065.  
  1066. except Exception as e:
  1067. print(f"Error processing image {image_path}: {e}")
  1068. return f"OCR Error: {e}"
  1069.  
  1070. def install_context_menu(self):
  1071. try:
  1072. script_path = os.path.abspath(sys.argv[0])
  1073. python_exe = sys.executable
  1074. keys = [
  1075. (r"Software\Classes\Directory\shell\MergedOCR", "%1"),
  1076. (r"Software\Classes\Directory\Background\shell\MergedOCR", "%V")
  1077. ]
  1078. for base_key, param in keys:
  1079. key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, base_key)
  1080. winreg.SetValueEx(key, "", 0, winreg.REG_SZ, "Enhanced OCR & File Merger")
  1081. command_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, f"{base_key}\\command")
  1082. winreg.SetValueEx(command_key, "", 0, winreg.REG_SZ, f'"{python_exe}" "{script_path}" "{param}"')
  1083. messagebox.showinfo("Success", "Context menu installed successfully.")
  1084. except Exception as e:
  1085. messagebox.showerror("Error", f"Failed to install context menu: {e}")
  1086.  
  1087. def uninstall_context_menu(self):
  1088. try:
  1089. keys_to_delete = [
  1090. r"Software\Classes\Directory\shell\MergedOCR\command",
  1091. r"Software\Classes\Directory\shell\MergedOCR",
  1092. r"Software\Classes\Directory\Background\shell\MergedOCR\command",
  1093. r"Software\Classes\Directory\Background\shell\MergedOCR"
  1094. ]
  1095. for key in keys_to_delete:
  1096. try: winreg.DeleteKey(winreg.HKEY_CURRENT_USER, key)
  1097. except FileNotFoundError: pass
  1098. messagebox.showinfo("Success", "Context menu uninstalled successfully.")
  1099. except Exception as e:
  1100. messagebox.showerror("Error", f"Failed to uninstall context menu: {e}")
  1101.  
  1102. if __name__ == "__main__":
  1103. # --- Dependency Check ---
  1104. try:
  1105. import numpy
  1106. import cv2
  1107. import piexif
  1108. import subprocess
  1109. import tkinter.simpledialog # NEW: Import for template naming dialog
  1110. except ImportError as e:
  1111. tk.Tk().withdraw()
  1112. if 'piexif' in str(e):
  1113. messagebox.showerror("Missing Dependency",
  1114. "The 'piexif' library is required for safe metadata editing.\n\nPlease install it by running:\npip install piexif")
  1115. elif 'numpy' in str(e) or 'cv2' in str(e):
  1116. messagebox.showerror("Missing Dependencies",
  1117. "Required libraries for image processing (numpy, opencv-python) are missing.\n\nPlease install them by running:\npip install numpy opencv-python")
  1118. else:
  1119. messagebox.showerror("Missing Dependencies",
  1120. "Please install required libraries:\npip install opencv-python numpy pytesseract pillow piexif")
  1121. sys.exit(1)
  1122.  
  1123. # Check if exiftool is available
  1124. try:
  1125. subprocess.run(["exiftool", "-ver"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  1126. except FileNotFoundError:
  1127. tk.Tk().withdraw()
  1128. messagebox.showwarning("ExifTool Not Found",
  1129. "ExifTool (exiftool.exe) was not found in your system PATH.\n\n"
  1130. "While the script will still embed text using piexif, the comment may not display in Windows Explorer properties.\n\n"
  1131. "For full compatibility, please download ExifTool from https://exiftool.org/ and add its directory to your system PATH.")
  1132. except Exception as e:
  1133. tk.Tk().withdraw()
  1134. messagebox.showwarning("ExifTool Error", f"An error occurred while checking ExifTool: {e}\n"
  1135. "Please ensure ExifTool is correctly installed and accessible.")
  1136.  
  1137. root = tk.Tk()
  1138. app = MergedOCRApp(root)
  1139. root.mainloop()
  1140. --------------------------------------------------------------------------------
  1141. --- END: enhanced_ocr_filtered.py ---
  1142.  
  1143.  
  1144. --- START: file_randomizer_advancedv2.py ---
  1145. --------------------------------------------------------------------------------
  1146. import os
  1147. import sys
  1148. import random
  1149. import string
  1150. import tkinter as tk
  1151. from tkinter import ttk, filedialog, messagebox
  1152. from datetime import datetime, timedelta
  1153. import winreg
  1154. import subprocess
  1155. from pathlib import Path
  1156. import json
  1157. import tempfile
  1158. import re
  1159. from collections import defaultdict
  1160.  
  1161. class AdvancedFileRandomizer:
  1162. def __init__(self):
  1163. self.root = tk.Tk()
  1164. self.root.title("Advanced File & Folder Randomizer with Numbering")
  1165. self.root.geometry("1000x720") # Increased width for new preview column and controls
  1166. self.root.resizable(True, True)
  1167. self.root.minsize(800, 600)
  1168.  
  1169. # --- Internal State ---
  1170. # This dictionary will hold the data for items in the preview tree
  1171. # Key: item_id (from treeview), Value: dictionary with item details
  1172. self.preview_items = {}
  1173. self.settings_dir = Path("C:/Scripts/Saved_json")
  1174.  
  1175. # --- Variables for UI ---
  1176. self.folder_path = tk.StringVar()
  1177. self.randomize_names = tk.BooleanVar(value=True)
  1178. self.randomize_dates = tk.BooleanVar(value=True)
  1179. self.randomize_modified = tk.BooleanVar(value=True)
  1180. self.preserve_extensions = tk.BooleanVar(value=True)
  1181. self.randomize_folders = tk.BooleanVar(value=False)
  1182. self.randomize_folder_dates = tk.BooleanVar(value=False)
  1183. self.process_subfolders = tk.BooleanVar(value=False)
  1184. self.min_name_length = tk.IntVar(value=5)
  1185. self.max_name_length = tk.IntVar(value=15)
  1186. self.min_folder_length = tk.IntVar(value=5)
  1187. self.max_folder_length = tk.IntVar(value=15)
  1188.  
  1189. # Variables for numbering
  1190. self.enable_numbering = tk.BooleanVar(value=False)
  1191. self.numbering_mode = tk.StringVar(value="sequential")
  1192. self.number_format = tk.StringVar(value="001")
  1193. self.start_number = tk.IntVar(value=1)
  1194. self.custom_prefix = tk.StringVar(value="File_")
  1195. self.custom_suffix = tk.StringVar(value="")
  1196. self.separator = tk.StringVar(value="_")
  1197. self.number_files = tk.BooleanVar(value=True)
  1198. self.number_folders = tk.BooleanVar(value=False)
  1199.  
  1200. # Advanced features
  1201. self.filter_by_extension = tk.BooleanVar(value=False)
  1202. self.extension_filter = tk.StringVar(value=".txt,.jpg,.png,.pdf")
  1203. self.sort_before_numbering = tk.BooleanVar(value=True)
  1204. self.sort_method = tk.StringVar(value="name")
  1205. self.backup_original_names = tk.BooleanVar(value=True)
  1206. self.date_range_start = tk.StringVar(value="2019-01-01")
  1207. self.date_range_end = tk.StringVar(value="2024-12-31")
  1208.  
  1209. # Get script path for context menu
  1210. if getattr(sys, 'frozen', False):
  1211. self.script_path = sys.executable
  1212. else:
  1213. self.script_path = os.path.abspath(__file__)
  1214.  
  1215. self.setup_gui()
  1216. self.auto_load_settings()
  1217.  
  1218. def setup_gui(self):
  1219. # Create notebook for tabs
  1220. self.notebook = ttk.Notebook(self.root)
  1221. self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
  1222.  
  1223. # Main tab
  1224. self.main_tab = ttk.Frame(self.notebook)
  1225. self.notebook.add(self.main_tab, text="Main Operations")
  1226.  
  1227. # Numbering tab
  1228. self.numbering_tab = ttk.Frame(self.notebook)
  1229. self.notebook.add(self.numbering_tab, text="Numbering System")
  1230.  
  1231. # Advanced tab
  1232. self.advanced_tab = ttk.Frame(self.notebook)
  1233. self.notebook.add(self.advanced_tab, text="Advanced Features")
  1234.  
  1235. # Context menu tab
  1236. self.context_tab = ttk.Frame(self.notebook)
  1237. self.notebook.add(self.context_tab, text="Context Menu")
  1238.  
  1239. self.setup_main_tab()
  1240. self.setup_numbering_tab()
  1241. self.setup_advanced_tab()
  1242. self.setup_context_tab()
  1243.  
  1244. # Status bar
  1245. self.status_label = ttk.Label(self.root, text="Ready", relief=tk.SUNKEN)
  1246. self.status_label.pack(fill=tk.X, side=tk.BOTTOM)
  1247.  
  1248. def setup_main_tab(self):
  1249. main_frame = ttk.Frame(self.main_tab, padding="5")
  1250. main_frame.pack(fill=tk.BOTH, expand=True)
  1251.  
  1252. # Folder selection
  1253. folder_frame = ttk.Frame(main_frame)
  1254. folder_frame.pack(fill=tk.X, pady=2)
  1255.  
  1256. ttk.Label(folder_frame, text="Folder:").pack(side=tk.LEFT)
  1257. ttk.Entry(folder_frame, textvariable=self.folder_path, width=50).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
  1258. ttk.Button(folder_frame, text="Browse", command=self.browse_folder).pack(side=tk.RIGHT)
  1259.  
  1260. # Two column layout
  1261. columns_frame = ttk.Frame(main_frame)
  1262. columns_frame.pack(fill=tk.BOTH, expand=True, pady=5)
  1263.  
  1264. # Left column - File options
  1265. left_frame = ttk.LabelFrame(columns_frame, text="File Randomization", padding="5")
  1266. left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 2))
  1267.  
  1268. ttk.Checkbutton(left_frame, text="Randomize filenames", variable=self.randomize_names).pack(anchor=tk.W)
  1269. ttk.Checkbutton(left_frame, text="Randomize creation dates", variable=self.randomize_dates).pack(anchor=tk.W)
  1270. ttk.Checkbutton(left_frame, text="Randomize modified dates", variable=self.randomize_modified).pack(anchor=tk.W)
  1271. ttk.Checkbutton(left_frame, text="Preserve file extensions", variable=self.preserve_extensions).pack(anchor=tk.W)
  1272.  
  1273. length_frame = ttk.Frame(left_frame)
  1274. length_frame.pack(fill=tk.X, pady=2)
  1275. ttk.Label(length_frame, text="Name length:").pack(side=tk.LEFT)
  1276. ttk.Spinbox(length_frame, from_=1, to=30, width=4, textvariable=self.min_name_length).pack(side=tk.LEFT, padx=2)
  1277. ttk.Label(length_frame, text="to").pack(side=tk.LEFT)
  1278. ttk.Spinbox(length_frame, from_=1, to=30, width=4, textvariable=self.max_name_length).pack(side=tk.LEFT, padx=2)
  1279.  
  1280. # Right column - Folder options
  1281. right_frame = ttk.LabelFrame(columns_frame, text="Folder Options", padding="5")
  1282. right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(2, 0))
  1283.  
  1284. ttk.Checkbutton(right_frame, text="Randomize folder names", variable=self.randomize_folders).pack(anchor=tk.W)
  1285. ttk.Checkbutton(right_frame, text="Randomize folder dates", variable=self.randomize_folder_dates).pack(anchor=tk.W)
  1286. ttk.Checkbutton(right_frame, text="Process subfolders", variable=self.process_subfolders).pack(anchor=tk.W)
  1287.  
  1288. folder_length_frame = ttk.Frame(right_frame)
  1289. folder_length_frame.pack(fill=tk.X, pady=2)
  1290. ttk.Label(folder_length_frame, text="Name length:").pack(side=tk.LEFT)
  1291. ttk.Spinbox(folder_length_frame, from_=1, to=30, width=4, textvariable=self.min_folder_length).pack(side=tk.LEFT, padx=2)
  1292. ttk.Label(folder_length_frame, text="to").pack(side=tk.LEFT)
  1293. ttk.Spinbox(folder_length_frame, from_=1, to=30, width=4, textvariable=self.max_folder_length).pack(side=tk.LEFT, padx=2)
  1294.  
  1295. # --- NEW: Preview using Treeview for columns ---
  1296. preview_frame = ttk.LabelFrame(main_frame, text="Items Preview", padding="5")
  1297. preview_frame.pack(fill=tk.BOTH, expand=True, pady=5)
  1298.  
  1299. tree_container = ttk.Frame(preview_frame)
  1300. tree_container.pack(fill=tk.BOTH, expand=True)
  1301.  
  1302. self.preview_tree = ttk.Treeview(tree_container, columns=("original", "new", "type", "size"), show="headings", height=8)
  1303.  
  1304. self.preview_tree.heading("original", text="Original Name")
  1305. self.preview_tree.heading("new", text="New Name (Preview)")
  1306. self.preview_tree.heading("type", text="Type") # New column for file/folder type
  1307. self.preview_tree.heading("size", text="Size")
  1308.  
  1309. self.preview_tree.column("original", width=250, stretch=tk.YES)
  1310. self.preview_tree.column("new", width=250, stretch=tk.YES)
  1311. self.preview_tree.column("type", width=80, stretch=tk.NO) # Adjusted width
  1312. self.preview_tree.column("size", width=100, stretch=tk.NO)
  1313.  
  1314. v_scrollbar = ttk.Scrollbar(tree_container, orient=tk.VERTICAL, command=self.preview_tree.yview)
  1315. h_scrollbar = ttk.Scrollbar(tree_container, orient=tk.HORIZONTAL, command=self.preview_tree.xview)
  1316. self.preview_tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
  1317.  
  1318. self.preview_tree.grid(row=0, column=0, sticky="nsew")
  1319. v_scrollbar.grid(row=0, column=1, sticky="ns")
  1320. h_scrollbar.grid(row=1, column=0, sticky="ew")
  1321.  
  1322. tree_container.grid_columnconfigure(0, weight=1)
  1323. tree_container.grid_rowconfigure(0, weight=1)
  1324.  
  1325. # Preview buttons
  1326. preview_buttons = ttk.Frame(preview_frame)
  1327. preview_buttons.pack(fill=tk.X, pady=(5,0))
  1328. ttk.Button(preview_buttons, text="Load/Refresh Items", command=self.load_items_to_preview).pack(side=tk.LEFT, padx=2)
  1329. ttk.Button(preview_buttons, text="Preview Changes", command=self.preview_changes).pack(side=tk.LEFT, padx=2)
  1330. ttk.Button(preview_buttons, text="Remove Selected", command=self.remove_selected_from_preview).pack(side=tk.LEFT, padx=2)
  1331.  
  1332. # --- NEW: Edit buttons for selected items ---
  1333. ttk.Button(preview_buttons, text="Edit Selected Name", command=self.edit_selected_name).pack(side=tk.LEFT, padx=2)
  1334. ttk.Button(preview_buttons, text="Edit Selected Number", command=self.edit_selected_number).pack(side=tk.LEFT, padx=2)
  1335.  
  1336. # --- NEW: File Type Filters ---
  1337. filter_frame = ttk.LabelFrame(preview_frame, text="Filter by Type", padding="5")
  1338. filter_frame.pack(fill=tk.X, pady=5)
  1339.  
  1340. filter_buttons_row1 = ttk.Frame(filter_frame)
  1341. filter_buttons_row1.pack(fill=tk.X, pady=2)
  1342. ttk.Button(filter_buttons_row1, text="Show All Files", command=lambda: self.filter_preview_by_type("all_files")).pack(side=tk.LEFT, padx=2)
  1343. ttk.Button(filter_buttons_row1, text="Show Folders Only", command=lambda: self.filter_preview_by_type("folders")).pack(side=tk.LEFT, padx=2)
  1344. ttk.Button(filter_buttons_row1, text=".txt", command=lambda: self.filter_preview_by_type(".txt")).pack(side=tk.LEFT, padx=2)
  1345. ttk.Button(filter_buttons_row1, text=".log", command=lambda: self.filter_preview_by_type(".log")).pack(side=tk.LEFT, padx=2)
  1346. ttk.Button(filter_buttons_row1, text=".pdf", command=lambda: self.filter_preview_by_type(".pdf")).pack(side=tk.LEFT, padx=2)
  1347.  
  1348. filter_buttons_row2 = ttk.Frame(filter_frame)
  1349. filter_buttons_row2.pack(fill=tk.X, pady=2)
  1350. ttk.Button(filter_buttons_row2, text=".jpg", command=lambda: self.filter_preview_by_type(".jpg")).pack(side=tk.LEFT, padx=2)
  1351. ttk.Button(filter_buttons_row2, text=".png", command=lambda: self.filter_preview_by_type(".png")).pack(side=tk.LEFT, padx=2)
  1352. ttk.Button(filter_buttons_row2, text=".gif", command=lambda: self.filter_preview_by_type(".gif")).pack(side=tk.LEFT, padx=2)
  1353. ttk.Button(filter_buttons_row2, text=".mp4", command=lambda: self.filter_preview_by_type(".mp4")).pack(side=tk.LEFT, padx=2)
  1354. ttk.Button(filter_buttons_row2, text=".mp3", command=lambda: self.filter_preview_by_type(".mp3")).pack(side=tk.LEFT, padx=2)
  1355.  
  1356.  
  1357. # Action buttons
  1358. action_button_frame = ttk.Frame(main_frame)
  1359. action_button_frame.pack(fill=tk.X, pady=5)
  1360.  
  1361. ttk.Button(action_button_frame, text="Apply Changes", command=self.apply_changes).pack(side=tk.LEFT, padx=2)
  1362.  
  1363. def setup_numbering_tab(self):
  1364. numbering_frame = ttk.Frame(self.numbering_tab, padding="5")
  1365. numbering_frame.pack(fill=tk.BOTH, expand=True)
  1366.  
  1367. ttk.Checkbutton(numbering_frame, text="Enable Numbering System",
  1368. variable=self.enable_numbering, command=self.toggle_numbering).pack(anchor=tk.W, pady=2)
  1369.  
  1370. self.numbering_options_frame = ttk.LabelFrame(numbering_frame, text="Numbering Options", padding="5")
  1371. self.numbering_options_frame.pack(fill=tk.BOTH, expand=True, pady=5)
  1372.  
  1373. mode_frame = ttk.Frame(self.numbering_options_frame)
  1374. mode_frame.pack(fill=tk.X, pady=2)
  1375. ttk.Label(mode_frame, text="Mode:").pack(side=tk.LEFT)
  1376. ttk.Radiobutton(mode_frame, text="Sequential", variable=self.numbering_mode, value="sequential").pack(side=tk.LEFT, padx=5)
  1377. ttk.Radiobutton(mode_frame, text="Random Order", variable=self.numbering_mode, value="random").pack(side=tk.LEFT, padx=5)
  1378.  
  1379. format_frame = ttk.Frame(self.numbering_options_frame)
  1380. format_frame.pack(fill=tk.X, pady=2)
  1381. ttk.Label(format_frame, text="Format:").pack(side=tk.LEFT)
  1382. format_combo = ttk.Combobox(format_frame, textvariable=self.number_format, width=10,
  1383. values=["1", "01", "001", "0001", "A", "a", "I", "i"])
  1384. format_combo.pack(side=tk.LEFT, padx=5)
  1385. ttk.Label(format_frame, text="Start:").pack(side=tk.LEFT, padx=(10, 0))
  1386. ttk.Spinbox(format_frame, from_=0, to=9999, width=6, textvariable=self.start_number).pack(side=tk.LEFT, padx=2)
  1387.  
  1388. pattern_frame = ttk.LabelFrame(self.numbering_options_frame, text="Naming Pattern", padding="5")
  1389. pattern_frame.pack(fill=tk.X, pady=5)
  1390. ttk.Label(pattern_frame, text="Prefix:").pack(side=tk.LEFT)
  1391. ttk.Entry(pattern_frame, textvariable=self.custom_prefix, width=20).pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
  1392. ttk.Label(pattern_frame, text="Separator:").pack(side=tk.LEFT)
  1393. sep_combo = ttk.Combobox(pattern_frame, textvariable=self.separator, width=5, values=["_", "-", ".", " ", ""])
  1394. sep_combo.pack(side=tk.LEFT, padx=5)
  1395. ttk.Label(pattern_frame, text="Suffix:").pack(side=tk.LEFT)
  1396. ttk.Entry(pattern_frame, textvariable=self.custom_suffix, width=20).pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
  1397.  
  1398. apply_frame = ttk.Frame(self.numbering_options_frame)
  1399. apply_frame.pack(fill=tk.X, pady=2)
  1400. ttk.Label(apply_frame, text="Apply to:").pack(side=tk.LEFT)
  1401. ttk.Checkbutton(apply_frame, text="Files", variable=self.number_files).pack(side=tk.LEFT, padx=5)
  1402. ttk.Checkbutton(apply_frame, text="Folders", variable=self.number_folders).pack(side=tk.LEFT, padx=5)
  1403.  
  1404. preview_frame = ttk.LabelFrame(self.numbering_options_frame, text="Preview", padding="5")
  1405. preview_frame.pack(fill=tk.X, pady=5)
  1406. self.preview_label = ttk.Label(preview_frame, text="Example: File_001.txt, File_002.txt, ...")
  1407. self.preview_label.pack(anchor=tk.W)
  1408. ttk.Button(preview_frame, text="Update Preview", command=self.update_numbering_preview).pack(anchor=tk.W, pady=2)
  1409.  
  1410. self.toggle_numbering()
  1411.  
  1412. def setup_advanced_tab(self):
  1413. advanced_frame = ttk.Frame(self.advanced_tab, padding="5")
  1414. advanced_frame.pack(fill=tk.BOTH, expand=True)
  1415.  
  1416. filter_frame = ttk.LabelFrame(advanced_frame, text="File Filtering", padding="5")
  1417. filter_frame.pack(fill=tk.X, pady=2)
  1418. ttk.Checkbutton(filter_frame, text="Filter by extension", variable=self.filter_by_extension).pack(anchor=tk.W)
  1419. ext_frame = ttk.Frame(filter_frame)
  1420. ext_frame.pack(fill=tk.X, pady=2)
  1421. ttk.Label(ext_frame, text="Extensions:").pack(side=tk.LEFT)
  1422. ttk.Entry(ext_frame, textvariable=self.extension_filter, width=30).pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
  1423.  
  1424. sort_frame = ttk.LabelFrame(advanced_frame, text="Sorting Options", padding="5")
  1425. sort_frame.pack(fill=tk.X, pady=2)
  1426. ttk.Checkbutton(sort_frame, text="Sort before numbering", variable=self.sort_before_numbering).pack(anchor=tk.W)
  1427. sort_method_frame = ttk.Frame(sort_frame)
  1428. sort_method_frame.pack(fill=tk.X, pady=2)
  1429. ttk.Label(sort_method_frame, text="Sort by:").pack(side=tk.LEFT)
  1430. sort_combo = ttk.Combobox(sort_method_frame, textvariable=self.sort_method, width=15,
  1431. values=["name", "size", "date_modified", "date_created", "extension"])
  1432. sort_combo.pack(side=tk.LEFT, padx=5)
  1433.  
  1434. date_frame = ttk.LabelFrame(advanced_frame, text="Date Range for Randomization", padding="5")
  1435. date_frame.pack(fill=tk.X, pady=2)
  1436. ttk.Label(date_frame, text="Start Date:").pack(side=tk.LEFT)
  1437. ttk.Entry(date_frame, textvariable=self.date_range_start, width=12).pack(side=tk.LEFT, padx=5)
  1438. ttk.Label(date_frame, text="End Date:").pack(side=tk.LEFT)
  1439. ttk.Entry(date_frame, textvariable=self.date_range_end, width=12).pack(side=tk.LEFT, padx=5)
  1440.  
  1441. backup_frame = ttk.LabelFrame(advanced_frame, text="Backup & Safety", padding="5")
  1442. backup_frame.pack(fill=tk.X, pady=2)
  1443. ttk.Checkbutton(backup_frame, text="Create backup of original names", variable=self.backup_original_names).pack(anchor=tk.W)
  1444.  
  1445. batch_frame = ttk.LabelFrame(advanced_frame, text="Batch Operations", padding="5")
  1446. batch_frame.pack(fill=tk.X, pady=2)
  1447. ttk.Button(batch_frame, text="Save Settings", command=self.save_settings).pack(side=tk.LEFT, padx=2)
  1448. ttk.Button(batch_frame, text="Load Settings", command=self.load_settings).pack(side=tk.LEFT, padx=2)
  1449. ttk.Button(batch_frame, text="Reset All", command=self.reset_settings).pack(side=tk.LEFT, padx=2)
  1450.  
  1451. def setup_context_tab(self):
  1452. context_frame = ttk.Frame(self.context_tab, padding="10")
  1453. context_frame.pack(fill=tk.BOTH, expand=True)
  1454.  
  1455. info_text = ("Right-click context menu integration allows you to quickly access this tool "
  1456. "from Windows Explorer. You can right-click on any folder or empty space in "
  1457. "a folder to access the randomizer.")
  1458. ttk.Label(context_frame, text=info_text, wraplength=600, justify=tk.LEFT).pack(pady=10)
  1459.  
  1460. button_frame = ttk.Frame(context_frame)
  1461. button_frame.pack(pady=10)
  1462. ttk.Button(button_frame, text="Install Context Menu", command=self.install_context_menu).pack(side=tk.LEFT, padx=5)
  1463. ttk.Button(button_frame, text="Uninstall Context Menu", command=self.uninstall_context_menu).pack(side=tk.LEFT, padx=5)
  1464. ttk.Button(button_frame, text="Check Status", command=self.check_context_menu_status).pack(side=tk.LEFT, padx=5)
  1465.  
  1466. def browse_folder(self):
  1467. folder = filedialog.askdirectory()
  1468. if folder:
  1469. self.folder_path.set(folder)
  1470. self.load_items_to_preview()
  1471.  
  1472. def load_items_to_preview(self):
  1473. # Clear existing items
  1474. for i in self.preview_tree.get_children():
  1475. self.preview_tree.delete(i)
  1476. self.preview_items.clear()
  1477.  
  1478. folder = self.folder_path.get()
  1479. if not folder or not os.path.exists(folder):
  1480. self.status_label.config(text="Please select a valid folder")
  1481. return
  1482.  
  1483. if self.process_subfolders.get():
  1484. messagebox.showwarning("Warning", "Preview and selective removal are disabled when 'Process subfolders' is checked.\nChanges will be applied recursively.")
  1485. self.status_label.config(text="Subfolder mode: Preview disabled.")
  1486. return
  1487.  
  1488. try:
  1489. items_to_sort = []
  1490. for item_name in os.listdir(folder):
  1491. item_path = os.path.join(folder, item_name)
  1492. is_folder = os.path.isdir(item_path)
  1493.  
  1494. if not is_folder and self.filter_by_extension.get():
  1495. ext = os.path.splitext(item_name)[1].lower()
  1496. allowed_exts = [e.strip().lower() for e in self.extension_filter.get().split(',')]
  1497. if ext not in allowed_exts:
  1498. continue
  1499.  
  1500. size_str = self.format_size(os.path.getsize(item_path)) if not is_folder else "" # Size only for files
  1501. item_type = "Folder" if is_folder else os.path.splitext(item_name)[1].lower() # Type is extension for files
  1502. if not item_type: item_type = "File" # For files without extension
  1503.  
  1504. items_to_sort.append({
  1505. "name": item_name,
  1506. "path": item_path,
  1507. "is_folder": is_folder,
  1508. "size_str": size_str,
  1509. "type_str": item_type, # Store the type string
  1510. "new_name": "" # Initialize new_name for preview
  1511. })
  1512.  
  1513. # Sort if requested
  1514. if self.sort_before_numbering.get():
  1515. items_to_sort.sort(key=lambda x: self.get_sort_key(x["path"], x["is_folder"]))
  1516.  
  1517. # Populate treeview
  1518. for item_data in items_to_sort:
  1519. item_id = self.preview_tree.insert("", tk.END, values=(item_data["name"], item_data["new_name"], item_data["type_str"], item_data["size_str"]))
  1520. self.preview_items[item_id] = item_data
  1521.  
  1522. self.status_label.config(text=f"Loaded {len(self.preview_items)} items. Ready for preview.")
  1523. except Exception as e:
  1524. messagebox.showerror("Error", f"Failed to read folder: {str(e)}")
  1525.  
  1526. def remove_selected_from_preview(self):
  1527. selected_items = self.preview_tree.selection()
  1528. if not selected_items:
  1529. messagebox.showwarning("No Selection", "Please select items in the list to remove.")
  1530. return
  1531.  
  1532. for item_id in selected_items:
  1533. self.preview_tree.delete(item_id)
  1534. if item_id in self.preview_items:
  1535. del self.preview_items[item_id]
  1536.  
  1537. self.status_label.config(text=f"Removed items. {len(self.preview_items)} items remaining.")
  1538.  
  1539. def preview_changes(self):
  1540. if not self.preview_items:
  1541. messagebox.showwarning("No Items", "Please load items into the preview list first.")
  1542. return
  1543.  
  1544. all_item_ids = self.preview_tree.get_children()
  1545.  
  1546. # Create separate lists for files and folders to handle numbering/randomization order
  1547. files_to_process = []
  1548. folders_to_process = []
  1549. for iid in all_item_ids:
  1550. item = self.preview_items[iid]
  1551. # Reset new_name for a fresh preview calculation
  1552. item['new_name'] = ""
  1553. if item['is_folder']:
  1554. folders_to_process.append(item)
  1555. else:
  1556. files_to_process.append(item)
  1557.  
  1558. # --- Handle numbering ---
  1559. if self.enable_numbering.get():
  1560. if self.numbering_mode.get() == 'random':
  1561. random.shuffle(files_to_process)
  1562. random.shuffle(folders_to_process)
  1563.  
  1564. current_number = self.start_number.get()
  1565.  
  1566. # Number folders
  1567. if self.number_folders.get():
  1568. for item in folders_to_process:
  1569. item['new_name'] = self.generate_numbered_name(item['name'], current_number, is_folder=True)
  1570. current_number += 1
  1571.  
  1572. # Number files
  1573. if self.number_files.get():
  1574. for item in files_to_process:
  1575. item['new_name'] = self.generate_numbered_name(item['name'], current_number, is_folder=False)
  1576. current_number += 1
  1577.  
  1578. # --- Handle randomization (for items not already numbered or if numbering is off) ---
  1579. # Note: If numbering is enabled, and an item is numbered, randomization is skipped for that item.
  1580. if self.randomize_folders.get():
  1581. for item in folders_to_process:
  1582. if not item['new_name']: # Only randomize if not already numbered
  1583. item['new_name'] = self.generate_random_name(item['name'], is_folder=True)
  1584.  
  1585. if self.randomize_names.get():
  1586. for item in files_to_process:
  1587. if not item['new_name']: # Only randomize if not already numbered
  1588. item['new_name'] = self.generate_random_name(item['name'], is_folder=False)
  1589.  
  1590. # --- Update the Treeview ---
  1591. # Check for potential name collisions in the preview
  1592. new_name_counts = defaultdict(int)
  1593. for iid in all_item_ids:
  1594. item = self.preview_items[iid]
  1595. if item.get('new_name'): # Only count if a new name was generated
  1596. new_name_counts[item['new_name'].lower()] += 1
  1597.  
  1598. # Update tree and handle collisions in preview (by adding a temporary suffix)
  1599. for iid in all_item_ids:
  1600. item = self.preview_items[iid]
  1601. new_name = item.get('new_name', "")
  1602.  
  1603. # If no new name was generated (e.g., randomization/numbering off for this type), use original
  1604. if not new_name:
  1605. new_name = item['name']
  1606. item['new_name'] = new_name # Ensure new_name is set for consistency
  1607.  
  1608. # Apply temporary collision suffix for preview if needed
  1609. if new_name_counts[new_name.lower()] > 1:
  1610. # Add a suffix to distinguish duplicates in the preview
  1611. name, ext = os.path.splitext(new_name)
  1612. # Ensure the temporary suffix doesn't conflict with actual file names
  1613. temp_suffix = f"__DUP{random.randint(100, 999)}"
  1614. new_name_final_for_preview = f"{name}{temp_suffix}{ext}"
  1615. self.preview_tree.set(iid, "new", new_name_final_for_preview)
  1616. else:
  1617. self.preview_tree.set(iid, "new", new_name)
  1618.  
  1619. self.status_label.config(text="Preview generated. Review the 'New Name' column.")
  1620.  
  1621. def apply_changes(self):
  1622. folder = self.folder_path.get()
  1623. if not folder or not os.path.exists(folder):
  1624. messagebox.showerror("Error", "Please select a valid folder")
  1625. return
  1626.  
  1627. # If processing subfolders, use the original recursive logic
  1628. if self.process_subfolders.get():
  1629. self.apply_changes_recursively()
  1630. return
  1631.  
  1632. # --- Apply changes based on the preview tree ---
  1633. item_ids = self.preview_tree.get_children()
  1634. if not item_ids:
  1635. messagebox.showwarning("No Items", "There are no items to process.")
  1636. return
  1637.  
  1638. if not messagebox.askyesno("Confirm Changes", f"You are about to rename/modify {len(item_ids)} items in:\n{folder}\n\nThis action cannot be undone. Proceed?"):
  1639. return
  1640.  
  1641. self.create_backup(folder)
  1642. processed_count = 0
  1643. errors = []
  1644.  
  1645. # We process in reverse order of name to avoid conflicts if renaming 'a' to 'b' and 'b' to 'c'
  1646. # This sorting is based on the ORIGINAL name, which is safer for actual file system operations.
  1647. sorted_item_ids = sorted(item_ids, key=lambda iid: self.preview_items[iid]['name'].lower(), reverse=True)
  1648.  
  1649. for iid in sorted_item_ids:
  1650. item_data = self.preview_items[iid]
  1651. original_path = item_data['path']
  1652. # Get the *actual* new name from the item_data, not the potentially temp-suffixed one from treeview
  1653. new_name_from_preview = item_data.get('new_name', item_data['name'])
  1654.  
  1655. try:
  1656. # --- 1. Rename file/folder ---
  1657. # Only rename if the new name is different from the original name
  1658. if new_name_from_preview and new_name_from_preview != item_data['name']:
  1659. new_path = os.path.join(folder, new_name_from_preview)
  1660.  
  1661. # Ensure the new path is unique before renaming (for actual file system)
  1662. counter = 1
  1663. final_new_path = new_path
  1664. while os.path.exists(final_new_path):
  1665. name, ext = os.path.splitext(new_name_from_preview)
  1666. final_new_name_unique = f"{name}_conflict{counter}{ext}"
  1667. final_new_path = os.path.join(folder, final_new_name_unique)
  1668. counter += 1
  1669.  
  1670. os.rename(original_path, final_new_path)
  1671. current_path = final_new_path
  1672. else:
  1673. current_path = original_path
  1674.  
  1675. # --- 2. Change dates ---
  1676. is_folder = item_data['is_folder']
  1677. if (not is_folder and (self.randomize_dates.get() or self.randomize_modified.get())) or \
  1678. (is_folder and self.randomize_folder_dates.get()):
  1679.  
  1680. creation_time = self.generate_random_date() if (self.randomize_dates.get() and not is_folder) or (self.randomize_folder_dates.get() and is_folder) else datetime.now()
  1681. modified_time = self.generate_random_date() if (self.randomize_modified.get() and not is_folder) or (self.randomize_folder_dates.get() and is_folder) else datetime.now()
  1682. self.set_file_times(current_path, creation_time, modified_time)
  1683.  
  1684. processed_count += 1
  1685. except Exception as e:
  1686. errors.append(f"Failed '{item_data['name']}' to '{new_name_from_preview}': {e}")
  1687.  
  1688. # --- Final report ---
  1689. message = f"Operation complete!\nProcessed {processed_count} items."
  1690. if errors:
  1691. message += f"\n\nErrors ({len(errors)}):\n" + "\n".join(errors[:5])
  1692. if len(errors) > 5:
  1693. message += f"\n... and {len(errors) - 5} more errors."
  1694.  
  1695. messagebox.showinfo("Results", message)
  1696. self.load_items_to_preview() # Refresh the list
  1697.  
  1698. def apply_changes_recursively(self):
  1699. """ Fallback to original logic when processing subfolders """
  1700. if messagebox.askyesno("Confirm Recursive Changes", "You are about to apply changes to this folder AND ALL ITS SUBFOLDERS.\nThis can affect many files and cannot be undone.\n\nAre you sure you want to proceed?"):
  1701. self.randomize_files() # This was the original entry point for recursive ops
  1702.  
  1703. def format_size(self, size):
  1704. for unit in ['B', 'KB', 'MB', 'GB']:
  1705. if size < 1024.0:
  1706. return f"{size:.1f} {unit}"
  1707. size /= 1024.0
  1708. return f"{size:.1f} TB"
  1709.  
  1710. def get_sort_key(self, file_path, is_folder):
  1711. sort_method = self.sort_method.get()
  1712. try:
  1713. if sort_method == "name":
  1714. return os.path.basename(file_path).lower()
  1715. if sort_method == "size" and not is_folder:
  1716. return os.path.getsize(file_path)
  1717. if sort_method == "date_modified":
  1718. return os.path.getmtime(file_path)
  1719. if sort_method == "date_created":
  1720. return os.path.getctime(file_path)
  1721. if sort_method == "extension" and not is_folder:
  1722. return os.path.splitext(file_path)[1].lower()
  1723. except OSError: # Handle cases where file might be moved/deleted
  1724. pass
  1725. return os.path.basename(file_path).lower()
  1726.  
  1727. def generate_random_name(self, original_name, is_folder=False):
  1728. if not is_folder and self.preserve_extensions.get():
  1729. name, ext = os.path.splitext(original_name)
  1730. else:
  1731. ext = ""
  1732. length = random.randint(self.min_folder_length.get() if is_folder else self.min_name_length.get(),
  1733. self.max_folder_length.get() if is_folder else self.max_name_length.get())
  1734. chars = string.ascii_letters + string.digits
  1735. return ''.join(random.choice(chars) for _ in range(length)) + ext
  1736.  
  1737. def generate_numbered_name(self, original_name, number, is_folder=False):
  1738. if not is_folder and self.preserve_extensions.get():
  1739. _, ext = os.path.splitext(original_name)
  1740. else:
  1741. ext = ""
  1742.  
  1743. format_str = self.number_format.get()
  1744. if format_str.isdigit(): formatted_number = str(number).zfill(len(format_str))
  1745. elif format_str == "A": formatted_number = self.number_to_alpha(number, upper=True)
  1746. elif format_str == "a": formatted_number = self.number_to_alpha(number, upper=False)
  1747. elif format_str == "I": formatted_number = self.number_to_roman(number, upper=True)
  1748. elif format_str == "i": formatted_number = self.number_to_roman(number, upper=False)
  1749. else: formatted_number = str(number)
  1750.  
  1751. parts = [p for p in [self.custom_prefix.get(), formatted_number, self.custom_suffix.get()] if p]
  1752. return self.separator.get().join(parts) + ext
  1753.  
  1754. def number_to_alpha(self, num, upper=True):
  1755. result = ""
  1756. base = 65 if upper else 97
  1757. while num > 0:
  1758. num, rem = divmod(num - 1, 26)
  1759. result = chr(base + rem) + result
  1760. return result or ("A" if upper else "a")
  1761.  
  1762. def number_to_roman(self, num, upper=True):
  1763. val_map = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"),
  1764. (90, "XC"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]
  1765. result = ""
  1766. for val, sym in val_map:
  1767. while num >= val:
  1768. result += sym
  1769. num -= val
  1770. return result if upper else result.lower()
  1771.  
  1772. def generate_random_date(self):
  1773. try:
  1774. start_date = datetime.strptime(self.date_range_start.get(), "%Y-%m-%d")
  1775. end_date = datetime.strptime(self.date_range_end.get(), "%Y-%m-%d")
  1776. delta = end_date - start_date
  1777. return start_date + timedelta(seconds=random.randint(0, int(delta.total_seconds())))
  1778. except (ValueError, TypeError):
  1779. end_date = datetime.now()
  1780. start_date = end_date - timedelta(days=365 * 5)
  1781. delta = end_date - start_date
  1782. return start_date + timedelta(seconds=random.randint(0, int(delta.total_seconds())))
  1783.  
  1784. def set_file_times(self, filepath, creation_time, modified_time):
  1785. try:
  1786. os.utime(filepath, (modified_time.timestamp(), modified_time.timestamp()))
  1787. if sys.platform == 'win32':
  1788. # More robust method for setting creation time on Windows
  1789. subprocess.run(['powershell', '-command', f'(Get-Item "{filepath}").CreationTime=("{(creation_time.isoformat())}")'], check=True, capture_output=True)
  1790. except Exception as e:
  1791. print(f"Warning: Could not set file times for {os.path.basename(filepath)}: {e}")
  1792.  
  1793. def create_backup(self, folder_path):
  1794. if not self.backup_original_names.get(): return
  1795. try:
  1796. backup_file = os.path.join(folder_path, f"filename_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json")
  1797. backup_data = {}
  1798. for item in os.listdir(folder_path):
  1799. item_path = os.path.join(folder_path, item)
  1800. backup_data[item] = {
  1801. 'is_folder': os.path.isdir(item_path),
  1802. 'size': os.path.getsize(item_path) if os.path.isfile(item_path) else 0,
  1803. 'modified': os.path.getmtime(item_path),
  1804. 'created': os.path.getctime(item_path)
  1805. }
  1806. with open(backup_file, 'w') as f: json.dump(backup_data, f, indent=2)
  1807. except Exception as e:
  1808. print(f"Warning: Could not create backup: {e}")
  1809.  
  1810. def randomize_files(self):
  1811. """Apply randomization recursively (original method for subfolder processing)"""
  1812. folder = self.folder_path.get()
  1813. if not folder or not os.path.exists(folder):
  1814. messagebox.showerror("Error", "Please select a valid folder")
  1815. return
  1816.  
  1817. self.create_backup(folder)
  1818. processed_files, processed_folders, errors = [0], [0], []
  1819. self._randomize_folder_recursive(folder, errors, processed_files, processed_folders)
  1820.  
  1821. message = f"Recursive Randomization complete!\nProcessed {processed_files[0]} files and {processed_folders[0]} folders"
  1822. if errors:
  1823. message += f"\n\nErrors ({len(errors)}):\n" + "\n".join(errors[:5])
  1824. messagebox.showinfo("Results", message)
  1825. self.load_items_to_preview()
  1826.  
  1827. def _randomize_folder_recursive(self, folder_path, errors, processed_files, processed_folders):
  1828. try:
  1829. items = os.listdir(folder_path)
  1830. for item in items:
  1831. original_path = os.path.join(folder_path, item)
  1832. if os.path.isdir(original_path) and self.process_subfolders.get():
  1833. self._randomize_folder_recursive(original_path, errors, processed_files, processed_folders)
  1834.  
  1835. # Process items after recursion to avoid renaming a folder before processing its contents
  1836. for item in items:
  1837. original_path = os.path.join(folder_path, item)
  1838. try:
  1839. if os.path.isfile(original_path):
  1840. # Apply file operations
  1841. new_name = self.generate_random_name(item, is_folder=False) if self.randomize_names.get() else item
  1842. new_path = os.path.join(folder_path, new_name)
  1843. os.rename(original_path, new_path)
  1844.  
  1845. if self.randomize_dates.get() or self.randomize_modified.get():
  1846. self.set_file_times(new_path, self.generate_random_date(), self.generate_random_date())
  1847. processed_files[0] += 1
  1848.  
  1849. elif os.path.isdir(original_path):
  1850. # Apply folder operations
  1851. new_name = self.generate_random_name(item, is_folder=True) if self.randomize_folders.get() else item
  1852. new_path = os.path.join(folder_path, new_name)
  1853. if new_name != item: os.rename(original_path, new_path)
  1854.  
  1855. if self.randomize_folder_dates.get():
  1856. self.set_file_times(new_path, self.generate_random_date(), self.generate_random_date())
  1857. processed_folders[0] += 1
  1858.  
  1859. except Exception as e: errors.append(f"Failed '{item}': {e}")
  1860. except Exception as e: errors.append(f"Error in folder '{folder_path}': {e}")
  1861.  
  1862. def update_numbering_preview(self):
  1863. try:
  1864. start_num = self.start_number.get()
  1865. examples = [self.generate_numbered_name("example.txt", start_num + i) for i in range(3)]
  1866. self.preview_label.config(text=f"Examples: {', '.join(examples)}, ...")
  1867. except Exception:
  1868. self.preview_label.config(text="Preview error - check settings")
  1869.  
  1870. def save_settings(self):
  1871. self.settings_dir.mkdir(parents=True, exist_ok=True)
  1872. filename = filedialog.asksaveasfilename(
  1873. initialdir=self.settings_dir,
  1874. defaultextension=".json",
  1875. filetypes=[("JSON files", "*.json")],
  1876. title="Save Settings"
  1877. )
  1878. if not filename: return
  1879.  
  1880. settings = {k: v.get() for k, v in self.__dict__.items() if isinstance(v, tk.Variable)}
  1881. try:
  1882. with open(filename, 'w') as f: json.dump(settings, f, indent=2)
  1883. messagebox.showinfo("Success", "Settings saved successfully!")
  1884. except Exception as e:
  1885. messagebox.showerror("Error", f"Failed to save settings: {e}")
  1886.  
  1887. def load_settings(self):
  1888. self.settings_dir.mkdir(parents=True, exist_ok=True)
  1889. filename = filedialog.askopenfilename(
  1890. initialdir=self.settings_dir,
  1891. filetypes=[("JSON files", "*.json")],
  1892. title="Load Settings"
  1893. )
  1894. if filename:
  1895. self._load_settings_from_path(filename)
  1896.  
  1897. def _load_settings_from_path(self, filepath):
  1898. try:
  1899. with open(filepath, 'r') as f: settings = json.load(f)
  1900. for key, value in settings.items():
  1901. if hasattr(self, key) and isinstance(getattr(self, key), tk.Variable):
  1902. getattr(self, key).set(value)
  1903. self.toggle_numbering()
  1904. messagebox.showinfo("Success", f"Settings loaded from {os.path.basename(filepath)}!")
  1905. except Exception as e:
  1906. messagebox.showerror("Error", f"Failed to load settings: {e}")
  1907.  
  1908. def auto_load_settings(self):
  1909. self.settings_dir.mkdir(parents=True, exist_ok=True)
  1910. default_settings_file = self.settings_dir / "file_randomizer_advancedv2.json"
  1911. if default_settings_file.exists():
  1912. print(f"Found default settings at {default_settings_file}, loading...")
  1913. try:
  1914. with open(default_settings_file, 'r') as f: settings = json.load(f)
  1915. for key, value in settings.items():
  1916. if hasattr(self, key) and isinstance(getattr(self, key), tk.Variable):
  1917. getattr(self, key).set(value)
  1918. self.toggle_numbering()
  1919. self.status_label.config(text="Default settings loaded automatically.")
  1920. except Exception as e:
  1921. self.status_label.config(text=f"Error loading default settings: {e}")
  1922. else:
  1923. print(f"No default settings file found at {default_settings_file}")
  1924.  
  1925. def reset_settings(self):
  1926. if messagebox.askyesno("Confirm Reset", "Are you sure you want to reset all settings?"):
  1927. # Re-initialize to reset all tk.Variables to their default values
  1928. self.__init__() # This is a bit of a shortcut, but effective here
  1929. messagebox.showinfo("Success", "Settings reset to defaults!")
  1930.  
  1931. def toggle_numbering(self):
  1932. state = tk.NORMAL if self.enable_numbering.get() else tk.DISABLED
  1933. for child in self.numbering_options_frame.winfo_children():
  1934. self._set_state_recursive(child, state)
  1935.  
  1936. def _set_state_recursive(self, widget, state):
  1937. try: widget.configure(state=state)
  1938. except tk.TclError: pass
  1939. for child in widget.winfo_children(): self._set_state_recursive(child, state)
  1940.  
  1941. # --- NEW: Edit Name and Number Functions ---
  1942. def edit_selected_name(self):
  1943. selected_items = self.preview_tree.selection()
  1944. if not selected_items:
  1945. messagebox.showwarning("No Selection", "Please select an item to edit its name.")
  1946. return
  1947. if len(selected_items) > 1:
  1948. messagebox.showwarning("Multiple Selection", "Please select only one item to edit its name.")
  1949. return
  1950.  
  1951. item_id = selected_items[0]
  1952. item_data = self.preview_items[item_id]
  1953. current_new_name = self.preview_tree.set(item_id, "new")
  1954. original_name = item_data['name']
  1955.  
  1956. # Create a Toplevel window for editing
  1957. edit_window = tk.Toplevel(self.root)
  1958. edit_window.title("Edit New Name")
  1959. edit_window.transient(self.root) # Make it appear on top of the main window
  1960. edit_window.grab_set() # Make it modal
  1961.  
  1962. # Center the dialog
  1963. self.root.update_idletasks()
  1964. x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (edit_window.winfo_width() // 2)
  1965. y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (edit_window.winfo_height() // 2)
  1966. edit_window.geometry(f"+{x-100}+{y-50}") # Adjust for initial size
  1967.  
  1968. ttk.Label(edit_window, text=f"Original Name: {original_name}").pack(padx=10, pady=5)
  1969. ttk.Label(edit_window, text="Enter New Name:").pack(padx=10, pady=5)
  1970.  
  1971. new_name_var = tk.StringVar(value=current_new_name)
  1972. name_entry = ttk.Entry(edit_window, textvariable=new_name_var, width=50)
  1973. name_entry.pack(padx=10, pady=5)
  1974. name_entry.focus_set()
  1975.  
  1976. def save_new_name():
  1977. new_name = new_name_var.get().strip()
  1978. if new_name:
  1979. item_data['new_name'] = new_name # Update internal data
  1980. self.preview_tree.set(item_id, "new", new_name) # Update treeview
  1981. self.status_label.config(text=f"Name for '{original_name}' updated to '{new_name}'.")
  1982. edit_window.destroy()
  1983. else:
  1984. messagebox.showwarning("Input Error", "New name cannot be empty.")
  1985.  
  1986. ttk.Button(edit_window, text="Save", command=save_new_name).pack(pady=10)
  1987. edit_window.bind('<Return>', lambda e: save_new_name()) # Allow Enter to save
  1988. edit_window.bind('<Escape>', lambda e: edit_window.destroy()) # Allow Escape to close
  1989.  
  1990. def edit_selected_number(self):
  1991. selected_items = self.preview_tree.selection()
  1992. if not selected_items:
  1993. messagebox.showwarning("No Selection", "Please select an item to edit its number.")
  1994. return
  1995. if len(selected_items) > 1:
  1996. messagebox.showwarning("Multiple Selection", "Please select only one item to edit its number.")
  1997. return
  1998. if not self.enable_numbering.get():
  1999. messagebox.showwarning("Numbering Disabled", "Please enable numbering system on the 'Numbering System' tab first.")
  2000. return
  2001.  
  2002. item_id = selected_items[0]
  2003. item_data = self.preview_items[item_id]
  2004. original_name = item_data['name']
  2005. is_folder = item_data['is_folder']
  2006.  
  2007. # Try to extract current number if it exists
  2008. current_new_name = self.preview_tree.set(item_id, "new")
  2009. current_number_match = re.search(r'(\d+)', current_new_name) # Simple regex to find a number
  2010. initial_number = int(current_number_match.group(1)) if current_number_match else self.start_number.get()
  2011.  
  2012. edit_window = tk.Toplevel(self.root)
  2013. edit_window.title("Edit Number")
  2014. edit_window.transient(self.root)
  2015. edit_window.grab_set()
  2016.  
  2017. self.root.update_idletasks()
  2018. x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (edit_window.winfo_width() // 2)
  2019. y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (edit_window.winfo_height() // 2)
  2020. edit_window.geometry(f"+{x-100}+{y-50}")
  2021.  
  2022. ttk.Label(edit_window, text=f"Original Name: {original_name}").pack(padx=10, pady=5)
  2023. ttk.Label(edit_window, text="Enter New Number:").pack(padx=10, pady=5)
  2024.  
  2025. new_number_var = tk.IntVar(value=initial_number)
  2026. number_spinbox = ttk.Spinbox(edit_window, from_=0, to=99999, textvariable=new_number_var, width=10)
  2027. number_spinbox.pack(padx=10, pady=5)
  2028. number_spinbox.focus_set()
  2029.  
  2030. def save_new_number():
  2031. try:
  2032. new_number = new_number_var.get()
  2033. if new_number < 0:
  2034. messagebox.showwarning("Input Error", "Number cannot be negative.")
  2035. return
  2036.  
  2037. # Regenerate the new name based on the new number and current numbering settings
  2038. new_name = self.generate_numbered_name(original_name, new_number, is_folder=is_folder)
  2039.  
  2040. item_data['new_name'] = new_name # Update internal data
  2041. self.preview_tree.set(item_id, "new", new_name) # Update treeview
  2042. self.status_label.config(text=f"Number for '{original_name}' updated to '{new_number}'.")
  2043. edit_window.destroy()
  2044. except ValueError:
  2045. messagebox.showwarning("Input Error", "Please enter a valid number.")
  2046.  
  2047. ttk.Button(edit_window, text="Save", command=save_new_number).pack(pady=10)
  2048. edit_window.bind('<Return>', lambda e: save_new_number())
  2049. edit_window.bind('<Escape>', lambda e: edit_window.destroy())
  2050.  
  2051. # --- NEW: Filter Preview by Type Function ---
  2052. def filter_preview_by_type(self, filter_type):
  2053. # Clear any existing filters (by showing all first)
  2054. for item_id in self.preview_items:
  2055. self.preview_tree.item(item_id, open=True) # Ensure it's not hidden by parent collapse
  2056. self.preview_tree.reattach(item_id, '', tk.END) # Re-add to root to make visible
  2057.  
  2058. if filter_type == "all_files":
  2059. self.status_label.config(text="Showing all files and folders.")
  2060. return # All are already visible
  2061.  
  2062. items_to_hide = []
  2063. for item_id, item_data in self.preview_items.items():
  2064. is_folder = item_data['is_folder']
  2065. item_type = item_data['type_str'] # e.g., "Folder", ".txt"
  2066.  
  2067. should_hide = False
  2068. if filter_type == "folders":
  2069. if not is_folder:
  2070. should_hide = True
  2071. elif filter_type.startswith("."): # Specific extension filter
  2072. if is_folder or item_type != filter_type:
  2073. should_hide = True
  2074. else: # Should not happen with current buttons, but as a fallback
  2075. should_hide = True
  2076.  
  2077. if should_hide:
  2078. items_to_hide.append(item_id)
  2079.  
  2080. for item_id in items_to_hide:
  2081. self.preview_tree.detach(item_id) # Detach to hide
  2082.  
  2083. self.status_label.config(text=f"Filtered by: {filter_type}. Showing {len(self.preview_items) - len(items_to_hide)} items.")
  2084.  
  2085.  
  2086. # --- Context Menu Methods ---
  2087. def install_context_menu(self):
  2088. try:
  2089. command = f'"{sys.executable}" "{self.script_path}" "%V"'
  2090. bg_key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, r"Directory\Background\shell\AdvancedRandomizeFiles")
  2091. winreg.SetValue(bg_key, "", winreg.REG_SZ, "Advanced File & Folder Randomizer")
  2092. winreg.SetValueEx(bg_key, "Icon", 0, winreg.REG_SZ, f'"{self.script_path}",0')
  2093. winreg.SetValue(winreg.CreateKey(bg_key, "command"), "", winreg.REG_SZ, command)
  2094.  
  2095. command = f'"{sys.executable}" "{self.script_path}" "%1"'
  2096. folder_key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, r"Directory\shell\AdvancedRandomizeFiles")
  2097. winreg.SetValue(folder_key, "", winreg.REG_SZ, "Advanced File & Folder Randomizer")
  2098. winreg.SetValueEx(folder_key, "Icon", 0, winreg.REG_SZ, f'"{self.script_path}",0')
  2099. winreg.SetValue(winreg.CreateKey(folder_key, "command"), "", winreg.REG_SZ, command)
  2100.  
  2101. messagebox.showinfo("Success", "Context menu installed successfully!")
  2102. except Exception as e:
  2103. messagebox.showerror("Error", f"Failed to install context menu (run as admin?): {e}")
  2104.  
  2105. def uninstall_context_menu(self):
  2106. try:
  2107. for path in [r"Directory\Background\shell\AdvancedRandomizeFiles", r"Directory\shell\AdvancedRandomizeFiles"]:
  2108. try: winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, path + r"\command")
  2109. except FileNotFoundError: pass
  2110. try: winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, path)
  2111. except FileNotFoundError: pass
  2112. messagebox.showinfo("Success", "Context menu uninstalled successfully!")
  2113. except Exception as e:
  2114. messagebox.showerror("Error", f"Failed to uninstall context menu (run as admin?): {e}")
  2115.  
  2116. def check_context_menu_status(self):
  2117. try:
  2118. winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"Directory\shell\AdvancedRandomizeFiles")
  2119. messagebox.showinfo("Status", "Context menu appears to be installed.")
  2120. except FileNotFoundError:
  2121. messagebox.showinfo("Status", "Context menu is not installed.")
  2122. except Exception as e:
  2123. messagebox.showerror("Error", f"Failed to check status: {e}")
  2124.  
  2125. def run(self):
  2126. if len(sys.argv) > 1 and os.path.isdir(sys.argv[1]):
  2127. self.folder_path.set(sys.argv[1])
  2128. self.load_items_to_preview()
  2129. self.root.mainloop()
  2130.  
  2131. if __name__ == "__main__":
  2132. app = AdvancedFileRandomizer()
  2133. app.run()
  2134. --------------------------------------------------------------------------------
  2135. --- END: file_randomizer_advancedv2.py ---
  2136.  
  2137.  
  2138. --- START: fixed_exiftool_gui.py ---
  2139. --------------------------------------------------------------------------------
  2140. import tkinter as tk
  2141. from tkinter import filedialog, messagebox, scrolledtext, ttk
  2142. import subprocess
  2143. import os
  2144. import sys
  2145. import winreg
  2146. import ctypes
  2147. import traceback
  2148. from pathlib import Path
  2149. import threading
  2150. from PIL import Image, ImageTk
  2151. import datetime
  2152.  
  2153. try:
  2154. import piexif
  2155. from PIL import Image
  2156. HAS_PIEXIF = True
  2157. except ImportError:
  2158. HAS_PIEXIF = False
  2159. print("Warning: piexif and/or PIL not installed. Using exiftool only.")
  2160.  
  2161. class AdvancedExifToolGUI(tk.Tk):
  2162. def __init__(self):
  2163. super().__init__()
  2164. self.title("Advanced Universal Comment Editor")
  2165. self.geometry("1200x800")
  2166. self.minsize(800, 600)
  2167.  
  2168. # --- Point directly to C:\Windows\exiftool.exe ---
  2169. self.exiftool_path = r"C:\Windows\exiftool.exe"
  2170. if not os.path.exists(self.exiftool_path):
  2171. messagebox.showerror("Error", f"exiftool.exe not found at:\n{self.exiftool_path}")
  2172. self.destroy()
  2173. return
  2174.  
  2175. # Supported image/video extensions
  2176. self.supported_extensions = {
  2177. '.jpg', '.jpeg', '.png', '.tiff', '.tif', '.bmp', '.gif', '.webp',
  2178. '.cr2', '.nef', '.arw', '.dng', '.raw', '.orf', '.rw2', '.pef',
  2179. '.mp4', '.mov', '.avi', '.mkv', '.wmv', '.flv', '.m4v', '.3gp'
  2180. }
  2181.  
  2182. # Variables
  2183. self.current_folder = tk.StringVar()
  2184. self.current_file = tk.StringVar()
  2185. self.file_list = []
  2186. self.selected_file_index = None
  2187. self.thumbnails = {}
  2188. self.modified_comments = {}
  2189.  
  2190. self.configure_layout()
  2191. self.create_menu()
  2192. self.create_widgets()
  2193.  
  2194. # Load folder if passed as an argument
  2195. if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
  2196. if os.path.isfile(sys.argv[1]):
  2197. folder = os.path.dirname(sys.argv[1])
  2198. self.after(500, lambda: self.load_folder(folder, select_file=sys.argv[1]))
  2199. else:
  2200. self.after(500, lambda: self.load_folder(sys.argv[1]))
  2201.  
  2202. def configure_layout(self):
  2203. self.grid_rowconfigure(1, weight=1)
  2204. self.grid_columnconfigure(0, weight=1)
  2205.  
  2206. def create_menu(self):
  2207. menu_bar = tk.Menu(self)
  2208. self.config(menu=menu_bar)
  2209.  
  2210. file_menu = tk.Menu(menu_bar, tearoff=0)
  2211. menu_bar.add_cascade(label="File", menu=file_menu)
  2212. file_menu.add_command(label="Open Folder...", command=self.browse_folder, accelerator="Ctrl+O")
  2213. file_menu.add_command(label="Refresh Folder", command=self.refresh_folder, accelerator="F5")
  2214. file_menu.add_separator()
  2215. file_menu.add_command(label="Exit", command=self.quit, accelerator="Ctrl+Q")
  2216.  
  2217. edit_menu = tk.Menu(menu_bar, tearoff=0)
  2218. menu_bar.add_cascade(label="Edit", menu=edit_menu)
  2219. # --- NEW: Undo/Redo Menu Items ---
  2220. edit_menu.add_command(label="Undo", command=self.undo_comment, accelerator="Ctrl+Z")
  2221. edit_menu.add_command(label="Redo", command=self.redo_comment, accelerator="Ctrl+Y")
  2222. edit_menu.add_separator()
  2223. edit_menu.add_command(label="Save Comment", command=self.save_metadata, accelerator="Ctrl+S")
  2224. edit_menu.add_command(label="Save All Comments", command=self.save_all_comments, accelerator="Ctrl+Shift+S")
  2225. edit_menu.add_separator()
  2226. edit_menu.add_command(label="Copy Comment", command=self.copy_comment, accelerator="Ctrl+C")
  2227. edit_menu.add_command(label="Paste Comment", command=self.paste_comment, accelerator="Ctrl+V")
  2228. edit_menu.add_command(label="Clear Comment", command=self.clear_comment, accelerator="Delete")
  2229.  
  2230. view_menu = tk.Menu(menu_bar, tearoff=0)
  2231. menu_bar.add_cascade(label="View", menu=view_menu)
  2232. view_menu.add_command(label="Show Thumbnails", command=self.toggle_thumbnails)
  2233. view_menu.add_command(label="Zoom In Preview", command=self.zoom_in_preview, accelerator="Ctrl++")
  2234. view_menu.add_command(label="Zoom Out Preview", command=self.zoom_out_preview, accelerator="Ctrl+-")
  2235.  
  2236. tools_menu = tk.Menu(menu_bar, tearoff=0)
  2237. menu_bar.add_cascade(label="Tools", menu=tools_menu)
  2238. tools_menu.add_command(label="Batch Apply Comment", command=self.batch_apply_comment)
  2239. tools_menu.add_command(label="Export Comments to Text", command=self.export_comments)
  2240. tools_menu.add_command(label="Import Comments from Text", command=self.import_comments)
  2241. tools_menu.add_separator()
  2242. tools_menu.add_command(label="Add to Right-Click Menu", command=self.install_context_menu)
  2243. tools_menu.add_command(label="Remove from Right-Click Menu", command=self.uninstall_context_menu)
  2244.  
  2245. self.bind('<Control-o>', lambda e: self.browse_folder())
  2246. self.bind('<F5>', lambda e: self.refresh_folder())
  2247. self.bind('<Control-q>', lambda e: self.quit())
  2248. self.bind('<Control-s>', lambda e: self.save_metadata())
  2249. self.bind('<Control-Shift-S>', lambda e: self.save_all_comments())
  2250. self.bind('<Control-c>', lambda e: self.copy_comment())
  2251. self.bind('<Control-v>', lambda e: self.paste_comment())
  2252. # Note: Ctrl+Z and Ctrl+Y are handled by the text widget itself due to `undo=True`
  2253.  
  2254. def create_widgets(self):
  2255. toolbar = tk.Frame(self, relief=tk.RAISED, bd=1)
  2256. toolbar.grid(row=0, column=0, sticky="ew", padx=2, pady=2)
  2257.  
  2258. tk.Label(toolbar, text="Folder:").pack(side=tk.LEFT, padx=(5, 2))
  2259. folder_entry = tk.Entry(toolbar, textvariable=self.current_folder, state="readonly", width=50)
  2260. folder_entry.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
  2261. tk.Button(toolbar, text="Browse...", command=self.browse_folder).pack(side=tk.LEFT, padx=2)
  2262. tk.Button(toolbar, text="Refresh", command=self.refresh_folder).pack(side=tk.LEFT, padx=2)
  2263.  
  2264. main_container = tk.PanedWindow(self, orient=tk.HORIZONTAL, sashrelief=tk.RAISED)
  2265. main_container.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
  2266.  
  2267. left_frame = tk.Frame(main_container)
  2268. main_container.add(left_frame, minsize=300)
  2269.  
  2270. tk.Label(left_frame, text="Files:", font=("Arial", 10, "bold")).pack(anchor="w", pady=(0, 5))
  2271. list_frame = tk.Frame(left_frame)
  2272. list_frame.pack(fill=tk.BOTH, expand=True)
  2273. list_scroll_frame = tk.Frame(list_frame)
  2274. list_scroll_frame.pack(fill=tk.BOTH, expand=True)
  2275.  
  2276. self.file_listbox = tk.Listbox(list_scroll_frame, selectmode=tk.SINGLE)
  2277. list_scrollbar = tk.Scrollbar(list_scroll_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
  2278. self.file_listbox.config(yscrollcommand=list_scrollbar.set)
  2279. self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
  2280. list_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
  2281.  
  2282. self.file_listbox.bind('<<ListboxSelect>>', self.on_file_select)
  2283. self.file_listbox.bind('<Double-Button-1>', self.on_file_double_click)
  2284. self.file_listbox.bind('<Up>', lambda e: self.on_key_press(-1))
  2285. self.file_listbox.bind('<Down>', lambda e: self.on_key_press(1))
  2286.  
  2287. info_frame = tk.Frame(left_frame)
  2288. info_frame.pack(fill=tk.X, pady=(10, 0))
  2289. self.file_info_text = tk.Text(info_frame, height=4, state=tk.DISABLED, wrap=tk.WORD)
  2290. info_scroll = tk.Scrollbar(info_frame, orient=tk.VERTICAL, command=self.file_info_text.yview)
  2291. self.file_info_text.config(yscrollcommand=info_scroll.set)
  2292. self.file_info_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
  2293. info_scroll.pack(side=tk.RIGHT, fill=tk.Y)
  2294.  
  2295. right_container = tk.PanedWindow(main_container, orient=tk.VERTICAL, sashrelief=tk.RAISED)
  2296. main_container.add(right_container, minsize=400)
  2297.  
  2298. preview_frame = tk.Frame(right_container)
  2299. right_container.add(preview_frame, minsize=200)
  2300. tk.Label(preview_frame, text="Preview:", font=("Arial", 10, "bold")).pack(anchor="w", pady=(0, 5))
  2301. preview_container = tk.Frame(preview_frame)
  2302. preview_container.pack(fill=tk.BOTH, expand=True)
  2303.  
  2304. self.preview_canvas = tk.Canvas(preview_container, bg="white", width=300, height=200)
  2305. preview_h_scroll = tk.Scrollbar(preview_container, orient=tk.HORIZONTAL, command=self.preview_canvas.xview)
  2306. preview_v_scroll = tk.Scrollbar(preview_container, orient=tk.VERTICAL, command=self.preview_canvas.yview)
  2307. self.preview_canvas.config(xscrollcommand=preview_h_scroll.set, yscrollcommand=preview_v_scroll.set)
  2308. self.preview_canvas.grid(row=0, column=0, sticky="nsew")
  2309. preview_h_scroll.grid(row=1, column=0, sticky="ew")
  2310. preview_v_scroll.grid(row=0, column=1, sticky="ns")
  2311. preview_container.grid_rowconfigure(0, weight=1)
  2312. preview_container.grid_columnconfigure(0, weight=1)
  2313.  
  2314. comment_frame = tk.Frame(right_container)
  2315. right_container.add(comment_frame, minsize=200)
  2316. file_display_frame = tk.Frame(comment_frame)
  2317. file_display_frame.pack(fill=tk.X, pady=(0, 5))
  2318. tk.Label(file_display_frame, text="Current File:", font=("Arial", 10, "bold")).pack(side=tk.LEFT)
  2319. self.current_file_label = tk.Label(file_display_frame, text="No file selected", fg="blue", cursor="hand2")
  2320. self.current_file_label.pack(side=tk.LEFT, padx=(10, 0))
  2321. self.current_file_label.bind("<Button-1>", self.open_file_location)
  2322.  
  2323. tk.Label(comment_frame, text="Comment:", font=("Arial", 10, "bold")).pack(anchor="w", pady=(5, 2))
  2324.  
  2325. # --- MODIFIED: Enabled undo feature on the text widget ---
  2326. self.txt_comment = scrolledtext.ScrolledText(comment_frame, wrap=tk.WORD, height=8, undo=True)
  2327. self.txt_comment.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  2328. self.txt_comment.bind("<KeyRelease>", self.on_comment_modified)
  2329.  
  2330. button_frame = tk.Frame(comment_frame)
  2331. button_frame.pack(fill=tk.X)
  2332.  
  2333. tk.Button(button_frame, text="Save Comment", command=self.save_metadata, bg="#4CAF50", fg="white", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 5))
  2334. tk.Button(button_frame, text="Clear", command=self.clear_comment).pack(side=tk.LEFT, padx=2)
  2335. tk.Button(button_frame, text="Copy", command=self.copy_comment).pack(side=tk.LEFT, padx=2)
  2336. tk.Button(button_frame, text="Paste", command=self.paste_comment).pack(side=tk.LEFT, padx=2)
  2337. # --- NEW: Undo/Redo Buttons ---
  2338. tk.Button(button_frame, text="Undo", command=self.undo_comment).pack(side=tk.LEFT, padx=2)
  2339. tk.Button(button_frame, text="Redo", command=self.redo_comment).pack(side=tk.LEFT, padx=2)
  2340.  
  2341. self.progress_var = tk.DoubleVar()
  2342. self.progress_bar = ttk.Progressbar(comment_frame, variable=self.progress_var, maximum=100, length=300)
  2343. self.progress_label = tk.Label(comment_frame, text="")
  2344.  
  2345. status_frame = tk.Frame(self, relief=tk.SUNKEN, bd=1)
  2346. status_frame.grid(row=2, column=0, sticky="ew")
  2347. self.status_label = tk.Label(status_frame, text="Ready", anchor="w")
  2348. self.status_label.pack(side=tk.LEFT, padx=5)
  2349. self.file_count_label = tk.Label(status_frame, text="No files loaded", anchor="e")
  2350. self.file_count_label.pack(side=tk.RIGHT, padx=5)
  2351.  
  2352. # --- NEW: Helper function to set text and reset undo history ---
  2353. def _set_comment_text(self, text):
  2354. """Sets text in the comment box and resets the undo/redo stack."""
  2355. self.txt_comment.delete(1.0, tk.END)
  2356. self.txt_comment.insert(1.0, text)
  2357. self.txt_comment.edit_reset()
  2358.  
  2359. def update_status(self, message):
  2360. self.status_label.config(text=message)
  2361. self.update_idletasks()
  2362.  
  2363. def show_progress(self, show=True):
  2364. if show:
  2365. self.progress_bar.pack(fill=tk.X, pady=(10, 5))
  2366. self.progress_label.pack()
  2367. else:
  2368. self.progress_bar.pack_forget()
  2369. self.progress_label.pack_forget()
  2370.  
  2371. def update_progress(self, value, text=""):
  2372. self.progress_var.set(value)
  2373. self.progress_label.config(text=text)
  2374. self.update_idletasks()
  2375.  
  2376. def browse_folder(self):
  2377. folder_path = filedialog.askdirectory()
  2378. if folder_path: self.load_folder(folder_path)
  2379.  
  2380. def load_folder(self, folder_path, select_file=None):
  2381. """Load all supported files from the folder with a numbered list."""
  2382. try:
  2383. self.update_status("Loading folder...")
  2384. self.current_folder.set(folder_path)
  2385.  
  2386. # Clear current data
  2387. self.file_listbox.delete(0, tk.END)
  2388. self.file_list.clear()
  2389. self.thumbnails.clear()
  2390. self.modified_comments.clear()
  2391.  
  2392. # Get all supported files
  2393. folder = Path(folder_path)
  2394. all_files = [p for p in folder.iterdir() if p.is_file() and p.suffix.lower() in self.supported_extensions]
  2395.  
  2396. # Sort files naturally
  2397. all_files.sort(key=lambda x: x.name.lower())
  2398.  
  2399. # Add to list with numbers
  2400. for i, file_path in enumerate(all_files):
  2401. self.file_list.append(str(file_path))
  2402. # --- MODIFIED: Prepend number to the listbox entry ---
  2403. self.file_listbox.insert(tk.END, f"{i + 1}. {file_path.name}")
  2404.  
  2405. # Update file count
  2406. self.file_count_label.config(text=f"{len(self.file_list)} files loaded")
  2407.  
  2408. # Select specific file if requested
  2409. if select_file and select_file in self.file_list:
  2410. index = self.file_list.index(select_file)
  2411. self.file_listbox.selection_set(index)
  2412. self.file_listbox.see(index)
  2413. self.on_file_select(None)
  2414. elif self.file_list:
  2415. # Select first file by default
  2416. self.file_listbox.selection_set(0)
  2417. self.on_file_select(None)
  2418.  
  2419. self.update_status("Folder loaded successfully")
  2420.  
  2421. except Exception as e:
  2422. messagebox.showerror("Error", f"Failed to load folder: {str(e)}")
  2423. self.update_status("Error loading folder")
  2424.  
  2425. def refresh_folder(self):
  2426. folder_path = self.current_folder.get()
  2427. if folder_path and os.path.exists(folder_path):
  2428. self.load_folder(folder_path, select_file=self.get_selected_file())
  2429.  
  2430. def on_file_select(self, event):
  2431. selection = self.file_listbox.curselection()
  2432. if selection:
  2433. index = selection[0]
  2434. if index == self.selected_file_index: return
  2435.  
  2436. self.selected_file_index = index
  2437. file_path = self.file_list[index]
  2438. self.current_file.set(file_path)
  2439. self.current_file_label.config(text=os.path.basename(file_path))
  2440.  
  2441. threading.Thread(target=self.load_file_data, args=(file_path,), daemon=True).start()
  2442.  
  2443. def on_file_double_click(self, event):
  2444. file_path = self.get_selected_file()
  2445. if file_path:
  2446. try: os.startfile(file_path)
  2447. except Exception as e: messagebox.showerror("Error", f"Failed to open file: {str(e)}")
  2448.  
  2449. def on_key_press(self, direction):
  2450. selection = self.file_listbox.curselection()
  2451. if not selection: return
  2452. current_index = selection[0]
  2453. new_index = current_index + direction
  2454. if 0 <= new_index < self.file_listbox.size():
  2455. self.file_listbox.selection_clear(current_index)
  2456. self.file_listbox.selection_set(new_index)
  2457. self.file_listbox.see(new_index)
  2458. self.on_file_select(None)
  2459.  
  2460. def get_selected_file(self):
  2461. return self.current_file.get() or None
  2462.  
  2463. def load_file_data(self, file_path):
  2464. try:
  2465. self.after(0, lambda: self.update_file_info(file_path))
  2466. self.after(0, lambda: self.load_preview(file_path))
  2467.  
  2468. # Use the new helper to load text and reset undo history
  2469. if file_path in self.modified_comments:
  2470. comment_to_load = self.modified_comments[file_path]
  2471. self.after(0, self._set_comment_text, comment_to_load)
  2472. else:
  2473. self.after(0, lambda: self.read_metadata(file_path))
  2474. except Exception as e:
  2475. self.after(0, lambda: self.update_status(f"Error loading file data: {str(e)}"))
  2476.  
  2477. def update_file_info(self, file_path):
  2478. try:
  2479. stat = os.stat(file_path)
  2480. size = stat.st_size
  2481. if size < 1024: size_str = f"{size} B"
  2482. elif size < 1024**2: size_str = f"{size/1024:.1f} KB"
  2483. else: size_str = f"{size/1024**2:.1f} MB"
  2484.  
  2485. dimensions = ""
  2486. try:
  2487. with Image.open(file_path) as img:
  2488. dimensions = f"\nDimensions: {img.width} x {img.height}"
  2489. except: pass
  2490.  
  2491. info_text = f"Size: {size_str}{dimensions}\nPath: {file_path}"
  2492.  
  2493. self.file_info_text.config(state=tk.NORMAL)
  2494. self.file_info_text.delete(1.0, tk.END)
  2495. self.file_info_text.insert(1.0, info_text)
  2496. self.file_info_text.config(state=tk.DISABLED)
  2497. except Exception as e:
  2498. # Handle error
  2499. pass
  2500.  
  2501. def load_preview(self, file_path):
  2502. try:
  2503. self.preview_canvas.delete("all")
  2504. if not any(file_path.lower().endswith(ext) for ext in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']):
  2505. self.preview_canvas.create_text(150, 100, text="Preview not available", fill="gray", font=("Arial", 12))
  2506. return
  2507.  
  2508. with Image.open(file_path) as img:
  2509. canvas_width = self.preview_canvas.winfo_width() or 300
  2510. canvas_height = self.preview_canvas.winfo_height() or 200
  2511.  
  2512. img_ratio = img.width / img.height
  2513. canvas_ratio = canvas_width / canvas_height
  2514.  
  2515. if img_ratio > canvas_ratio:
  2516. new_width = min(canvas_width - 20, img.width)
  2517. new_height = int(new_width / img_ratio)
  2518. else:
  2519. new_height = min(canvas_height - 20, img.height)
  2520. new_width = int(new_height * img_ratio)
  2521.  
  2522. img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
  2523. photo = ImageTk.PhotoImage(img_resized)
  2524.  
  2525. self.preview_canvas.config(scrollregion=(0, 0, new_width, new_height))
  2526. self.preview_canvas.create_image(new_width//2, new_height//2, image=photo, anchor="center")
  2527. self.preview_canvas.image = photo
  2528. except Exception as e:
  2529. self.preview_canvas.create_text(150, 100, text=f"Preview error:\n{e}", fill="red")
  2530.  
  2531. def read_metadata(self, file_path=None):
  2532. if not file_path: file_path = self.get_selected_file()
  2533. if not file_path or not os.path.exists(file_path): return
  2534.  
  2535. comment, source = self._read_comment_data(file_path)
  2536. display_text = f"[Found in: {source}]\n\n{comment}" if comment else "No comment found."
  2537. self._set_comment_text(display_text) # Use helper to load text
  2538.  
  2539. def _read_comment_data(self, file_path):
  2540. found_comment, source_tag = "", ""
  2541. if HAS_PIEXIF:
  2542. try:
  2543. exif_dict = piexif.load(file_path)
  2544. comment_bytes = exif_dict["0th"].get(piexif.ImageIFD.ImageDescription, b'')
  2545. if comment_bytes:
  2546. found_comment = comment_bytes.decode('utf-8', errors='ignore')
  2547. source_tag = "EXIF ImageDescription"
  2548. except: pass
  2549.  
  2550. if not found_comment:
  2551. try:
  2552. result = self.run_exiftool(["-Comment", "-s3", file_path])
  2553. if result.returncode == 0 and result.stdout.strip():
  2554. found_comment, source_tag = result.stdout.strip(), "Windows Comment"
  2555. except: pass
  2556.  
  2557. if not found_comment:
  2558. for tag in ['ImageDescription', 'UserComment', 'Description']:
  2559. try:
  2560. result = self.run_exiftool([f"-{tag}", "-s3", file_path])
  2561. if result.returncode == 0 and result.stdout.strip():
  2562. found_comment, source_tag = result.stdout.strip(), f"{tag} (exiftool)"
  2563. break
  2564. except: continue
  2565. return found_comment, source_tag
  2566.  
  2567. def _clean_comment_text(self, text):
  2568. if text.startswith("[Found in:"):
  2569. lines = text.split('\n', 2)
  2570. return lines[2] if len(lines) > 2 else ""
  2571. return text
  2572.  
  2573. def save_metadata(self):
  2574. file_path = self.get_selected_file()
  2575. if not file_path:
  2576. messagebox.showerror("Error", "No file selected.")
  2577. return
  2578.  
  2579. new_comment = self._clean_comment_text(self.txt_comment.get("1.0", tk.END).strip())
  2580.  
  2581. if self.write_comment_to_file(file_path, new_comment):
  2582. messagebox.showinfo("Success", "Comment saved! ✅")
  2583. self._mark_file_as_saved(file_path)
  2584. self.after(100, lambda: self.read_metadata(file_path))
  2585. else:
  2586. messagebox.showerror("Error", "Failed to write comment.")
  2587.  
  2588. def write_comment_to_file(self, file_path, comment):
  2589. success_count = 0
  2590. if HAS_PIEXIF:
  2591. try:
  2592. exif_dict = piexif.load(file_path)
  2593. exif_dict["0th"][piexif.ImageIFD.ImageDescription] = comment.encode('utf-8')
  2594. exif_bytes = piexif.dump(exif_dict)
  2595. image = Image.open(file_path)
  2596. image.save(file_path, exif=exif_bytes)
  2597. success_count += 1
  2598. except Exception as e: print(f"piexif error: {e}")
  2599.  
  2600. try:
  2601. result = self.run_exiftool([f"-Comment={comment}", "-overwrite_original", file_path])
  2602. if result.returncode == 0: success_count += 1
  2603. else: print(f"exiftool error: {result.stderr}")
  2604. except Exception as e: print(f"exiftool error: {e}")
  2605. return success_count > 0
  2606.  
  2607. def save_all_comments(self):
  2608. modified_files = list(self.modified_comments.keys())
  2609. if not modified_files:
  2610. messagebox.showinfo("Info", "No comments have been modified.")
  2611. return
  2612.  
  2613. msg = f"Save changes to {len(modified_files)} files?"
  2614. if not messagebox.askyesno("Save All Changes", msg): return
  2615.  
  2616. self.show_progress(True)
  2617. success_count, total_files = 0, len(modified_files)
  2618.  
  2619. for i, file_path in enumerate(modified_files):
  2620. comment = self._clean_comment_text(self.modified_comments[file_path])
  2621. self.update_progress((i / total_files) * 100, f"Saving {os.path.basename(file_path)}...")
  2622.  
  2623. if self.write_comment_to_file(file_path, comment):
  2624. success_count += 1
  2625. self.after(0, self._mark_file_as_saved, file_path)
  2626.  
  2627. self.show_progress(False)
  2628. messagebox.showinfo("Batch Save Complete", f"Successfully saved {success_count}/{total_files} files.")
  2629. self.update_status(f"Saved {success_count} files.")
  2630.  
  2631. def copy_comment(self):
  2632. try:
  2633. comment = self._clean_comment_text(self.txt_comment.get("1.0", tk.END).strip())
  2634. self.clipboard_clear()
  2635. self.clipboard_append(comment)
  2636. self.update_status("Comment copied")
  2637. except: pass
  2638.  
  2639. def paste_comment(self):
  2640. try:
  2641. self._set_comment_text(self.clipboard_get())
  2642. self.on_comment_modified()
  2643. self.update_status("Comment pasted")
  2644. except: pass
  2645.  
  2646. def clear_comment(self):
  2647. self._set_comment_text("")
  2648. self.on_comment_modified()
  2649.  
  2650. # --- NEW: Undo/Redo command handlers ---
  2651. def undo_comment(self, event=None):
  2652. try:
  2653. self.txt_comment.edit_undo()
  2654. except tk.TclError:
  2655. pass # Stack is empty
  2656.  
  2657. def redo_comment(self, event=None):
  2658. try:
  2659. self.txt_comment.edit_redo()
  2660. except tk.TclError:
  2661. pass # Stack is empty
  2662.  
  2663. def on_comment_modified(self, event=None):
  2664. file_path = self.get_selected_file()
  2665. if not file_path: return
  2666.  
  2667. current_comment = self.txt_comment.get("1.0", tk.END).strip()
  2668. self.modified_comments[file_path] = current_comment
  2669.  
  2670. try:
  2671. index = self.file_list.index(file_path)
  2672. current_name = self.file_listbox.get(index)
  2673. if not current_name.endswith(" *"):
  2674. self.file_listbox.delete(index)
  2675. self.file_listbox.insert(index, f"{current_name} *")
  2676. self.file_listbox.selection_set(index)
  2677. self.update_status(f"Modified {os.path.basename(file_path)}")
  2678. except (ValueError, tk.TclError): pass
  2679.  
  2680. def _mark_file_as_saved(self, file_path):
  2681. if file_path in self.modified_comments:
  2682. del self.modified_comments[file_path]
  2683.  
  2684. try:
  2685. index = self.file_list.index(file_path)
  2686. current_name = self.file_listbox.get(index)
  2687. if current_name.endswith(" *"):
  2688. self.file_listbox.delete(index)
  2689. self.file_listbox.insert(index, current_name[:-2])
  2690. if self.selected_file_index == index:
  2691. self.file_listbox.selection_set(index)
  2692. except (ValueError, tk.TclError): pass
  2693.  
  2694. self.update_status(f"Saved {os.path.basename(file_path)}")
  2695.  
  2696. def toggle_thumbnails(self): messagebox.showinfo("Info", "Thumbnail view not yet implemented.")
  2697. def zoom_in_preview(self): messagebox.showinfo("Info", "Zoom in not yet implemented.")
  2698. def zoom_out_preview(self): messagebox.showinfo("Info", "Zoom out not yet implemented.")
  2699.  
  2700. def batch_apply_comment(self):
  2701. if not self.file_list: return
  2702. comment = self._clean_comment_text(self.txt_comment.get("1.0", tk.END).strip())
  2703. if not comment: return
  2704.  
  2705. if not messagebox.askyesno("Batch Apply", f"Apply to all {len(self.file_list)} files?"): return
  2706.  
  2707. self.show_progress(True)
  2708. success_count = 0
  2709. for i, file_path in enumerate(self.file_list):
  2710. self.update_progress((i / len(self.file_list)) * 100, f"Processing {os.path.basename(file_path)}...")
  2711. if self.write_comment_to_file(file_path, comment):
  2712. success_count += 1
  2713. self._mark_file_as_saved(file_path)
  2714.  
  2715. self.show_progress(False)
  2716. messagebox.showinfo("Batch Complete", f"Applied comment to {success_count}/{len(self.file_list)} files.")
  2717.  
  2718. def export_comments(self):
  2719. if not self.file_list: return
  2720. file_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text", "*.txt")], title="Export Comments")
  2721. if not file_path: return
  2722.  
  2723. self.show_progress(True)
  2724. try:
  2725. with open(file_path, 'w', encoding='utf-8') as f:
  2726. f.write(f"# Exported Comments from {self.current_folder.get()}\n\n")
  2727. for i, img_path in enumerate(self.file_list):
  2728. self.update_progress((i / len(self.file_list)) * 100, f"Reading {os.path.basename(img_path)}...")
  2729. comment, _ = self._read_comment_data(img_path)
  2730. f.write(f"File: {os.path.basename(img_path)}\nComment: {comment or '(none)'}\n---\n")
  2731. self.show_progress(False)
  2732. messagebox.showinfo("Success", "Comments exported.")
  2733. except Exception as e:
  2734. self.show_progress(False)
  2735. messagebox.showerror("Error", f"Failed to export: {e}")
  2736.  
  2737. def import_comments(self): messagebox.showinfo("Info", "Import not yet implemented.")
  2738.  
  2739. def open_file_location(self, event):
  2740. file_path = self.get_selected_file()
  2741. if file_path and os.path.exists(file_path):
  2742. subprocess.run(['explorer', '/select,', file_path])
  2743.  
  2744. def run_exiftool(self, args):
  2745. command = [self.exiftool_path] + args
  2746. try:
  2747. return subprocess.run(
  2748. command, capture_output=True, text=True, encoding='utf-8', check=False,
  2749. startupinfo=subprocess.STARTUPINFO(wShowWindow=subprocess.SW_HIDE),
  2750. creationflags=subprocess.CREATE_NO_WINDOW)
  2751. except Exception as e:
  2752. class MockResult:
  2753. returncode, stdout, stderr = 1, "", str(e)
  2754. return MockResult()
  2755.  
  2756. def install_context_menu(self):
  2757. if not is_admin():
  2758. ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, f'"{__file__}" --install', None, 1)
  2759. return
  2760. try:
  2761. script_path = os.path.abspath(__file__)
  2762. python_exe = sys.executable.replace('python.exe', 'pythonw.exe')
  2763. if not os.path.exists(python_exe): python_exe = sys.executable
  2764. command = f'"{python_exe}" "{script_path}" "%1"'
  2765.  
  2766. key_path = r'*\shell\EditExifCommentGUI_Advanced'
  2767. with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, key_path) as key:
  2768. winreg.SetValue(key, '', winreg.REG_SZ, 'Edit Comment (Advanced) 📝')
  2769. with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, f'{key_path}\\command') as key:
  2770. winreg.SetValue(key, '', winreg.REG_SZ, command)
  2771.  
  2772. folder_key_path = r'Directory\shell\EditExifCommentGUI_Advanced'
  2773. with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, folder_key_path) as key:
  2774. winreg.SetValue(key, '', winreg.REG_SZ, 'Edit Comments (Advanced) 📁')
  2775. with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, f'{folder_key_path}\\command') as key:
  2776. winreg.SetValue(key, '', winreg.REG_SZ, command)
  2777. messagebox.showinfo("Success", "Context menu added!")
  2778. except Exception as e: messagebox.showerror("Error", f"Failed to add context menu:\n{e}")
  2779.  
  2780. def uninstall_context_menu(self):
  2781. if not is_admin():
  2782. ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, f'"{__file__}" --uninstall', None, 1)
  2783. return
  2784. try:
  2785. for key_path in [r'*\shell\EditExifCommentGUI_Advanced', r'Directory\shell\EditExifCommentGUI_Advanced']:
  2786. try:
  2787. winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, f'{key_path}\\command')
  2788. winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, key_path)
  2789. except FileNotFoundError: pass
  2790. messagebox.showinfo("Success", "Context menu removed!")
  2791. except Exception as e: messagebox.showerror("Error", f"Failed to remove context menu:\n{e}")
  2792.  
  2793. def is_admin():
  2794. try: return ctypes.windll.shell32.IsUserAnAdmin()
  2795. except: return False
  2796.  
  2797. if __name__ == "__main__":
  2798. try:
  2799. if len(sys.argv) > 1:
  2800. if sys.argv[1] == '--install' and is_admin():
  2801. AdvancedExifToolGUI().install_context_menu()
  2802. sys.exit(0)
  2803. elif sys.argv[1] == '--uninstall' and is_admin():
  2804. AdvancedExifToolGUI().uninstall_context_menu()
  2805. sys.exit(0)
  2806.  
  2807. app = AdvancedExifToolGUI()
  2808. if app.winfo_exists(): app.mainloop()
  2809. except Exception as e:
  2810. # Fallback error message box
  2811. error_log_path = os.path.join(os.path.expanduser("~"), "exif_gui_error.log")
  2812. with open(error_log_path, "w") as f:
  2813. f.write(f"Error: {e}\nTraceback: {traceback.format_exc()}\n")
  2814. messagebox.showerror("Fatal Error", f"A critical error occurred. See log for details:\n{error_log_path}")
  2815. --------------------------------------------------------------------------------
  2816. --- END: fixed_exiftool_gui.py ---
  2817.  
  2818.  
  2819. --- START: merged_output.txt ---
  2820. --------------------------------------------------------------------------------
  2821. --- Merged Files Compilation ---
  2822. Source Directory: C:\Scripts\Random_Scripts\New folder
  2823. Generated on: 2025-08-24 10:29:47
  2824. ================================================================================
  2825.  
  2826.  
  2827. --------------------------------------------------------------------------------
  2828. --- END: merged_output.txt ---
  2829.  
  2830.  
  2831.  
Add Comment
Please, Sign In to add comment