diff --git a/README.md b/README.md
index 5211142..f309537 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,9 @@ Display an image and text file side-by-side for easy manual captioning. + Tons o
- `Append Text Files`: Insert text at the END of all text files.
- `Search and Replace`: Edit all text files at once.
+ - Image Tools
+ -`Resize Images`: Resize to Resolution, Resize to Width, Resize to Height, Resize to Shorter Side, Resize to Longer Side
+
- Auto-Save
- Check the auto-save box to save text when navigating between img/txt pairs or closing the window.
- Text is cleaned when saved, so you can ignore typos such as duplicate tokens, multiple spaces or commas, missing spaces, and more.
@@ -61,13 +64,30 @@ The `pillow` library will be downloaded and installed *(if not already available
# 📜 Version History
-[v1.83 changes:](https://github.com/Nenotriple/img-txt_viewer/releases/tag/v1.83)
+[v1.84 changes:](https://github.com/Nenotriple/img-txt_viewer/releases/tag/v1.84)
+
+ - New:
+ - New tool: `Resize Images`: You can find this in the Tools menu.
+ - Resize operations: Resize to Resolution, Resize to Width, Resize to Height, Resize to Shorter Side, Resize to Longer Side
+ - Just like "batch_tag_delete.py", "resize_images.py" is a stand-alone tool. You can run it 100% independently of img-txt_viewer.
+ - Images will be overwritten when resized.
+ - New option: `Colored Suggestions`, Use this to enable or disable the color coded autocomplete suggestions.
+
+
- Fixed:
- - Fix text box duplicating when selecting a new directory.
- - Fixed some small issues with the file watcher and image index.
+ - Fixed suggestions breaking when typing a parentheses.
- Other changes:
- - Minor code cleanup and internal changes.
+ - Batch Tag Delete is no longer bundled within img-txt_viewer. This allows you to run them separately.
+
+___
+
+### Batch Token Delete
+v1.06 changes:
+
+ - Fixed:
+ - Fixed tag list refreshing twice
+ - Fixed multi-tag delete when "batch_tag_delete" is ran from "img-txt_viewer"
diff --git a/batch_tag_delete.py b/batch_tag_delete.py
index 948bdf8..49973f7 100644
--- a/batch_tag_delete.py
+++ b/batch_tag_delete.py
@@ -3,7 +3,7 @@
# #
# batch_tag_delete #
# #
-# Version : v1.05 #
+# Version : v1.06 #
# Author : github.com/Nenotriple #
# #
########################################
@@ -19,12 +19,14 @@
"""
+
################################################################################################################################################
################################################################################################################################################
# #
# Imports #
# #
+
import os
import sys
import time
@@ -35,25 +37,32 @@
from collections import Counter
from tkinter import messagebox, simpledialog, filedialog, TclError
+from threading import Lock
+
+
################################################################################################################################################
################################################################################################################################################
# #
# Variables #
# #
+
# Used to create a group ID so app shares the parent icon, and groups with the main window in the taskbar.
myappid = 'ImgTxtViewer.Nenotriple'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
+
sort_order = 'count'
stop_thread = False
+
################################################################################################################################################
################################################################################################################################################
# #
# Primary Functions #
# #
+
def display_tags(tag_dict, directory, scrollable_frame, filter_text=''):
if sort_order == 'alpha':
sorted_tag_items = sorted(tag_dict.items(), key=lambda item: item[0])
@@ -84,6 +93,8 @@ def display_tags(tag_dict, directory, scrollable_frame, filter_text=''):
checkbox.bind('', lambda event: toggle_all_checkboxes(event, scrollable_frame))
pair_frame.pack(side=tk.TOP, pady=2)
+
+
def count_tags(directory):
tag_dict = Counter()
for filename in os.listdir(directory):
@@ -96,6 +107,7 @@ def count_tags(directory):
tag_dict[tag.strip()] += 1
return tag_dict
+
def delete_tag(directory, tag, filter_text='', confirm_prompt=True):
parent = tk.Toplevel()
parent.withdraw()
@@ -119,6 +131,7 @@ def delete_tag(directory, tag, filter_text='', confirm_prompt=True):
file.write(new_line)
parent.destroy()
+
def cleanup_text(text):
import re
text = remove_duplicates(text)
@@ -134,6 +147,7 @@ def cleanup_text(text):
text = text.strip() # remove leading and trailing spaces
return text
+
def remove_duplicates(text):
text = text.lower().split(',')
text = [item.strip() for item in text]
@@ -141,15 +155,18 @@ def remove_duplicates(text):
text = ','.join(text)
return text
+
def create_hover_effect(widget, hover_color):
return (lambda event: widget.config(bg=hover_color), lambda event: widget.config(bg="SystemButtonFace"))
+
################################################################################################################################################
################################################################################################################################################
# #
# Batch Delete #
# #
+
def batch_delete(directory, count_threshold, scrollable_frame, max_display_tags=150):
tag_dict = count_tags(directory)
tags_to_delete = [tag for tag, count in tag_dict.items() if count <= count_threshold]
@@ -169,11 +186,13 @@ def batch_delete(directory, count_threshold, scrollable_frame, max_display_tags=
delete_tag(directory, tag, confirm_prompt=False)
display_tags(count_tags(directory), directory, scrollable_frame)
+
def ask_count_threshold(directory, scrollable_frame, root):
count_threshold = simpledialog.askinteger("Delete all less than or equal to", "\tEnter the count threshold\t\t", parent=root)
if count_threshold is not None:
batch_delete(directory, count_threshold, scrollable_frame)
+
def delete_selected_tags(root, directory, scrollable_frame, filter_text=''):
parent = tk.Toplevel()
parent.withdraw()
@@ -196,6 +215,7 @@ def delete_selected_tags(root, directory, scrollable_frame, filter_text=''):
parent.destroy()
display_tags(count_tags(directory), directory, scrollable_frame, filter_text)
+
def toggle_all_checkboxes(event, scrollable_frame):
clicked_checkbox_state = event.widget.var.get()
new_state = not clicked_checkbox_state
@@ -205,12 +225,14 @@ def toggle_all_checkboxes(event, scrollable_frame):
if isinstance(sub_widget, tk.Checkbutton):
sub_widget.var.set(new_state)
+
################################################################################################################################################
################################################################################################################################################
# #
# Sorting #
# #
+
def toggle_tag_order(tag_dict, directory, scrollable_frame, filter_text=''):
global sort_order
if sort_order == 'count':
@@ -219,12 +241,14 @@ def toggle_tag_order(tag_dict, directory, scrollable_frame, filter_text=''):
sort_order = 'count'
display_tags(tag_dict, directory, scrollable_frame, filter_text)
+
def filter_tags(event, directory, scrollable_frame):
filter_text = event.widget.get()
tag_dict = count_tags(directory)
filtered_dict = {k: v for k, v in tag_dict.items() if fuzzy_search(filter_text.lower(), k.lower())}
display_tags(filtered_dict, directory, scrollable_frame, filter_text)
+
def fuzzy_search(str1, str2):
m = len(str1)
n = len(str2)
@@ -236,12 +260,14 @@ def fuzzy_search(str1, str2):
i = i + 1
return j == m
+
################################################################################################################################################
################################################################################################################################################
# #
# Manage Backups #
# #
+
def backup_files(directory):
backup_directory = os.path.join(directory, "text_backup")
if not os.path.exists(backup_directory):
@@ -250,6 +276,7 @@ def backup_files(directory):
if filename.endswith(".txt"):
shutil.copy2(os.path.join(directory, filename), os.path.join(backup_directory, filename))
+
def restore_backup(directory, scrollable_frame):
backup_directory = os.path.join(directory, "text_backup")
for filename in os.listdir(backup_directory):
@@ -257,6 +284,7 @@ def restore_backup(directory, scrollable_frame):
shutil.copy(os.path.join(backup_directory, filename), os.path.join(directory, filename))
display_tags(count_tags(directory), directory, scrollable_frame)
+
def on_closing(directory, root):
global stop_thread
stop_thread = True
@@ -265,12 +293,14 @@ def on_closing(directory, root):
shutil.rmtree(backup_directory)
root.destroy()
+
################################################################################################################################################
################################################################################################################################################
# #
# Main #
# #
+
def main(directory=None, main_window_x=None, main_window_y=None):
# If directory is not provided or is not a valid directory, open a directory dialog
if not directory or not os.path.isdir(directory):
@@ -281,9 +311,10 @@ def main(directory=None, main_window_x=None, main_window_y=None):
if not directory:
return
+
# Initialize root window
root = tk.Tk()
- root.title(f"tag List: {directory}")
+ root.title(f"Tag List: {directory}")
window_width = 490
window_height = 800
@@ -301,13 +332,18 @@ def main(directory=None, main_window_x=None, main_window_y=None):
root.maxsize(490, 2000)
root.focus_force()
+
# Count tags in the directory
tag_dict = count_tags(directory)
+
# Initialize last modification times
last_modification_times = {}
+
# Function to refresh the display
+ display_lock = Lock()
+
def start_watching_files():
global stop_thread
stop_thread = False
@@ -325,9 +361,13 @@ def refresh():
if file not in last_modification_times or mod_time > last_modification_times[file]:
last_modification_times[file] = mod_time
modified = True
- if modified:
- display_tags(count_tags(directory), directory, scrollable_frame)
- time.sleep(2)
+ if modified and display_lock.acquire(False):
+ try:
+ display_tags(count_tags(directory), directory, scrollable_frame)
+ finally:
+ display_lock.release()
+ time.sleep(1)
+
# Function to set the icon
def set_icon():
@@ -341,10 +381,12 @@ def set_icon():
except TclError:
pass
+
# Initialize menubar
menubar = tk.Menu(root)
root.config(menu=menubar)
+
# Add commands to the menubar
menubar.add_command(label="Change Sort", command=lambda: toggle_tag_order(tag_dict, directory, scrollable_frame))
menubar.add_separator()
@@ -354,6 +396,7 @@ def set_icon():
menubar.add_separator()
menubar.add_command(label="Undo All", command=lambda: restore_backup(directory, scrollable_frame))
+
# Initialize filter entry
filter_entry = tk.Entry(root)
filter_entry.insert(0, "Filter tags here (fuzzy search)")
@@ -361,6 +404,7 @@ def set_icon():
filter_entry.pack(side="top", fill="x", ipady=1)
filter_entry.bind("", lambda event: filter_tags(event, directory, scrollable_frame))
+
# Initialize frame, canvas, scrollbar, and scrollable frame
frame = tk.Frame(root)
frame.pack(fill="both", expand=True)
@@ -375,18 +419,22 @@ def set_icon():
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
+
# Display tags, backup files, and set closing protocol
display_tags(tag_dict, directory, scrollable_frame)
backup_files(directory)
root.protocol("WM_DELETE_WINDOW", lambda: on_closing(directory, root))
+
# Refresh display and set icon
start_watching_files()
set_icon()
+
# Start main loop
root.mainloop()
+# How can I launch this as an executable? The name of the executable is "batch_tag_delete.exe"
if __name__ == "__main__":
# Call the main function with command line arguments if provided
# sys.argv[1] is the first command line argument, which is the directory
@@ -396,51 +444,39 @@ def set_icon():
main(sys.argv[1] if len(sys.argv) > 1 else None, sys.argv[2] if len(sys.argv) > 2 else None, sys.argv[3] if len(sys.argv) > 3 else None)
+
################################################################################################################################################
################################################################################################################################################
# #
# Changelog #
# #
+
'''
-v1.05 changes:
+v1.06 changes:
- New:
- - `Undo All` You can now restore the text files to their original state from when Batch Tag Delete was launched. [#7d574a8][7d574a8]
- - Implement Auto-Refresh Feature. [#4f78be5][4f78be5]
- - Renamed to: Batch Tag Delete [#f7e9389][f7e9389]
- - Window position can be controlled with cmd arguments. This is used to position this window beside img-txt_viewer. [#9fe7499][9fe7499]
- - Example: `python batch_tag_delete.py /path/to/directory 500 800`
+ -
- Fixed:
- - Properly set app icon. [#358ee1d][358ee1d]
- - Improved popup handling when clicking `Delete Selected` when no tags are selected. [#3a0a60b][3a0a60b]
- - Fixed error related to file refresh being called after closing when launched from img-txt_viewer.
-
- - Other: [#7ccd0fb][7ccd0fb], [3a0a60b][3a0a60b], [42e01c5][42e01c5]
-
-[7d574a8]: https://github.com/Nenotriple/img-txt_viewer/commit/7d574a85b300f60bd01015aeadfca4e3d38cdf71
-[4f78be5]: https://github.com/Nenotriple/img-txt_viewer/commit/4f78be5df917f6af19796591fbbff05e64f8e944
-[f7e9389]: https://github.com/Nenotriple/img-txt_viewer/commit/f7e9389d77ed86508ccb4f9705c3d709eb00ab0e
-[9fe7499]: https://github.com/Nenotriple/img-txt_viewer/commit/9fe7499d89d5689606a3e576554c03c8c3f4f4c8
+ - Fixed tag list refreshing twice
+ - Fixed Multi-tag delete when "batch_tag_delete" is ran from "img-txt_viewer"
-[358ee1d]: https://github.com/Nenotriple/img-txt_viewer/commit/358ee1d93636d0001a3e9b96d72ba3230697fcdd
-
-[7ccd0fb]: https://github.com/Nenotriple/img-txt_viewer/commit/7ccd0fb7c41a82eb31e128b656b16fbccd78c784
-[3a0a60b]: https://github.com/Nenotriple/img-txt_viewer/commit/3a0a60bbf41a2da0c5b943624bfe61dceba71703
-[42e01c5]: https://github.com/Nenotriple/img-txt_viewer/commit/42e01c591b73fef211ac636fa945da84dfd67d61
+ - Other:
'''
+
################################################################################################################################################
################################################################################################################################################
# #
# todo #
# #
+
'''
- Todo
diff --git a/img-txt_viewer.pyw b/img-txt_viewer.pyw
index 6ac6626..c9670a2 100644
--- a/img-txt_viewer.pyw
+++ b/img-txt_viewer.pyw
@@ -3,6 +3,7 @@
# #
# IMG-TXT VIEWER #
# #
+# Version : v1.84 #
# Author : github.com/Nenotriple #
# #
########################################
@@ -15,14 +16,17 @@ More info here: https://github.com/Nenotriple/img-txt_viewer
"""
+
VERSION = "v1.84"
+
################################################################################################################################################
################################################################################################################################################
# #
#region - Imports #
# #
+
import os
import re
import csv
@@ -41,7 +45,6 @@ from tkinter import ttk, messagebox
from tkinter.filedialog import askdirectory
from tkinter.scrolledtext import ScrolledText
-import batch_tag_delete
##################
# #
@@ -49,6 +52,7 @@ import batch_tag_delete
# #
##################
+
try:
from PIL import Image, ImageTk
except ImportError:
@@ -56,6 +60,7 @@ except ImportError:
import threading
from tkinter import Tk, Label, messagebox
+
def download_pillow():
cmd = ["pythonw", '-m', 'pip', 'install', 'pillow']
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
@@ -69,6 +74,7 @@ except ImportError:
done_label.pack(anchor="w")
root.after(3000, root.destroy)
+
root = Tk()
root.title("Pillow Is Installing...")
root.geometry('600x200')
@@ -76,6 +82,7 @@ except ImportError:
root.withdraw()
root.protocol("WM_DELETE_WINDOW", lambda: None)
+
install_pillow = messagebox.askyesno("Pillow not installed!", "Pillow not found!\npypi.org/project/Pillow\n\nWould you like to install it? ~2.5MB \n\n It's required to view images.")
if install_pillow:
root.deiconify()
@@ -88,6 +95,7 @@ except ImportError:
else:
sys.exit()
+
#endregion
##########################################################################################################################################################################
##########################################################################################################################################################################
@@ -95,6 +103,7 @@ except ImportError:
#region AboutWindow #
# #
+
class AboutWindow(Toplevel):
info_headers = ["Shortcuts:", "Tips:", "Text Tools:", "Auto-Save:"]
info_content = [
@@ -123,6 +132,7 @@ class AboutWindow(Toplevel):
" ⦁Text is cleaned up when saved, so you can ignore things like duplicate tags, trailing comma/spaces, double comma/spaces, etc.\n"
" ⦁Text cleanup can be disabled from the options menu.",]
+
def __init__(self, master=None):
super().__init__(master=master)
self.title("About")
@@ -134,10 +144,12 @@ class AboutWindow(Toplevel):
self.create_info_text()
self.create_made_by_label()
+
def create_url_button(self):
self.url_button = Button(self, text=f"Open: {self.github_url}", fg="blue", overrelief="groove", command=self.open_url)
self.url_button.pack(fill="x")
- ToolTip.create_tooltip(self.url_button, "Click this button to open the repo in your default browser", 10, 6, 4)
+ ToolTip.create(self.url_button, "Click this button to open the repo in your default browser", 10, 6, 4)
+
def create_info_text(self):
self.info_text = ScrolledText(self)
@@ -149,15 +161,18 @@ class AboutWindow(Toplevel):
self.info_text.tag_config("section", font=("Segoe UI", 10))
self.info_text.config(state='disabled', wrap="word")
+
def create_made_by_label(self):
self.made_by_label = Label(self, text=f"{VERSION} img-txt_viewer - Created by: Nenotriple (2023)", font=("Arial", 10))
self.made_by_label.pack(pady=5)
- ToolTip.create_tooltip(self.made_by_label, "🤍Thank you for using my app!🤍 (^‿^)", 10, 6, 4)
+ ToolTip.create(self.made_by_label, "🤍Thank you for using my app!🤍 (^‿^)", 10, 6, 4)
+
def open_url(self):
import webbrowser
webbrowser.open(f"{self.github_url}")
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -165,6 +180,9 @@ class AboutWindow(Toplevel):
#region - ToolTips #
# #
+''' Example ToolTip: ToolTip.create(WIDGET, "TOOLTIP TEXT", delay=0, x_offset=0, y_offset=0) '''
+
+
class ToolTip:
def __init__(self, widget, x_offset=0, y_offset=0):
self.widget = widget
@@ -172,6 +190,7 @@ class ToolTip:
self.x_offset = x_offset
self.y_offset = y_offset
self.id = None
+ self.hide_id = None
self.hide_time = 0
def show_tip(self, tip_text, x, y):
@@ -186,7 +205,7 @@ class ToolTip:
tw.wm_attributes("-disabled", True)
label = Label(tw, text=tip_text, background="#ffffee", relief="ridge", borderwidth=1, justify="left", padx=4, pady=4)
label.pack()
- self.id = self.widget.after(3000, self.hide_tip)
+ self.hide_id = self.widget.after(5000, self.hide_tip)
def hide_tip(self):
tw = self.tip_window
@@ -196,7 +215,7 @@ class ToolTip:
self.hide_time = time.time()
@staticmethod
- def create_tooltip(widget, text, delay=0, x_offset=0, y_offset=0):
+ def create(widget, text, delay=0, x_offset=0, y_offset=0):
tool_tip = ToolTip(widget, x_offset, y_offset)
def enter(event):
if tool_tip.id:
@@ -207,8 +226,14 @@ class ToolTip:
if tool_tip.id:
widget.after_cancel(tool_tip.id)
tool_tip.hide_tip()
+ def motion(event):
+ if tool_tip.id:
+ widget.after_cancel(tool_tip.id)
+ tool_tip.id = widget.after(delay, lambda: tool_tip.show_tip(text, widget.winfo_pointerx(), widget.winfo_pointery()))
widget.bind('', enter)
widget.bind('', leave)
+ widget.bind('', motion)
+
#endregion
################################################################################################################################################
@@ -217,6 +242,7 @@ class ToolTip:
#region - Autocomplete #
# #
+
class Autocomplete:
def __init__(self, data_file, max_suggestions=4):
self.data = self.load_data(data_file)
@@ -224,6 +250,7 @@ class Autocomplete:
self.previous_text = None
self.previous_suggestions = None
+
def download_data(self):
files = {'danbooru.csv': "https://raw.githubusercontent.com/Nenotriple/img-txt_viewer/main/danbooru.csv",
'dictionary.csv': "https://raw.githubusercontent.com/Nenotriple/img-txt_viewer/main/dictionary.csv",
@@ -236,11 +263,13 @@ class Autocomplete:
url = files[data_file]
threading.Thread(target=self.download_file, args=(url, data_file)).start()
+
def download_file(self, url, data_file):
response = requests.get(url)
with open(data_file, 'wb') as f:
f.write(response.content)
+
def load_data(self, data_file, additional_file='my_tags.csv'):
if getattr(sys, 'frozen', False):
application_path = sys._MEIPASS
@@ -274,6 +303,7 @@ class Autocomplete:
data[true_name] = ('', similar_names)
return data
+
def autocomplete(self, text):
if not hasattr(self, 'data') or not self.data:
return None
@@ -299,6 +329,7 @@ class Autocomplete:
self.previous_suggestions = suggestions
return suggestions[:self.max_suggestions]
+
def get_score(self, suggestion, text):
score = 0
if suggestion == text:
@@ -313,6 +344,7 @@ class Autocomplete:
score += 1
return score
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -322,15 +354,18 @@ class Autocomplete:
#region - Main Class #
# #
+
class ImgTxtViewer:
def __init__(self, master):
self.master = master
+
# Window settings
self.set_appid()
self.set_window_size(master)
self.set_icon()
+
# Variables
self.stop_thread = False
self.about_window = None
@@ -339,17 +374,20 @@ class ImgTxtViewer:
self.watching_files = False
self.is_alt_arrow_pressed = False
+
# Navigation variables
self.selected_suggestion_index = 0
self.prev_num_files = 0
self.current_index = 0
+
# Text tool strings
self.search_string_var = StringVar()
self.replace_string_var = StringVar()
self.prefix_string_var = StringVar()
self.append_string_var = StringVar()
+
# File lists
self.text_files = []
self.image_files = []
@@ -357,6 +395,7 @@ class ImgTxtViewer:
self.deleted_pairs = []
self.new_text_files = []
+
# Settings
self.font_var = StringVar()
self.max_img_width = IntVar(value=2500)
@@ -366,14 +405,17 @@ class ImgTxtViewer:
self.bold_commas = BooleanVar(value=False)
self.cleaning_text = BooleanVar(value=True)
self.auto_save_var = BooleanVar(value=False)
+ self.use_colored_suggestions = BooleanVar(value=True)
self.highlighting_duplicates = BooleanVar(value=True)
self.highlighting_all_duplicates = BooleanVar(value=False)
+
# Autocomplete settings
self.autocomplete = Autocomplete("danbooru.csv")
self.csv_var = StringVar(value='danbooru.csv')
self.suggestion_quantity = IntVar(value=4)
+
# Bindings
master.bind("", lambda event: self.toggle_highlight_all_duplicates())
master.bind("", lambda event: self.save_text_file())
@@ -389,14 +431,17 @@ class ImgTxtViewer:
#region - Menubar #
# #
+
####### Initilize Menu Bar ############################################
menubar = Menu(self.master)
self.master.config(menu=menubar)
+
####### Options Menu ##################################################
self.optionsMenu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Options", underline=0, menu=self.optionsMenu)
+
# Suggestion Dictionary Menu
dictionaryMenu = Menu(self.optionsMenu, tearoff=0)
self.optionsMenu.add_cascade(label="Suggestion Dictionary", underline=11, state="disable", menu=dictionaryMenu)
@@ -406,6 +451,7 @@ class ImgTxtViewer:
dictionaryMenu.add_separator()
dictionaryMenu.add_checkbutton(label="All (slow)", underline=0, variable=self.csv_var, onvalue='all', command=self.change_autocomplete_dictionary)
+
# Suggestion Quantity Menu
suggestion_quantity_menu = Menu(self.optionsMenu, tearoff=0)
self.optionsMenu.add_cascade(label="Suggestion Quantity", underline=11, state="disable", menu=suggestion_quantity_menu)
@@ -413,18 +459,21 @@ class ImgTxtViewer:
suggestion_quantity_menu.add_radiobutton(label=str(i), variable=self.suggestion_quantity, value=i, command=lambda suggestion_quantity=i: self.set_suggestion_quantity(suggestion_quantity))
self.optionsMenu.add_separator()
+
# Max Image Size Menu
sizeMenu = Menu(self.optionsMenu, tearoff=0)
self.optionsMenu.add_cascade(label="Max Image Size", underline=0, state="disable", menu=sizeMenu)
- self.sizes = [("Smaller", 512),
+ self.sizes = [("Small", 512),
("Normal", 2500),
("Larger", 4000)]
for size in self.sizes:
sizeMenu.add_radiobutton(label=size[0], variable=self.max_img_width, value=size[1], underline=0, command=lambda s=size: self.save_text_file())
+# Please alter "Use Colored Suggestion" so it also runs "change_autocomplete_dictionary"
# Options
self.optionsMenu.add_checkbutton(label="Highlighting Duplicates", underline=0, state="disable", variable=self.highlighting_duplicates)
self.optionsMenu.add_checkbutton(label="Cleaning Text on Save", underline=0, state="disable", variable=self.cleaning_text)
+ self.optionsMenu.add_checkbutton(label="Colored Suggestions", underline=0, state="disable", variable=self.use_colored_suggestions, command=self.change_autocomplete_dictionary)
self.optionsMenu.add_checkbutton(label="Big Comma Mode", underline=0, state="disable", variable=self.bold_commas, command=self.toggle_big_comma_mode)
self.optionsMenu.add_checkbutton(label="List View", underline=0, state="disable", variable=self.list_mode, command=self.toggle_list_mode)
self.optionsMenu.add_separator()
@@ -432,12 +481,15 @@ class ImgTxtViewer:
self.optionsMenu.add_checkbutton(label="Vertical View", underline=0, state="disable", command=self.swap_pane_orientation)
self.optionsMenu.add_command(label="Swap img-txt Sides", underline=0, state="disable", command=self.swap_pane_sides)
+
####### Tools Menu ##################################################
self.toolsMenu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Tools", underline=0, menu=self.toolsMenu)
+
# Tools
- self.toolsMenu.add_command(label="Batch Tag Delete", underline=0, command=self.batch_tag_delete)
+ self.toolsMenu.add_command(label="Batch Tag Delete...", underline=0, command=self.batch_tag_delete)
+ self.toolsMenu.add_command(label="Resize Images...", underline=0, command=self.resize_images)
self.toolsMenu.add_separator()
self.toolsMenu.add_command(label="Cleanup Text", underline=0, state="disable", command=self.cleanup_all_text_files)
self.toolsMenu.add_separator()
@@ -447,9 +499,11 @@ class ImgTxtViewer:
self.toolsMenu.add_command(label="Delete img-txt Pair", accelerator="Del", state="disable", command=self.delete_pair)
self.toolsMenu.add_command(label="Undo Delete", underline=0, command=self.undo_delete_pair, state="disabled")
+
####### About Menu ##################################################
menubar.add_command(label="About", underline=0, command=self.toggle_about_window)
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -457,21 +511,25 @@ class ImgTxtViewer:
#region - Buttons, Labels, and more #
# #
+
# This PanedWindow holds both master frames.
self.primary_paned_window = PanedWindow(master, orient="horizontal", sashwidth=5, bg="#d0d0d0", bd=0)
self.primary_paned_window.pack(fill="both", expand=1)
self.primary_paned_window.bind('', self.snap_sash_to_half)
+
# This frame is exclusively used for the displayed image.
self.master_image_frame = Frame(master)
self.primary_paned_window.add(self.master_image_frame, stretch="always")
+
# This frame serves as a container for all primary UI frames, with the exception of the master_image_frame.
self.master_control_frame = Frame(master)
self.primary_paned_window.add(self.master_control_frame, stretch="always", )
self.primary_paned_window.paneconfigure(self.master_control_frame, minsize=300)
self.primary_paned_window.update(); self.primary_paned_window.sash_place(0, 0, 0)
+
# Image Label
self.image_preview = Button(self.master_image_frame, relief="flat")
self.image_preview.pack(side="left")
@@ -479,7 +537,8 @@ class ImgTxtViewer:
self.image_preview.bind('', self.open_current_directory)
self.image_preview.bind("", self.mouse_scroll)
self.image_preview.bind("", self.show_imageContext_menu)
- ToolTip.create_tooltip(self.image_preview, "Double-Click to open in system image viewer \n\nRight-click / Middle-click to open in file explorer\n\nALT+Left/Right, Mouse-Wheel to move between img-txt pairs", 1000, 6, 4)
+ ToolTip.create(self.image_preview, "Double-Click to open in system image viewer \n\nRight-click / Middle-click to open in file explorer\n\nALT+Left/Right, Mouse-Wheel to move between img-txt pairs", 1000, 6, 4)
+
# Directory Button
top_button_frame = Frame(self.master_control_frame)
@@ -488,12 +547,14 @@ class ImgTxtViewer:
self.directory_button.pack(side="top", fill="x")
self.directory_button.bind('', self.open_current_directory)
self.directory_button.bind('', self.copy_to_clipboard)
- ToolTip.create_tooltip(self.directory_button, "Right click to copy path\n\nMiddle click to open in file explorer", 1000, 6, 4)
+ ToolTip.create(self.directory_button, "Right click to copy path\n\nMiddle click to open in file explorer", 1000, 6, 4)
+
# Save Button
self.save_button = Button(top_button_frame, overrelief="groove", text="Save", fg="blue", state="disabled", command=self.save_text_file)
self.save_button.pack(side="top", fill="x", pady=2)
- ToolTip.create_tooltip(self.save_button, "CTRL+S ", 1000, 6, 4)
+ ToolTip.create(self.save_button, "CTRL+S ", 1000, 6, 4)
+
# Navigation Buttons
nav_button_frame = Frame(self.master_control_frame)
@@ -502,8 +563,9 @@ class ImgTxtViewer:
self.prev_button = Button(nav_button_frame, overrelief="groove", text="<---Previous", state="disabled", command=lambda event=None: self.prev_pair(event), width=16)
self.next_button.pack(side="right", padx=2, pady=2)
self.prev_button.pack(side="right", padx=2, pady=2)
- ToolTip.create_tooltip(self.next_button, "ALT+R ", 1000, 6, 4)
- ToolTip.create_tooltip(self.prev_button, "ALT+L ", 1000, 6, 4)
+ ToolTip.create(self.next_button, "ALT+R ", 1000, 6, 4)
+ ToolTip.create(self.prev_button, "ALT+L ", 1000, 6, 4)
+
# Saved Label / Autosave
saved_label_frame = Frame(self.master_control_frame)
@@ -513,6 +575,7 @@ class ImgTxtViewer:
self.saved_label = Label(saved_label_frame, text="No Changes", state="disabled", width=23)
self.saved_label.pack()
+
# Image Index
self.index_frame = Frame(self.master_control_frame)
self.index_frame.pack(side="top", expand="no")
@@ -524,11 +587,12 @@ class ImgTxtViewer:
self.total_images_label = Label(self.index_frame, text=f"of {len(self.image_files)}", state="disabled")
self.total_images_label.pack(side="left", expand="yes")
+
# Suggestion text
self.suggestion_textbox = Text(self.master_control_frame, height=1, borderwidth=0, highlightthickness=0, bg='#f0f0f0')
- self.suggestion_colors = {0: "black", 1: "#c00004", 2: "black", 3: "#a800aa", 4: "#00ab2c", 5: "#fd9200"} #0=General tags, 1=Artists, 2=UNUSED, 3=Copyright, 4=Character, 5=Meta
self.suggestion_textbox.pack(side="top", fill="x")
+
# Startup info text
self.info_text = ScrolledText(self.master_control_frame)
self.info_text.pack(expand=True, fill="both")
@@ -540,6 +604,7 @@ class ImgTxtViewer:
self.info_text.bind("", self.show_textContext_menu)
self.info_text.config(state='disabled', wrap="word")
+
# #
# End of __init__ #
# #
@@ -552,11 +617,13 @@ class ImgTxtViewer:
#region - Text Box setup #
# #
+
def create_text_pane(self):
if not hasattr(self, 'text_pane'):
self.text_pane = PanedWindow(self.master_control_frame, orient="vertical", sashwidth=5, bg="#d0d0d0", bd=0)
self.text_pane.pack(side="bottom", fill="both", expand=1)
+
def create_text_box(self):
self.create_text_pane()
if not hasattr(self, 'text_frame'):
@@ -569,7 +636,7 @@ class ImgTxtViewer:
self.set_text_box_binds()
if not hasattr(self, 'text_widget_frame'):
self.create_text_control_frame()
- ToolTip.create_tooltip(self.suggestion_textbox,
+ ToolTip.create(self.suggestion_textbox,
"TAB: insert highlighted suggestion\n"
"ALT: Cycle suggestions\n\n"
"Danbooru Color Code:\n"
@@ -588,6 +655,7 @@ class ImgTxtViewer:
" Dark Green = Lore",
1000, 6, 4)
+
def create_text_control_frame(self):
self.text_widget_frame = Frame(self.master_control_frame)
self.text_pane.add(self.text_widget_frame, stretch="never")
@@ -611,65 +679,69 @@ class ImgTxtViewer:
self.create_font_widgets()
self.create_custom_dictionary_widgets()
+
def create_search_and_replace_widgets(self):
def clear_all():
self.search_entry.delete(0, 'end')
self.replace_entry.delete(0, 'end')
self.search_label = Label(self.tab1, text="Search for:")
self.search_label.pack(side='left', anchor="n", pady=4)
- ToolTip.create_tooltip(self.search_label, "Enter the EXACT text you want to search for", 200, 6, 4)
+ ToolTip.create(self.search_label, "Enter the EXACT text you want to search for", 200, 6, 4)
self.search_entry = Entry(self.tab1, textvariable=self.search_string_var, width=4)
self.search_entry.pack(side='left', anchor="n", pady=4, fill='x', expand=True)
self.replace_label = Label(self.tab1, text="Replace with:")
self.replace_label.pack(side='left', anchor="n", pady=4)
- ToolTip.create_tooltip(self.replace_label, "Enter the text you want to replace the searched text with\n\nLeave empty to replace with nothing (delete)", 200, 6, 4)
+ ToolTip.create(self.replace_label, "Enter the text you want to replace the searched text with\n\nLeave empty to replace with nothing (delete)", 200, 6, 4)
self.replace_entry = Entry(self.tab1, textvariable=self.replace_string_var, width=4)
self.replace_entry.pack(side='left', anchor="n", pady=4, fill='x', expand=True)
self.replace_entry.bind('', lambda event: self.search_and_replace())
self.replace_button = Button(self.tab1, text="Go!", overrelief="groove", width=4, command=self.search_and_replace)
self.replace_button.pack(side='left', anchor="n", pady=4, padx=1)
- ToolTip.create_tooltip(self.replace_button, "Text files will be backup up", 200, 6, 4)
+ ToolTip.create(self.replace_button, "Text files will be backup up", 200, 6, 4)
self.clear_button = Button(self.tab1, text="Clear", overrelief="groove", width=4, command=clear_all)
self.clear_button.pack(side='left', anchor="n", pady=4, padx=1)
self.undo_button = Button(self.tab1, text="Undo", overrelief="groove", width=4, command=self.restore_backup)
self.undo_button.pack(side='left', anchor="n", pady=4, padx=1)
- ToolTip.create_tooltip(self.undo_button, "Revert last action", 200, 6, 4)
+ ToolTip.create(self.undo_button, "Revert last action", 200, 6, 4)
+
def create_prefix_text_widgets(self):
def clear():
self.prefix_entry.delete(0, 'end')
self.prefix_label = Label(self.tab2, text="Prefix text:")
self.prefix_label.pack(side='left', anchor="n", pady=4)
- ToolTip.create_tooltip(self.prefix_label, "Enter the text you want to insert at the START of all text files\n\nCommas will be inserted as needed", 200, 6, 4)
+ ToolTip.create(self.prefix_label, "Enter the text you want to insert at the START of all text files\n\nCommas will be inserted as needed", 200, 6, 4)
self.prefix_entry = Entry(self.tab2, textvariable=self.prefix_string_var, width=4)
self.prefix_entry.pack(side='left', anchor="n", pady=4, fill='x', expand=True)
self.prefix_entry.bind('', lambda event: self.prefix_text_files())
self.prefix_button = Button(self.tab2, text="Go!", overrelief="groove", width=4, command=self.prefix_text_files)
self.prefix_button.pack(side='left', anchor="n", pady=4, padx=1)
- ToolTip.create_tooltip(self.prefix_button, "Text files will be backup up", 200, 6, 4)
+ ToolTip.create(self.prefix_button, "Text files will be backup up", 200, 6, 4)
self.clear_button = Button(self.tab2, text="Clear", overrelief="groove", width=4, command=clear)
self.clear_button.pack(side='left', anchor="n", pady=4, padx=1)
self.undo_button = Button(self.tab2, text="Undo", overrelief="groove", width=4, command=self.restore_backup)
self.undo_button.pack(side='left', anchor="n", pady=4, padx=1)
- ToolTip.create_tooltip(self.undo_button, "Revert last action", 200, 6, 4)
+ ToolTip.create(self.undo_button, "Revert last action", 200, 6, 4)
+
def create_append_text_widgets(self):
def clear():
self.append_entry.delete(0, 'end')
self.append_label = Label(self.tab3, text="Append text:")
self.append_label.pack(side='left', anchor="n", pady=4)
- ToolTip.create_tooltip(self.append_label, "Enter the text you want to insert at the END of all text files\n\nCommas will be inserted as needed", 200, 6, 4)
+ ToolTip.create(self.append_label, "Enter the text you want to insert at the END of all text files\n\nCommas will be inserted as needed", 200, 6, 4)
self.append_entry = Entry(self.tab3, textvariable=self.append_string_var, width=4)
self.append_entry.pack(side='left', anchor="n", pady=4, fill='x', expand=True)
self.append_entry.bind('', lambda event: self.append_text_files())
self.append_button = Button(self.tab3, text="Go!", overrelief="groove", width=4, command=self.append_text_files)
self.append_button.pack(side='left', anchor="n", pady=4, padx=1)
- ToolTip.create_tooltip(self.append_button, "Text files will be backup up", 200, 6, 4)
+ ToolTip.create(self.append_button, "Text files will be backup up", 200, 6, 4)
self.clear_button = Button(self.tab3, text="Clear", overrelief="groove", width=4, command=clear)
self.clear_button.pack(side='left', anchor="n", pady=4, padx=1)
self.undo_button = Button(self.tab3, text="Undo", overrelief="groove", width=4, command=self.restore_backup)
self.undo_button.pack(side='left', anchor="n", pady=4, padx=1)
- ToolTip.create_tooltip(self.undo_button, "Revert last action", 200, 6, 4)
+ ToolTip.create(self.undo_button, "Revert last action", 200, 6, 4)
+
def create_font_widgets(self, event=None):
def open_dropdown(event):
@@ -688,7 +760,7 @@ class ImgTxtViewer:
default_size = current_font_size
font_label = Label(self.tab4, text="Font:")
font_label.pack(side="left", anchor="n", pady=4)
- ToolTip.create_tooltip(font_label, "Recommended Fonts: Courier New, Ariel, Consolas, Segoe UI", 200, 6, 4)
+ ToolTip.create(font_label, "Recommended Fonts: Courier New, Ariel, Consolas, Segoe UI", 200, 6, 4)
font_box = ttk.Combobox(self.tab4, textvariable=self.font_var, width=4, takefocus=False)
font_box['values'] = list(tkinter.font.families())
font_box.set(current_font_name)
@@ -697,7 +769,7 @@ class ImgTxtViewer:
font_box.pack(side="left", anchor="n", pady=4, fill="x", expand=True)
font_size = Label(self.tab4, text="Font Size:")
font_size.pack(side="left", anchor="n", pady=4)
- ToolTip.create_tooltip(font_size, "Default size = 10", 200, 6, 4)
+ ToolTip.create(font_size, "Default size = 10", 200, 6, 4)
size_scale = Scale(self.tab4, from_=8, to=19, showvalue=False, orient="horizontal", cursor="hand2", takefocus=False)
size_scale.set(current_font_size)
size_scale.bind("", lambda event: set_font_and_size(self.font_var.get(), size_scale.get()))
@@ -705,6 +777,7 @@ class ImgTxtViewer:
reset_button = Button(self.tab4, text="Reset", overrelief="groove", width=4, command=reset_to_defaults)
reset_button.pack(side="left", anchor="n", pady=4, padx=1)
+
def create_custom_dictionary_widgets(self):
def save_content():
with open('my_tags.csv', 'w') as file:
@@ -722,13 +795,13 @@ class ImgTxtViewer:
self.tab5_button_frame = Frame(self.tab5_frame)
self.tab5_button_frame.pack(side='top', fill='x', pady=4)
self.save_dictionary_button = Button(self.tab5_button_frame, text="Save", overrelief="groove", takefocus=False, command=save_content)
- ToolTip.create_tooltip(self.save_dictionary_button, "Save the current changes to the 'my_tags.csv' file", 200, 6, 4)
+ ToolTip.create(self.save_dictionary_button, "Save the current changes to the 'my_tags.csv' file", 200, 6, 4)
self.save_dictionary_button.pack(side='left', padx=1, fill='x', expand=True)
self.tab5_label = Label(self.tab5_button_frame, text="^^^Expand this frame to view the text box^^^")
self.tab5_label.pack(side='left')
- ToolTip.create_tooltip(self.tab5_label, "Click and drag the gray bar up to reveal the text box", 200, 6, 4)
+ ToolTip.create(self.tab5_label, "Click and drag the gray bar up to reveal the text box", 200, 6, 4)
self.refresh_button = Button(self.tab5_button_frame, text="Refresh", overrelief="groove", takefocus=False, command=refresh_content)
- ToolTip.create_tooltip(self.refresh_button, "Refresh the suggestion dictionary after saving your changes", 200, 6, 4)
+ ToolTip.create(self.refresh_button, "Refresh the suggestion dictionary after saving your changes", 200, 6, 4)
self.refresh_button.pack(side='left', padx=1, fill='x', expand=True)
self.custom_dictionary_textbox = ScrolledText(self.tab5_frame)
self.custom_dictionary_textbox.pack(side='bottom', fill='both')
@@ -737,6 +810,7 @@ class ImgTxtViewer:
self.custom_dictionary_textbox.insert('end', content)
self.custom_dictionary_textbox.configure(undo=True)
+
def set_text_box_binds(self):
# Mouse binds
self.text_box.bind("", lambda event: (self.remove_tag(), self.clear_suggestions()))
@@ -764,6 +838,7 @@ class ImgTxtViewer:
self.text_box.bind("", self.disable_button)
self.text_box.bind("", self.disable_button)
+
# Text Box context menu
def show_textContext_menu(self, e):
textContext_menu = Menu(root, tearoff=0)
@@ -789,6 +864,7 @@ class ImgTxtViewer:
textContext_menu.add_checkbutton(label="List View", variable=self.list_mode, command=self.toggle_list_mode)
textContext_menu.tk_popup(e.x_root, e.y_root)
+
# Image context menu
def show_imageContext_menu(self, event):
imageContext_menu = Menu(self.master, tearoff=0)
@@ -806,6 +882,7 @@ class ImgTxtViewer:
imageContext_menu.add_radiobutton(label=size[0], variable=self.max_img_width, value=size[1], command=lambda s=size: self.save_text_file())
imageContext_menu.tk_popup(event.x_root, event.y_root)
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -813,6 +890,7 @@ class ImgTxtViewer:
#region - Additional Interface Setup #
# #
+
def set_icon(self):
if getattr(sys, 'frozen', False):
application_path = sys._MEIPASS
@@ -823,6 +901,7 @@ class ImgTxtViewer:
self.master.iconbitmap(icon_path)
except TclError: pass
+
def enable_menu_options(self):
tool_commands = [
"Open Current Directory...",
@@ -836,6 +915,7 @@ class ImgTxtViewer:
"Max Image Size",
"Highlighting Duplicates",
"Cleaning Text on Save",
+ "Colored Suggestions",
"Big Comma Mode",
"List View",
"Vertical View",
@@ -854,12 +934,14 @@ class ImgTxtViewer:
self.prev_button.configure(state="normal")
self.auto_save_checkbutton.configure(state="normal")
+
####### PanedWindow ##################################################
def configure_pane_position(self):
window_width = self.master.winfo_width()
self.primary_paned_window.sash_place(0, window_width // 2, 0)
self.configure_pane()
+
def swap_pane_sides(self):
self.primary_paned_window.remove(self.master_image_frame)
self.primary_paned_window.remove(self.master_control_frame)
@@ -873,6 +955,7 @@ class ImgTxtViewer:
self.configure_pane()
self.panes_swapped = not self.panes_swapped
+
def swap_pane_orientation(self):
current_orient = self.primary_paned_window.cget('orient')
new_orient = 'vertical' if current_orient == 'horizontal' else 'horizontal'
@@ -883,6 +966,7 @@ class ImgTxtViewer:
self.master.minsize(300, 600)
self.master.after_idle(self.configure_pane_position)
+
def snap_sash_to_half(self, event):
total_width = self.primary_paned_window.winfo_width()
half_point = int(total_width / 2)
@@ -891,10 +975,12 @@ class ImgTxtViewer:
self.primary_paned_window.sash_place(0, half_point, 0)
self.configure_pane()
+
def configure_pane(self):
self.primary_paned_window.paneconfigure(self.master_image_frame, minsize=300, stretch="always")
self.primary_paned_window.paneconfigure(self.master_control_frame, minsize=300, stretch="always")
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -902,6 +988,7 @@ class ImgTxtViewer:
#region - Autocomplete #
# #
+
### Display Suggestions ##################################################
def handle_suggestion_event(self, event):
if event.keysym == "Tab":
@@ -928,6 +1015,7 @@ class ImgTxtViewer:
return False
return True
+
def update_suggestions(self, event=None):
tags_with_underscore = self.get_tags_with_underscore()
if event is None:
@@ -959,6 +1047,7 @@ class ImgTxtViewer:
else:
self.clear_suggestions()
+
def highlight_suggestions(self):
self.suggestion_textbox.config(state='normal')
self.suggestion_textbox.delete('1.0', 'end')
@@ -980,6 +1069,7 @@ class ImgTxtViewer:
self.suggestion_textbox.insert('end', ', ')
self.suggestion_textbox.config(state='disabled')
+
def cursor_inside_tag(self, cursor_position):
line, column = map(int, cursor_position.split('.'))
line_text = self.text_box.get(f"{line}.0", f"{line}.end")
@@ -989,6 +1079,7 @@ class ImgTxtViewer:
inside_tag = not (column == 0 or line_text[column-1:column] in (',', ' ') or line_text[column:column+1] in (',', ' ') or column == len(line_text))
return inside_tag
+
def clear_suggestions(self):
self.suggestions = []
self.selected_suggestion_index = 0
@@ -997,6 +1088,7 @@ class ImgTxtViewer:
self.suggestion_textbox.insert('1.0', "...")
self.suggestion_textbox.config(state='disabled')
+
### Insert Suggestion ##################################################
def insert_selected_suggestion(self, selected_suggestion):
selected_suggestion = selected_suggestion.strip()
@@ -1019,6 +1111,7 @@ class ImgTxtViewer:
if self.list_mode.get():
self.insert_newline_listmode(called_from_insert=True)
+
def position_cursor(self, start_of_current_word, selected_suggestion):
if self.text_box.get(start_of_current_word).startswith(' '):
offset = len(selected_suggestion) + 2
@@ -1027,6 +1120,7 @@ class ImgTxtViewer:
self.text_box.mark_set("insert", "{}+{}c".format(start_of_current_word, offset))
self.text_box.insert("insert", " ")
+
def insert_newline_listmode(self, event=None, called_from_insert=False):
if self.list_mode.get():
self.text_box.insert(INSERT, '\n')
@@ -1034,25 +1128,38 @@ class ImgTxtViewer:
self.text_box.mark_set("insert", "insert-1l")
return 'break'
+
### Suggestion Settings ##################################################
+
+
def change_autocomplete_dictionary(self):
+ if self.use_colored_suggestions.get() == True:
+ self.suggestion_colors = {0: "black", 1: "#c00004", 2: "black", 3: "#a800aa", 4: "#00ab2c", 5: "#fd9200"} #0=General tags, 1=Artists, 2=UNUSED, 3=Copyright, 4=Character, 5=Meta
+ else:
+ self.suggestion_colors = {0: "black", 1: "black", 2: "black", 3: "black", 4: "black", 5: "black"}
if self.csv_var.get() == 'all':
self.autocomplete = Autocomplete('danbooru.csv')
self.autocomplete.data.update(Autocomplete('dictionary.csv').data)
self.autocomplete.data.update(Autocomplete('e621.csv').data)
elif self.csv_var.get() == 'e621.csv':
self.autocomplete = Autocomplete(self.csv_var.get())
- self.suggestion_colors = {-1: "black", 0: "black", 1: "#f2ac08", 3: "#dd00dd", 4: "#00aa00", 5: "#ed5d1f", 6: "#ff3d3d", 7: "#ff3d3d", 8: "#228822"}
+ if self.use_colored_suggestions.get() == True:
+ self.suggestion_colors = {-1: "black", 0: "black", 1: "#f2ac08", 3: "#dd00dd", 4: "#00aa00", 5: "#ed5d1f", 6: "#ff3d3d", 7: "#ff3d3d", 8: "#228822"}
+ else:
+ self.suggestion_colors = {0: "black", 1: "black", 2: "black", 3: "black", 4: "black", 5: "black", 6: "black", 7: "black", 8: "black"}
else:
self.autocomplete = Autocomplete(self.csv_var.get())
+
def set_suggestion_quantity(self, suggestion_quantity):
self.autocomplete.max_suggestions = suggestion_quantity
self.update_suggestions(event=None)
+
def get_tags_with_underscore(self):
return {"0_0", "o_o", ">_o", "x_x", "|_|", "._.", "^_^", ">_<", "@_@", ">_@", "+_+", "+_-", "=_=", "_", "<|>_<|>", "ಠ_ಠ"}
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1060,11 +1167,13 @@ class ImgTxtViewer:
#region - TextBox Highlights #
# #
+
def highlight_duplicates(self, event, mouse=True):
if not self.highlighting_duplicates.get():
return
self.text_box.after_idle(self._highlight_duplicates, mouse)
+
def _highlight_duplicates(self, mouse):
self.text_box.tag_remove("highlight", "1.0", "end")
if not self.text_box.tag_ranges("sel"):
@@ -1085,6 +1194,7 @@ class ImgTxtViewer:
end = match.end()
self.text_box.tag_add("highlight", f"1.0 + {start} chars", f"1.0 + {end} chars")
+
def toggle_highlight_all_duplicates(self):
self.highlighting_all_duplicates.set(not self.highlighting_all_duplicates.get())
if self.highlighting_all_duplicates.get():
@@ -1092,6 +1202,7 @@ class ImgTxtViewer:
else:
self.remove_highlight()
+
def highlight_all_duplicates(self):
self.text_box.tag_remove("highlight", "1.0", "end")
text = self.text_box.get("1.0", "end").strip().replace(',', '')
@@ -1120,10 +1231,12 @@ class ImgTxtViewer:
self.text_box.tag_add(word, pos, end)
start = end
+
def remove_highlight(self):
self.text_box.tag_remove("highlight", "1.0", "end")
self.master.after(100, lambda: self.remove_tag())
+
def remove_tag(self):
self.highlighting_all_duplicates.set(False)
for tag in self.text_box.tag_names():
@@ -1131,6 +1244,7 @@ class ImgTxtViewer:
if self.bold_commas.get():
self.toggle_big_comma_mode()
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1138,6 +1252,7 @@ class ImgTxtViewer:
#region - Primary Functions #
# #
+
def load_pairs(self):
self.info_text.pack_forget()
self.image_files = []
@@ -1166,6 +1281,7 @@ class ImgTxtViewer:
if not self.watching_files:
self.start_watching_file()
+
def show_pair(self):
if self.image_files:
self.image_file = self.image_files[self.current_index]
@@ -1199,6 +1315,7 @@ class ImgTxtViewer:
self.clear_suggestions()
self.highlighting_all_duplicates.set(False)
+
def resize_and_scale_image(self, image, max_img_width, max_height, event=None):
w, h = image.size
aspect_ratio = w / h
@@ -1222,6 +1339,7 @@ class ImgTxtViewer:
self.image_preview.image = photo
return image, aspect_ratio
+
def start_watching_file(self):
if not self.text_files:
return
@@ -1230,6 +1348,7 @@ class ImgTxtViewer:
thread = threading.Thread(target=self.watch_file)
thread.start()
+
def watch_file(self):
text_file = self.text_files[self.current_index]
if not os.path.exists(text_file):
@@ -1250,6 +1369,7 @@ class ImgTxtViewer:
self.show_pair()
last_modified = current_modified
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1257,6 +1377,7 @@ class ImgTxtViewer:
#region - Navigation #
# #
+
def update_pair(self, direction):
if self.image_dir.get() == "Choose Directory...":
return
@@ -1277,12 +1398,15 @@ class ImgTxtViewer:
self.current_index = (self.current_index - 1) % len(self.image_files)
self.show_pair()
+
def next_pair(self, event):
self.update_pair('next')
+
def prev_pair(self, event):
self.update_pair('prev')
+
def jump_to_image(self, event):
try:
index = int(self.image_index_entry.get()) - 1
@@ -1298,17 +1422,21 @@ class ImgTxtViewer:
self.saved_label.config(text="No Changes", bg="#f0f0f0", fg="black")
except ValueError: pass
+
def update_image_file_count(self):
extensions = ['.jpg', '.jpeg', '.jpg_large', '.jfif', '.png', '.webp', '.bmp']
self.image_files = [file for ext in extensions for file in glob.glob(f"{self.image_dir.get()}/*{ext}")]
self.text_files = [os.path.splitext(file)[0] + '.txt' for file in self.image_files]
self.total_images_label.config(text=f"of {len(self.image_files)}")
+
def mouse_scroll(self, event):
if event.delta > 0:
self.next_pair(event)
else:
self.prev_pair(event)
+
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1316,6 +1444,7 @@ class ImgTxtViewer:
#region - Text Options #
# #
+
def toggle_list_mode(self, event=None):
self.text_box.config(undo=False)
if self.list_mode.get():
@@ -1331,6 +1460,7 @@ class ImgTxtViewer:
self.text_box.insert("1.0", self.cleanup_text(formatted_contents))
self.text_box.config(undo=True)
+
def toggle_big_comma_mode(self, event=None):
if self.bold_commas.get():
self.text_box.tag_remove("bold", "1.0", "end")
@@ -1345,6 +1475,7 @@ class ImgTxtViewer:
else:
self.text_box.tag_remove("bold", "1.0", "end")
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1352,18 +1483,21 @@ class ImgTxtViewer:
#region - Text Tools #
# #
+
def batch_tag_delete(self):
- if getattr(sys, 'frozen', False):
- application_path = sys._MEIPASS
+ main_window_width = root.winfo_width()
+ main_window_height = root.winfo_height()
+ main_window_x = root.winfo_x() + 250 + main_window_width // 2
+ main_window_y = root.winfo_y() - 300 + main_window_height // 2
+ directory = self.image_dir.get()
+ python_script_path = "./batch_tag_delete.py"
+ if os.path.isfile(python_script_path):
+ command = ["python", python_script_path, str(directory), str(main_window_x), str(main_window_y)]
else:
- application_path = os.path.dirname(os.path.abspath(__file__))
- script_path = os.path.join(application_path, 'batch_tag_delete.py')
- if os.path.exists(script_path):
- main_window_width = root.winfo_width()
- main_window_height = root.winfo_height()
- main_window_x = root.winfo_x() + 250 + main_window_width // 2
- main_window_y = root.winfo_y() - 300 + main_window_height // 2
- batch_tag_delete.main(self.image_dir.get(), str(main_window_x), str(main_window_y))
+ executable_path = "./batch_tag_delete.exe"
+ command = [executable_path, str(directory), str(main_window_x), str(main_window_y)]
+ subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
def search_and_replace(self):
if not self.check_current_directory():
@@ -1389,6 +1523,7 @@ class ImgTxtViewer:
self.show_pair()
self.saved_label.config(text="Search & Replace Complete!", bg="#6ca079", fg="white")
+
def prefix_text_files(self):
if not self.check_current_directory():
return
@@ -1417,6 +1552,7 @@ class ImgTxtViewer:
self.show_pair()
self.saved_label.config(text="Prefix Text Complete!", bg="#6ca079", fg="white")
+
def append_text_files(self):
if not self.check_current_directory():
return
@@ -1443,6 +1579,7 @@ class ImgTxtViewer:
self.show_pair()
self.saved_label.config(text="Append Text Complete!", bg="#6ca079", fg="white")
+
def delete_tag_under_mouse(self, event):
cursor_pos = self.text_box.index(f"@{event.x},{event.y}")
line_start = self.text_box.index(f"{cursor_pos} linestart")
@@ -1468,6 +1605,23 @@ class ImgTxtViewer:
self.text_box.insert("1.0", cleaned_text)
self.text_box.tag_configure("highlight", background="#5da9be")
+
+#endregion
+################################################################################################################################################
+################################################################################################################################################
+# #
+#region - Image Tools #
+# #
+
+
+ def resize_images(self):
+ if os.path.isfile('resize_images.py'):
+ command = f'python resize_images.py --folder_path "{self.image_dir.get()}"'
+ else:
+ command = f'resize_images.exe --folder_path "{self.image_dir.get()}"'
+ subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1475,6 +1629,7 @@ class ImgTxtViewer:
#region - Misc Functions #
# #
+
# Used to position new windows beside the main window.
def position_dialog(self, dialog, window_width, window_height):
root_x = self.master.winfo_rootx()
@@ -1484,12 +1639,14 @@ class ImgTxtViewer:
position_top = root_y + -50
dialog.geometry(f"{window_width}x{window_height}+{position_right}+{position_top}")
+
def change_label(self):
if self.auto_save_var.get():
self.saved_label.config(text="Changes are autosaved", bg="#5da9be", fg="white")
else:
self.saved_label.config(text="Changes not saved", bg="#FD8A8A", fg="white")
+
def copy_to_clipboard(self, event):
try:
self.master.clipboard_clear()
@@ -1500,26 +1657,32 @@ class ImgTxtViewer:
self.master.after(400, lambda: self.image_dir.set(image_dir))
except Exception: pass
+
def disable_button(self, event):
return "break"
+
def toggle_always_on_top(self):
current_state = root.attributes('-topmost')
new_state = 0 if current_state == 1 else 1
root.attributes('-topmost', new_state)
+
#endregion
################################################################################################################################################
################################################################################################################################################
# #
#region - About Window #
# #
+
+
def toggle_about_window(self):
if self.about_window is not None:
self.close_about_window()
else:
self.open_about_window()
+
def open_about_window(self):
self.about_window = AboutWindow(self.master)
self.about_window.protocol("WM_DELETE_WINDOW", self.close_about_window)
@@ -1529,10 +1692,12 @@ class ImgTxtViewer:
main_window_y = root.winfo_y() - 275 + main_window_height // 2
self.about_window.geometry("+{}+{}".format(main_window_x, main_window_y))
+
def close_about_window(self):
self.about_window.destroy()
self.about_window = None
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1540,6 +1705,7 @@ class ImgTxtViewer:
#region - Text Cleanup #
# #
+
def cleanup_all_text_files(self, show_confirmation=True):
if not self.check_current_directory():
return
@@ -1560,6 +1726,7 @@ class ImgTxtViewer:
return
self.show_pair()
+
def cleanup_text(self, text):
if self.cleaning_text.get():
text = self.remove_duplicates(text)
@@ -1579,6 +1746,7 @@ class ImgTxtViewer:
text = text.strip() # remove leading and trailing spaces
return text
+
def remove_duplicates(self, text):
if self.list_mode.get():
text = text.lower().split('\n')
@@ -1592,6 +1760,7 @@ class ImgTxtViewer:
text = ','.join(text)
return text
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1599,6 +1768,7 @@ class ImgTxtViewer:
#region - Save and close #
# #
+
def save_text_file(self):
if self.image_dir.get() != "Choose Directory..." and self.check_current_directory() and self.text_files:
self.save_file()
@@ -1606,6 +1776,7 @@ class ImgTxtViewer:
self.save_file()
self.show_pair()
+
def save_file(self):
text_file = self.text_files[self.current_index]
text = self.text_box.get("1.0", "end").strip()
@@ -1620,6 +1791,7 @@ class ImgTxtViewer:
text = ', '.join(text.split('\n'))
f.write(text)
+
def on_closing(self):
self.stop_thread = True
if not os.path.isdir(self.image_dir.get()):
@@ -1638,6 +1810,7 @@ class ImgTxtViewer:
self.delete_text_backup()
self.delete_trash_folder()
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1645,10 +1818,12 @@ class ImgTxtViewer:
#region - File Management #
# #
+
def natural_sort(self, s):
return [int(text) if text.isdigit() else text.lower()
for text in re.split(r'(\d+)', s)]
+
def choose_working_directory(self):
try:
directory = askdirectory()
@@ -1663,22 +1838,26 @@ class ImgTxtViewer:
messagebox.showwarning("No Images", "The selected directory does not contain any images.")
except Exception: pass
+
def open_current_directory(self, event=None):
try:
os.startfile(self.image_dir.get())
except Exception: pass
+
def open_current_image(self, event=None):
try:
os.startfile(self.image_file)
except Exception: pass
+
def check_current_directory(self):
if not os.path.isdir(self.image_dir.get()):
messagebox.showerror("Error!", "Invalid or No directory selected.\n\n Select a directory before using this tool.")
return False
return True
+
def create_custom_dictionary(self):
csv_filename = 'my_tags.csv'
if not os.path.isfile(csv_filename):
@@ -1691,6 +1870,7 @@ class ImgTxtViewer:
writer.writerow(["supercalifragilisticexpialidocious"])
self.change_autocomplete_dictionary()
+
def rename_odd_files(self, filename):
file_extension = os.path.splitext(filename)[1].lower()
file_rename_dict = {".jpg_large": "jpg", ".jfif": "jpg"}
@@ -1715,6 +1895,7 @@ class ImgTxtViewer:
filename = new_filename
return filename
+
def restore_backup(self):
backup_dir = os.path.join(os.path.dirname(self.text_files[0]), 'text_backup')
if not os.path.exists(backup_dir):
@@ -1735,6 +1916,7 @@ class ImgTxtViewer:
self.saved_label.config(text="Files Restored", bg="#6ca079", fg="white")
except Exception: pass
+
def backup_text_files(self):
if not self.check_current_directory():
return
@@ -1748,17 +1930,20 @@ class ImgTxtViewer:
shutil.copy2(text_file, new_backup)
except Exception: pass
+
def delete_text_backup(self):
if self.text_files:
backup_folder = os.path.join(os.path.dirname(self.text_files[0]), 'text_backup')
if os.path.exists(backup_folder):
shutil.rmtree(backup_folder)
+
def delete_trash_folder(self):
trash_dir = os.path.join(self.image_dir.get(), "Trash")
if os.path.exists(trash_dir) and not os.listdir(trash_dir):
shutil.rmtree(trash_dir)
+
def delete_pair(self):
if not self.check_current_directory():
return
@@ -1782,6 +1967,7 @@ class ImgTxtViewer:
self.toolsMenu.entryconfig("Undo Delete", state="normal")
else: pass
+
def undo_delete_pair(self):
if not self.check_current_directory():
return
@@ -1798,6 +1984,7 @@ class ImgTxtViewer:
self.undo_state.set("disabled")
self.toolsMenu.entryconfig("Undo Delete", state="disabled")
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1805,10 +1992,12 @@ class ImgTxtViewer:
#region - Framework #
# #
+
def set_appid(self):
myappid = 'ImgTxtViewer.Nenotriple'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
+
def set_window_size(self, master):
master.minsize(600, 300) # Width x Height
window_width = 1280
@@ -1817,6 +2006,7 @@ class ImgTxtViewer:
position_top = root.winfo_screenheight()//2 - window_height//2
root.geometry(f"{window_width}x{window_height}+{position_right}+{position_top}")
+
root = Tk()
app = ImgTxtViewer(root)
app.toggle_always_on_top()
@@ -1825,6 +2015,7 @@ root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.title(f"{VERSION} - img-txt_viewer --- github.com/Nenotriple/img-txt_viewer")
root.mainloop()
+
#endregion
################################################################################################################################################
################################################################################################################################################
@@ -1832,12 +2023,18 @@ root.mainloop()
#region - Changelog #
# #
+
'''
+
[v1.84 changes:](https://github.com/Nenotriple/img-txt_viewer/releases/tag/v1.84)
- New:
- -
+ - New tool: `Resize Images`: You can find this in the Tools menu.
+ - Resize operations: Resize to Resolution, Resize to Width, Resize to Height, Resize to Shorter Side, Resize to Longer Side
+ - Just like "batch_tag_delete.py", "resize_images.py" is a stand-alone tool. You can run it 100% independently of img-txt_viewer.
+ - Images will be overwritten when resized.
+ - New option: `Colored Suggestions`, Use this to enable or disable the color coded autocomplete suggestions.
@@ -1847,7 +2044,7 @@ root.mainloop()
- Other changes:
- -
+ - Batch Tag Delete is no longer bundled within img-txt_viewer. This allows you to run them separately.
@@ -1859,16 +2056,20 @@ root.mainloop()
[]:
+
'''
+
################################################################################################################################################
################################################################################################################################################
# #
# todo #
# #
+
'''
+
- Todo
-
@@ -1878,5 +2079,7 @@ root.mainloop()
- **Minor** Undo should be less jarring when inserting a suggestion.
- **Minor** After deleting or Undo Delete. PanedWindow sash moves position.
+
'''
+
#endregion
diff --git a/resize_images.py b/resize_images.py
new file mode 100644
index 0000000..2cdf291
--- /dev/null
+++ b/resize_images.py
@@ -0,0 +1,387 @@
+"""
+########################################
+# #
+# Resize Image #
+# #
+# Version : v1.00 #
+# Author : github.com/Nenotriple #
+# #
+########################################
+
+Description:
+-------------
+This script allows you to select a directory and resize all images in the selected directory.
+Resize operations include: Resize to Resolution, Resize to Width, Resize to Height, Resize to Shorter Side, Resize to Longer Side
+
+
+"""
+
+
+
+################################################################################################################################################
+#region - Imports
+
+
+import os
+import sys
+import ctypes
+import argparse
+import tkinter as tk
+from tkinter import ttk, filedialog, messagebox, TclError
+from tkinter.scrolledtext import ScrolledText
+from PIL import Image
+
+myappid = 'ImgTxtViewer.Nenotriple'
+ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
+
+#endregion
+################################################################################################################################################
+#region - Main
+
+
+class Resize_Image(tk.Frame):
+ def __init__(self, master=None, folder_path=None):
+ super().__init__(master)
+ self.master = master
+ self.pack(fill="both", expand=True)
+ self.folder_path = folder_path
+ self.create_widgets()
+
+
+#endregion
+################################################################################################################################################
+#region - Interface
+
+
+ def create_widgets(self):
+ widget_frame = tk.Frame(self)
+ widget_frame.pack(fill="both", padx=2, pady=2)
+
+
+ # Directory Label
+ self.directory_label = tk.Label(widget_frame, relief="groove", text="", anchor="w", background="white")
+ if self.folder_path is None:
+ self.directory_label.config(text="Select a directory containing images to begin!")
+ else:
+ self.directory_label.config(text=f"{self.folder_path}")
+ self.directory_label.pack(fill="x")
+
+
+ # Select Frame
+ select_frame = tk.Frame(widget_frame)
+ select_frame.pack(fill="both")
+
+ self.select_button = tk.Button(select_frame, overrelief="groove")
+ self.select_button["text"] = " \tSelect Folder"
+ self.select_button["command"] = self.select_folder
+ self.select_button.pack(side="left", fill="x", expand=True, padx=2, pady=2)
+
+ self.open_button = tk.Button(select_frame, overrelief="groove", width=10)
+ self.open_button["text"] = "Open"
+ self.open_button["command"] = self.open_folder
+ self.open_button.pack(side="left", padx=2, pady=2)
+
+
+ # Resize Mode Frame
+ resize_mode_frame = tk.Frame(widget_frame)
+ resize_mode_frame.pack(fill="x")
+
+ self.resize_mode_label = tk.Label(resize_mode_frame, text="Resize Mode:")
+ self.resize_mode_label.pack(side="left", anchor="w", padx=2, pady=2)
+
+ self.resize_mode_var = tk.StringVar()
+ self.resize_mode = ttk.Combobox(resize_mode_frame, textvariable=self.resize_mode_var, values=["Resize to Resolution", "Resize to Width", "Resize to Height", "Resize to Shorter Side", "Resize to Longer Side"], state="readonly")
+ self.resize_mode.set("Resize to Resolution")
+ self.resize_mode.pack(side="left", fill="x", expand=True, padx=2, pady=2)
+
+ self.use_output_folder_var = tk.IntVar(value=1)
+ self.use_output_folder_checkbutton = tk.Checkbutton(resize_mode_frame, overrelief="groove", text="Use Output Folder", variable=self.use_output_folder_var)
+ self.use_output_folder_checkbutton.pack(side="left", fill="x")
+
+
+ # Width Frame
+ width_frame = tk.Frame(widget_frame)
+ width_frame.pack(fill="x", anchor="w")
+ self.width_label = tk.Label(width_frame, text="Width: ")
+ self.width_label.pack(side="left", anchor="w", padx=2, pady=2)
+ self.width_entry = tk.Entry(width_frame, width=10)
+ self.width_entry.pack(side="right", fill="x", expand=True, padx=2, pady=2)
+
+
+ # Height Frame
+ height_frame = tk.Frame(widget_frame)
+ height_frame.pack(fill="x", anchor="w")
+ self.height_label = tk.Label(height_frame, text="Height:")
+ self.height_label.pack(side="left", anchor="w", padx=2, pady=2)
+ self.height_entry = tk.Entry(height_frame, width=10)
+ self.height_entry.pack(side="right", fill="x", expand=True, padx=2, pady=2)
+
+
+ # Resize Button
+ self.resize_button = tk.Button(widget_frame, overrelief="groove")
+ self.resize_button["text"] = "Resize!\t "
+ self.resize_button["command"] = self.resize
+ self.resize_button.pack(fill="x", padx=2, pady=2)
+
+
+ # Percent Bar
+ self.percent_complete = tk.StringVar()
+ self.percent_bar = ttk.Progressbar(widget_frame, value=0)
+ self.percent_bar.pack(fill="x", padx=2, pady=2)
+
+
+ # Text Box
+ self.text_box = ScrolledText(widget_frame)
+ self.text_box.pack(fill="both", expand=True, padx=2, pady=2)
+ descriptions = ["Resize to Resolution: Resize to specific width and height\n\n",
+ "Preserves aspect ratio:\n",
+ "Resize to Width: Resize the image width\n",
+ "Resize to Height: Resize the image height\n",
+ "Resize to Shorter side: Resize the shorter side of the image\n",
+ "Resize to Longer side: Resize the longer side of the image"]
+ for description in descriptions:
+ self.text_box.insert("end", description + "\n")
+ self.text_box.config(state="disabled")
+
+
+ self.resize_mode_var.trace_add('write', self.update_entries)
+
+
+#endregion
+################################################################################################################################################
+#region - Misc
+
+
+ def update_entries(self, *args):
+ mode = self.resize_mode_var.get()
+ if mode == "Resize to Resolution":
+ self.width_entry.config(state="normal")
+ self.width_label.config(state="normal")
+ self.height_entry.config(state="normal")
+ self.height_label.config(state="normal")
+ elif mode in ["Resize to Width", "Resize to Shorter Side"]:
+ self.width_entry.config(state="normal")
+ self.width_label.config(state="normal")
+ self.height_entry.delete(0, 'end')
+ self.height_entry.config(state="disabled")
+ self.height_label.config(state="disabled")
+ elif mode in ["Resize to Height", "Resize to Longer Side"]:
+ self.width_entry.delete(0, 'end')
+ self.width_entry.config(state="disabled")
+ self.width_label.config(state="disabled")
+ self.height_entry.config(state="normal")
+ self.height_label.config(state="normal")
+
+
+ def select_folder(self):
+ new_folder_path = filedialog.askdirectory()
+ if new_folder_path:
+ self.folder_path = new_folder_path
+ self.directory_label.config(text=f"{self.folder_path}")
+
+
+ def open_folder(self):
+ try:
+ os.startfile(self.folder_path)
+ except Exception: pass
+
+
+ def get_output_folder_path(self):
+ if self.use_output_folder_var.get() == 1:
+ output_folder_path = os.path.join(self.folder_path, "output")
+ if not os.path.exists(output_folder_path):
+ os.makedirs(output_folder_path)
+ else:
+ output_folder_path = self.folder_path
+ return output_folder_path
+
+
+#endregion
+################################################################################################################################################
+#region - Resize
+
+
+ def resize_to_resolution(self, img, width, height):
+ if width is None or height is None:
+ messagebox.showinfo("Error", "Please enter a valid width and height.")
+ return
+ if not isinstance(width, int) or not isinstance(height, int):
+ raise TypeError("Width and height must be integers.")
+ if width <= 0 or height <= 0:
+ raise ValueError("Width and height must be greater than 0.")
+ img = img.resize((width, height), Image.LANCZOS)
+ return img
+
+
+ def resize_to_width(self, img, width):
+ if width is None:
+ messagebox.showinfo("Error", "Please enter a valid width")
+ return
+ if not isinstance(width, int):
+ raise TypeError("Width must be an integer.")
+ if width <= 0:
+ raise ValueError("Width must be greater than 0.")
+ wpercent = (width/float(img.size[0]))
+ hsize = int((float(img.size[1])*float(wpercent)))
+ img = img.resize((width, hsize), Image.LANCZOS)
+ return img
+
+
+ def resize_to_height(self, img, height):
+ if height is None:
+ messagebox.showinfo("Error", "Please enter a valid height")
+ return
+ if not isinstance(height, int):
+ raise TypeError("Height must be an integer.")
+ if height <= 0:
+ raise ValueError("Height must be greater than 0.")
+ hpercent = (height/float(img.size[1]))
+ wsize = int((float(img.size[0])*float(hpercent)))
+ img = img.resize((wsize, height), Image.LANCZOS)
+ return img
+
+
+ def resize_to_shorter_side(self, img, width):
+ if width is None:
+ messagebox.showinfo("Error", "Please enter a valid width")
+ return
+ if not isinstance(width, int):
+ raise TypeError("Width must be an integer.")
+ if width <= 0:
+ raise ValueError("Width must be greater than 0.")
+ if img.size[0] < img.size[1]:
+ wpercent = (width/float(img.size[0]))
+ hsize = int((float(img.size[1])*float(wpercent)))
+ img = img.resize((width, hsize), Image.LANCZOS)
+ else:
+ hpercent = (width/float(img.size[1]))
+ wsize = int((float(img.size[0])*float(hpercent)))
+ img = img.resize((wsize, width), Image.LANCZOS)
+ return img
+
+
+ def resize_to_longer_side(self, img, height):
+ if height is None:
+ messagebox.showinfo("Error", "Please enter a valid height")
+ return
+ if not isinstance(height, int):
+ raise TypeError("Height must be an integer.")
+ if height <= 0:
+ raise ValueError("Height must be greater than 0.")
+ if img.size[0] > img.size[1]:
+ wpercent = (height/float(img.size[0]))
+ hsize = int((float(img.size[1])*float(wpercent)))
+ img = img.resize((height, hsize), Image.LANCZOS)
+ else:
+ hpercent = (height/float(img.size[1]))
+ wsize = int((float(img.size[0])*float(hpercent)))
+ img = img.resize((wsize, height), Image.LANCZOS)
+ return img
+
+
+ def resize(self):
+ self.percent_complete.set(0)
+ if self.folder_path is not None:
+ try:
+ resize_mode = self.resize_mode_var.get()
+ width_entry = self.width_entry.get()
+ height_entry = self.height_entry.get()
+ width = int(width_entry) if width_entry else None
+ height = int(height_entry) if height_entry else None
+ image_files = [f for f in os.listdir(self.folder_path) if f.endswith((".jpg", ".jpeg", ".png", ".webp", ".bmp"))]
+ total_images = len(image_files)
+ output_folder_path = self.get_output_folder_path()
+ if self.use_output_folder_var.get() == 1:
+ confirm_message = f"Images will be saved to\n{os.path.normpath(output_folder_path)}"
+ else:
+ confirm_message = "Images will be overwritten, continue?"
+ if messagebox.askokcancel("Confirmation", confirm_message):
+ self.master.focus_force()
+ for i, filename in enumerate(image_files):
+ try:
+ img = Image.open(os.path.join(self.folder_path, filename))
+ if img is None:
+ return
+ img = img.convert('RGB')
+ if resize_mode == "Resize to Resolution":
+ img = self.resize_to_resolution(img, width, height)
+ elif resize_mode == "Resize to Width":
+ img = self.resize_to_width(img, width)
+ elif resize_mode == "Resize to Height":
+ img = self.resize_to_height(img, height)
+ elif resize_mode == "Resize to Shorter Side":
+ img = self.resize_to_shorter_side(img, width)
+ elif resize_mode == "Resize to Longer Side":
+ img = self.resize_to_longer_side(img, height)
+ if img is None:
+ return
+ if 'icc_profile' in img.info:
+ del img.info['icc_profile']
+ img.save(os.path.join(output_folder_path, filename), 'PNG')
+ self.percent_complete.set((i+1)/total_images*100)
+ self.percent_bar['value'] = self.percent_complete.get()
+ self.percent_bar.update()
+ except Exception as e:
+ print(f"Error processing file {filename}: {str(e)}")
+ messagebox.showinfo("Done!", "Resizing finished.")
+ self.master.focus_force()
+ except Exception as e:
+ print(f"Error in resize function: {str(e)}")
+ else:
+ return
+
+
+#endregion
+################################################################################################################################################
+#region - Framework
+
+
+def parse_arguments():
+ parser = argparse.ArgumentParser(description='Resize Images Resize_Image')
+ parser.add_argument('--folder_path', type=str, help='Path to the folder')
+ args = parser.parse_args()
+ return args
+
+
+def setup_root():
+ root = tk.Tk()
+ root.title("Resize Images")
+ root.geometry("525x425")
+ root.minsize(300,155)
+ root.maxsize(2000,500)
+ root.update_idletasks()
+ set_icon(root)
+ return root
+
+
+def set_icon(root):
+ if getattr(sys, 'frozen', False):
+ application_path = sys._MEIPASS
+ elif __file__:
+ application_path = os.path.dirname(__file__)
+ icon_path = os.path.join(application_path, "icon.ico")
+ try:
+ root.iconbitmap(icon_path)
+ except TclError:
+ pass
+
+
+def center_window(root):
+ x = (root.winfo_screenwidth() - root.winfo_width()) // 2
+ y = (root.winfo_screenheight() - root.winfo_height()) // 2
+ root.geometry(f"+{x}+{y}")
+
+
+def create_app(root, folder_path):
+ app = Resize_Image(master=root, folder_path=folder_path)
+
+
+if __name__ == "__main__":
+ args = parse_arguments()
+ root = setup_root()
+ center_window(root)
+ create_app(root, args.folder_path)
+ root.mainloop()
+
+
+#endregion