From bc0275b8422171acc536576751b156de2ba6b897 Mon Sep 17 00:00:00 2001 From: Nenotriple <70049990+Nenotriple@users.noreply.github.com> Date: Thu, 11 Jan 2024 03:31:55 -0800 Subject: [PATCH 1/6] Update img-txt_viewer.pyw - 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: - Fixed suggestions breaking when typing a parentheses. - Other changes: - Batch Tag Delete is no longer bundled within img-txt_viewer. This allows you to run them separately. --- img-txt_viewer.pyw | 287 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 245 insertions(+), 42 deletions(-) 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 From 04974d1e6b9d247a6cfa4c40623a22f5ef769ec3 Mon Sep 17 00:00:00 2001 From: Nenotriple <70049990+Nenotriple@users.noreply.github.com> Date: Thu, 11 Jan 2024 03:33:11 -0800 Subject: [PATCH 2/6] Update batch_tag_delete.py - v1.06 - Fixed: - Fixed tag list refreshing twice - Fixed Multi-tag delete when "batch_tag_delete" is ran from "img-txt_viewer" --- batch_tag_delete.py | 88 +++++++++++++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 26 deletions(-) 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 From 2ef204ea34ea5bef123bbf90116ee01a40cb4cd9 Mon Sep 17 00:00:00 2001 From: Nenotriple <70049990+Nenotriple@users.noreply.github.com> Date: Thu, 11 Jan 2024 03:34:37 -0800 Subject: [PATCH 3/6] Create resize_images.py 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 --- resize_images.py | 386 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 resize_images.py diff --git a/resize_images.py b/resize_images.py new file mode 100644 index 0000000..184453a --- /dev/null +++ b/resize_images.py @@ -0,0 +1,386 @@ +""" +######################################## +# # +# 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 + + +#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) + myappid = 'ImgTxtViewer.Nenotriple' + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) + 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 From be4487ce445fe393a460506abe6e5cdfb179cdca Mon Sep 17 00:00:00 2001 From: Nenotriple <70049990+Nenotriple@users.noreply.github.com> Date: Thu, 11 Jan 2024 03:37:49 -0800 Subject: [PATCH 4/6] Update README.md --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5211142..78b4df3 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,21 @@ 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. From d261dbc909fd1cd75b1609b34dbbdcc951d8e5a9 Mon Sep 17 00:00:00 2001 From: Nenotriple <70049990+Nenotriple@users.noreply.github.com> Date: Thu, 11 Jan 2024 03:49:30 -0800 Subject: [PATCH 5/6] Update resize_images.py - Properly set icon/appid --- resize_images.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resize_images.py b/resize_images.py index 184453a..2cdf291 100644 --- a/resize_images.py +++ b/resize_images.py @@ -17,6 +17,7 @@ """ + ################################################################################################################################################ #region - Imports @@ -30,6 +31,8 @@ from tkinter.scrolledtext import ScrolledText from PIL import Image +myappid = 'ImgTxtViewer.Nenotriple' +ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) #endregion ################################################################################################################################################ @@ -348,8 +351,6 @@ def setup_root(): root.maxsize(2000,500) root.update_idletasks() set_icon(root) - myappid = 'ImgTxtViewer.Nenotriple' - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) return root From e5dd5509e7a446de2595bfe84257a11781360b31 Mon Sep 17 00:00:00 2001 From: Nenotriple <70049990+Nenotriple@users.noreply.github.com> Date: Thu, 11 Jan 2024 03:51:10 -0800 Subject: [PATCH 6/6] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 78b4df3..f309537 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,12 @@ The `pillow` library will be downloaded and installed *(if not already available - Other 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"