├── .gitignore ├── Diffusion-Browser.desktop ├── Embedders ├── README.md ├── convertfooocus_2_1_852.py ├── convertfooocus_2_1_865.py ├── convertfooocus_2_2_1.py ├── convertfooocus_older.py ├── convertinvokeai.py └── convertsdwebui.py ├── Images ├── DiffusionBrowser_config.png ├── DiffusionBrowser_image.png ├── DiffusionBrowser_main_interface.png ├── DiffusionBrowser_paths.png ├── Logo.png └── LogoGitHub.png ├── Projects └── Default │ ├── difbrowser.ini │ └── parameters.txt ├── README.md ├── difbrow.pyw ├── linux-install.sh └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | -------------------------------------------------------------------------------- /Diffusion-Browser.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Comment[en_US]= 3 | Comment= 4 | Exec=python $HOME/Diffusion-Browser/difbrow.pyw 5 | GenericName[en_US]= 6 | GenericName= 7 | MimeType= 8 | Name[en_US]=DiffusionBrowser 9 | Name=DiffusionBrowser 10 | NoDisplay=false 11 | Path=~/Diffusion-Browser/ 12 | StartupNotify=true 13 | Terminal=true 14 | TerminalOptions=python difbrow.pyw 15 | Type=Application 16 | X-DBUS-ServiceName= 17 | X-DBUS-StartupType= 18 | X-KDE-SubstituteUID=false 19 | -------------------------------------------------------------------------------- /Embedders/README.md: -------------------------------------------------------------------------------- 1 | # Diffusion Browser 2 | ## Embedders 3 | 4 | These programs will embed the information found on their external files into the images. 5 | 6 | NOTE: The only tested recently were the Fooocus ones, the others are here for legacy purpose. 7 | 8 | ### Fooocus 9 | Fooocus stored the information on a HTML file. They changed the file structure a number of times. These scripts will help embed the information into the images. 10 | Change the **input** and **output** paths directrly into the script. 11 | The version number is the last know version to work with a particular embedder. 12 | After 2.2.1, Fooocus has an option to embed the information directly on the image. 13 | 14 | [convertfooocus_2_2_1.py](convertfooocus_2_2_1.py) 15 | [convertfooocus_2_1_865.py](convertfooocus_2_1_865.py) 16 | [convertfooocus_2_1_852.py](convertfooocus_2_1_852.py) 17 | [convertfooocus_older.py](convertfooocus_older.py) 18 | 19 | ### Invoke-AI and SD-WebUI 20 | These are very old, you are on your own. They are here nonetheless. 21 | 22 | [convertinvokeai.py](convertinvokeai.py) 23 | [convertsdwebui.py](convertsdwebui.py) 24 | -------------------------------------------------------------------------------- /Embedders/convertfooocus_2_1_852.py: -------------------------------------------------------------------------------- 1 | # Read log.html files from the Fooocus repository 2 | # and embedded into the PNG image chunk info 3 | 4 | import os 5 | import json 6 | import glob 7 | import time 8 | import datetime 9 | from collections import OrderedDict 10 | from PIL import Image, PngImagePlugin 11 | 12 | IN_FOLDER = 'D:\\Fooocus_win64_2-1-791\\Fooocus\\teste' 13 | OUT_FOLDER = 'D:\\Fooocus_win64_2-1-791\\Converted' 14 | 15 | # Parameters with "( )" to remove 16 | SETS = ('styles', 'resolution', 'adm guidance') 17 | 18 | # Gel all subfolders from output folder 19 | folders = glob.glob(IN_FOLDER + '\\*\\', recursive=True) 20 | 21 | 22 | def process_html(html): 23 | def get_key_value(line): 24 | try: 25 | key_start = line.index("class='key'>") 26 | key_end = line.index('") 28 | value_end = line.index('') 29 | except ValueError: 30 | return None, None 31 | 32 | key = line[key_start + 12:key_end].lower() 33 | value = line[value_start + 14:value_end].lower() 34 | 35 | if key in SETS: 36 | value = value[1:-1] 37 | if key == 'resolution': 38 | value = value.replace(', ', ' x ') 39 | if key[:4] == 'lora': 40 | key = (f'lora name {lora_numb}', f'lora weight {lora_numb}') 41 | lora_value = value.split(' : ') 42 | value = (lora_value[0], lora_value[1]) 43 | 44 | return key, value 45 | 46 | images_dict = {} 47 | html_list = html.split('\n\n')[1:-1] 48 | 49 | print('HTML data') 50 | print() 51 | 52 | for block in html_list: 53 | block_list = block.split('\n') 54 | parameters_dict = OrderedDict() 55 | lora_numb = 1 56 | 57 | for line in block_list: 58 | if line.startswith(' {line[start:end]}') 62 | image = line[start:end] 63 | 64 | key, value = get_key_value(line) 65 | 66 | if type(key) is tuple: 67 | print(f'{key[0]}: {value[0]}') 68 | print(f'{key[1]}: {value[1]}') 69 | parameters_dict[key[0]] = value[0] 70 | parameters_dict[key[1]] = value[1] 71 | lora_numb += 1 72 | elif key: 73 | print(f'{key}: {value}') 74 | parameters_dict[key] = value 75 | 76 | print() 77 | images_dict[image] = parameters_dict 78 | 79 | return images_dict 80 | 81 | 82 | # Gett all PNG file names in the subfolders 83 | folder_files = [] 84 | for folder in folders: 85 | pngs = glob.glob(folder + '*.png', recursive=True) 86 | folder_files.append([folder, pngs]) 87 | 88 | # Process HTML files on each folder 89 | folders_dict = {} 90 | 91 | for folder in folder_files: 92 | log_file = folder[0] + '\\log.html' 93 | try: 94 | with open(log_file) as file: 95 | html = file.read() 96 | except IOError: 97 | print(f'*** log.html not found on folder: {folder[0]}') 98 | continue 99 | 100 | # Parse HTML and create folder dictionary 101 | images_dict = process_html(html) 102 | 103 | # Add to the main dictionary 104 | folders_dict[folder[0]] = images_dict 105 | 106 | 107 | # Copying image with new embedded text 108 | for i, folder in enumerate(folders_dict, 1): 109 | print('Compiled data') 110 | print() 111 | print('Folder') 112 | print(folder) 113 | print() 114 | 115 | for n, file in enumerate(folders_dict[folder], 1): 116 | file_w_path = os.path.join(folder, file) 117 | 118 | # Open file 119 | try: 120 | im = Image.open(file_w_path) 121 | except FileNotFoundError: 122 | print(f'*** Image not found: {file_w_path}') 123 | continue 124 | 125 | # Show file name and original embedded information 126 | print(f'-> {file}') 127 | 128 | # Get modification date from original file 129 | date = os.path.getmtime(file_w_path) 130 | date = datetime.datetime.fromtimestamp(date) 131 | modTime = time.mktime(date.timetuple()) 132 | 133 | # Prepare information to embed 134 | info = PngImagePlugin.PngInfo() 135 | info.add_text('Fooocus', json.dumps(folders_dict[folder][file])) 136 | 137 | print('- Prepared data') 138 | print(json.dumps(folders_dict[folder][file])) 139 | 140 | # Save file 141 | file_name = os.path.basename(file) 142 | save_file = os.path.join(OUT_FOLDER, file) 143 | im.save(save_file, 'PNG', pnginfo=info) 144 | im3 = Image.open(save_file) 145 | 146 | # Apply original date to new file 147 | os.utime(save_file, (modTime, modTime)) 148 | 149 | # Show new embedded information 150 | print('- Embedded data') 151 | print(im3.text) 152 | print() 153 | -------------------------------------------------------------------------------- /Embedders/convertfooocus_2_1_865.py: -------------------------------------------------------------------------------- 1 | # Read log.html files from the Fooocus repository 2 | # and embedded into the PNG image chunk info 3 | 4 | import os 5 | import json 6 | import glob 7 | import time 8 | import datetime 9 | from collections import OrderedDict 10 | from PIL import Image, PngImagePlugin 11 | 12 | 13 | IN_FOLDER = 'D:\\Fooocus_win64_2-1-791\\Fooocus\\outputs' 14 | OUT_FOLDER = 'D:\\Fooocus_win64_2-1-791\\Converted' 15 | # Gel all subfolders from output folder 16 | FOLDERS = glob.glob(IN_FOLDER + '\\*\\', recursive=True) 17 | 18 | # Parameters with "( )" to remove 19 | SETS = ('styles', 'resolution', 'adm guidance') 20 | 21 | 22 | def process_html(html): 23 | def get_key_value(line): 24 | try: 25 | key_start = line.index("class='key'>") 26 | key_end = line.index('") 28 | value_end = line.index('') 29 | except ValueError: 30 | return None, None 31 | 32 | key = line[key_start + 12:key_end].lower() 33 | value = line[value_start + 14:value_end].lower() 34 | 35 | if key in SETS: 36 | value = value[1:-1] 37 | if key == 'resolution': 38 | value = value.replace(', ', ' x ') 39 | if key[:4] == 'lora': 40 | key = (f'lora name {lora_numb}', f'lora weight {lora_numb}') 41 | lora_value = value.split(' : ') 42 | value = (lora_value[0], lora_value[1]) 43 | 44 | return key, value 45 | 46 | images_dict = {} 47 | html_list = html.split('\n\n')[2:-1] 48 | 49 | print('HTML data') 50 | print() 51 | 52 | for block in html_list: 53 | block_list = block.split('\n') 54 | parameters_dict = OrderedDict() 55 | lora_numb = 1 56 | 57 | for line in block_list: 58 | if line.startswith(' {line[start:end]}') 62 | image = line[start:end] 63 | 64 | key, value = get_key_value(line) 65 | 66 | if type(key) is tuple: 67 | print(f'{key[0]}: {value[0]}') 68 | print(f'{key[1]}: {value[1]}') 69 | parameters_dict[key[0]] = value[0] 70 | parameters_dict[key[1]] = value[1] 71 | lora_numb += 1 72 | elif key: 73 | print(f'{key}: {value}') 74 | parameters_dict[key] = value 75 | 76 | print() 77 | images_dict[image] = parameters_dict 78 | 79 | return images_dict 80 | 81 | 82 | def process_folders(): 83 | # Gett all PNG file names in the subfolders 84 | folders_dict = {} 85 | 86 | for folder in FOLDERS: 87 | log_file = os.path.join(folder, 'log.html') 88 | try: 89 | with open(log_file) as file: 90 | html = file.read() 91 | except IOError: 92 | print(f'*** log.html not found on folder: {folder}') 93 | continue 94 | 95 | # Parse HTML and create folder dictionary 96 | images_dict = process_html(html) 97 | 98 | # Add to the main dictionary 99 | folders_dict[folder] = images_dict 100 | 101 | return folders_dict 102 | 103 | 104 | def process_images(folders_dict): 105 | # Copying image with new embedded text 106 | for folder in folders_dict: 107 | print('Compiled data') 108 | print() 109 | print('Folder') 110 | print(folder) 111 | print() 112 | 113 | for file in folders_dict[folder]: 114 | file_w_path = os.path.join(folder, file) 115 | 116 | # Open file 117 | try: 118 | im = Image.open(file_w_path) 119 | except FileNotFoundError: 120 | print(f'*** Image not found: {file_w_path}') 121 | continue 122 | 123 | # Show file name and original embedded information 124 | print(f'-> {file}') 125 | 126 | # Get modification date from original file 127 | date = os.path.getmtime(file_w_path) 128 | date = datetime.datetime.fromtimestamp(date) 129 | modTime = time.mktime(date.timetuple()) 130 | 131 | # Prepare information to embed 132 | info = PngImagePlugin.PngInfo() 133 | info.add_text('Fooocus', json.dumps(folders_dict[folder][file])) 134 | 135 | print('- Prepared data') 136 | print(json.dumps(folders_dict[folder][file])) 137 | 138 | # Save file 139 | save_file = os.path.join(OUT_FOLDER, file) 140 | im.save(save_file, 'PNG', pnginfo=info) 141 | im3 = Image.open(save_file) 142 | 143 | # Apply original date to new file 144 | os.utime(save_file, (modTime, modTime)) 145 | 146 | # Show new embedded information 147 | print('- Embedded data') 148 | print(im3.text) 149 | print() 150 | 151 | 152 | def main(): 153 | process_images(process_folders()) 154 | 155 | 156 | if __name__ == '__main__': 157 | main() 158 | -------------------------------------------------------------------------------- /Embedders/convertfooocus_2_2_1.py: -------------------------------------------------------------------------------- 1 | # Read log.html files from the Fooocus repository 2 | # and embedded into the PNG image chunk info 3 | 4 | import os 5 | import json 6 | import glob 7 | import time 8 | import datetime 9 | from collections import OrderedDict 10 | from PIL import Image, PngImagePlugin 11 | 12 | 13 | IN_FOLDER = 'D:\\AI\\Fooocus_win64_2-1-791\\Fooocus\\outputs' 14 | OUT_FOLDER = 'D:\\AI\\Fooocus_win64_2-1-791\\Converted' 15 | # Gel all subfolders from output folder 16 | FOLDERS = glob.glob(IN_FOLDER + '\\*\\', recursive=True) 17 | 18 | # Parameters with "( )" to remove 19 | SETS = ('styles', 'resolution', 'adm guidance') 20 | 21 | 22 | def process_html(html): 23 | def get_key_value(line): 24 | try: 25 | key_start = line.index("class='label'>") 26 | key_end = line.index('") 28 | value_end = line.index('') 29 | except ValueError: 30 | return None, None 31 | 32 | key = line[key_start + 14:key_end].lower() 33 | value = line[value_start + 14:value_end].lower() 34 | 35 | if key in SETS: 36 | value = value[1:-1] 37 | if key == 'resolution': 38 | value = value.replace(', ', ' x ') 39 | if key[:4] == 'lora': 40 | key = (f'lora name {lora_numb}', f'lora weight {lora_numb}') 41 | lora_value = value.split(' : ') 42 | value = (lora_value[0], lora_value[1]) 43 | 44 | return key, value 45 | 46 | images_dict = {} 47 | html_list = html.split('\n\n')[2:-1] 48 | 49 | print('HTML data') 50 | print() 51 | 52 | for block in html_list: 53 | block_list = block.split('\n') 54 | parameters_dict = OrderedDict() 55 | lora_numb = 1 56 | 57 | for line in block_list: 58 | if line.startswith(' {line[start:end]}') 62 | image = line[start:end] 63 | 64 | key, value = get_key_value(line) 65 | 66 | if type(key) is tuple: 67 | print(f'{key[0]}: {value[0]}') 68 | print(f'{key[1]}: {value[1]}') 69 | parameters_dict[key[0]] = value[0] 70 | parameters_dict[key[1]] = value[1] 71 | lora_numb += 1 72 | elif key: 73 | print(f'{key}: {value}') 74 | parameters_dict[key] = value 75 | 76 | print() 77 | images_dict[image] = parameters_dict 78 | 79 | return images_dict 80 | 81 | 82 | def process_folders(): 83 | # Gett all PNG file names in the subfolders 84 | folders_dict = {} 85 | 86 | for folder in FOLDERS: 87 | log_file = os.path.join(folder, 'log.html') 88 | try: 89 | with open(log_file) as file: 90 | html = file.read() 91 | except IOError: 92 | print(f'*** log.html not found on folder: {folder}') 93 | continue 94 | 95 | # Parse HTML and create folder dictionary 96 | images_dict = process_html(html) 97 | 98 | # Add to the main dictionary 99 | folders_dict[folder] = images_dict 100 | 101 | return folders_dict 102 | 103 | 104 | def process_images(folders_dict): 105 | # Copying image with new embedded text 106 | for folder in folders_dict: 107 | print('Compiled data') 108 | print() 109 | print('Folder') 110 | print(folder) 111 | print() 112 | 113 | for file in folders_dict[folder]: 114 | file_w_path = os.path.join(folder, file) 115 | 116 | # Open file 117 | try: 118 | im = Image.open(file_w_path) 119 | except FileNotFoundError: 120 | print(f'*** Image not found: {file_w_path}') 121 | continue 122 | 123 | # Show file name and original embedded information 124 | print(f'-> {file}') 125 | 126 | # Get modification date from original file 127 | date = os.path.getmtime(file_w_path) 128 | date = datetime.datetime.fromtimestamp(date) 129 | modTime = time.mktime(date.timetuple()) 130 | 131 | # Prepare information to embed 132 | info = PngImagePlugin.PngInfo() 133 | info.add_text('Fooocus', json.dumps(folders_dict[folder][file])) 134 | 135 | print('- Prepared data') 136 | print(json.dumps(folders_dict[folder][file])) 137 | 138 | # Save file 139 | save_file = os.path.join(OUT_FOLDER, file) 140 | im.save(save_file, 'PNG', pnginfo=info) 141 | im3 = Image.open(save_file) 142 | 143 | # Apply original date to new file 144 | os.utime(save_file, (modTime, modTime)) 145 | 146 | # Show new embedded information 147 | print('- Embedded data') 148 | print(im3.text) 149 | print() 150 | 151 | 152 | def main(): 153 | process_images(process_folders()) 154 | 155 | 156 | if __name__ == '__main__': 157 | main() 158 | -------------------------------------------------------------------------------- /Embedders/convertfooocus_older.py: -------------------------------------------------------------------------------- 1 | # Read log.html files from the Fooocus repository 2 | # and embedded into the PNG image chunk info 3 | 4 | import os 5 | import json 6 | import glob 7 | import time 8 | import datetime 9 | from collections import OrderedDict 10 | from html.parser import HTMLParser 11 | from PIL import Image, PngImagePlugin 12 | 13 | SETS = ('styles', 'resolution', 'adm guidance') 14 | 15 | parameters_dict = OrderedDict() 16 | folders_dict = {} 17 | 18 | IN_FOLDER = 'D:\\Fooocus_win64_2-1-791\\Fooocus\\outputs' 19 | OUT_FOLDER = 'D:\\Fooocus_win64_2-1-791\\Converted' 20 | 21 | # Gel all subfolders from output folder 22 | folders = glob.glob(IN_FOLDER + '\\*\\', recursive=True) 23 | 24 | 25 | # Create a subclass and override the handler methods for the HTML parser 26 | class MyHTMLParser(HTMLParser): 27 | 28 | def __init__(self): 29 | super().__init__() 30 | self.newimage = '' 31 | self.attr_name = '' 32 | self.lora_numb = 1 33 | 34 | # Get image name from the DIV tag. Each DIV has one image parameters 35 | def handle_starttag(self, tag, attrs): 36 | if tag == 'div': 37 | self.newimage = attrs[0][1][:-4] + '.png' 38 | self.lora_numb = 1 39 | 40 | # If DIV tag is closed, add parameters dict to the folder dict 41 | def handle_endtag(self, tag): 42 | global parameters_dict 43 | if tag == 'div': 44 | folders_dict[self.newimage] = parameters_dict 45 | parameters_dict = {} 46 | 47 | # Get data from HTML 48 | def handle_data(self, data): 49 | if data.strip() == '' and self.attr_name != '': 50 | self.get_data(self.attr_name, data) 51 | 52 | if data.strip() != '': 53 | if data.strip()[-1] == ':': 54 | self.attr_name = data 55 | else: 56 | self.get_data(self.attr_name, data) 57 | 58 | # Custom method, add parameter to parameters dictionary 59 | def get_data(self, name, data): 60 | name = name.strip(', ').strip(':').lower() 61 | data = data.strip(', ') 62 | if name in SETS: 63 | data = data[1:-1] 64 | if name == 'resolution': 65 | data = data.replace(', ', ' x ') 66 | if name[:4] == 'lora': 67 | lora = name[6:-9] 68 | parameters_dict[f'lora {self.lora_numb}'] = lora 69 | name = f'weight {self.lora_numb}' 70 | self.lora_numb += 1 71 | if name: 72 | parameters_dict[name] = data 73 | self.attr_name = '' 74 | 75 | 76 | parser = MyHTMLParser() 77 | 78 | # Gett all PNG file names in the subfolders 79 | files = [] 80 | for folder in folders: 81 | pngs = glob.glob(folder + '*.png', recursive=True) 82 | files.append([folder, pngs]) 83 | 84 | # Process HTML files on each folder 85 | images_dict = {} 86 | for folder in files: 87 | log_file = folder[0] + '\\log.html' 88 | try: 89 | with open(log_file) as file: 90 | html = file.read() 91 | except IOError: 92 | print(f'log.html not found on folder: {folder[0]}') 93 | continue 94 | 95 | # Parse HTML and create folder dictionary 96 | parser.feed(html) 97 | 98 | # Add to the main dictionary 99 | images_dict[folder[0]] = folders_dict 100 | 101 | folders_dict = {} 102 | 103 | # images_dict[folder[0]] = parameters_dict 104 | 105 | # for i, folder in enumerate(images_dict): 106 | # print(i, folder) 107 | # for n, file in enumerate(images_dict[folder]): 108 | # print(n, os.path.join(folder, file)) 109 | # print(json.dumps(images_dict[folder][file])) 110 | # print() 111 | # print() 112 | # print() 113 | # print() 114 | 115 | # raise SystemExit(0) 116 | 117 | # Copying image with new embedded text 118 | for i, folder in enumerate(images_dict, 1): 119 | print() 120 | print(folder) 121 | for n, file in enumerate(images_dict[folder], 1): 122 | file_w_path = os.path.join(folder, file) 123 | 124 | # Open file 125 | try: 126 | im = Image.open(file_w_path) 127 | except FileNotFoundError: 128 | print('*** Image not found:', file_w_path) 129 | continue 130 | 131 | # Show file name and original embedded information 132 | print(file) 133 | 134 | # Get modification date from original file 135 | date = os.path.getmtime(file_w_path) 136 | date = datetime.datetime.fromtimestamp(date) 137 | modTime = time.mktime(date.timetuple()) 138 | 139 | # Prepare information to embed 140 | info = PngImagePlugin.PngInfo() 141 | info.add_text('Fooocus', json.dumps(images_dict[folder][file])) 142 | 143 | print(json.dumps(images_dict[folder][file])) 144 | 145 | # Save file 146 | file_name = os.path.basename(file) 147 | save_file = os.path.join(OUT_FOLDER, file) 148 | im.save(save_file, 'PNG', pnginfo=info) 149 | im3 = Image.open(save_file) 150 | 151 | # Apply original date to new file 152 | os.utime(save_file, (modTime, modTime)) 153 | 154 | # Show new embedded information 155 | print(im3.text) 156 | print() 157 | -------------------------------------------------------------------------------- /Embedders/convertinvokeai.py: -------------------------------------------------------------------------------- 1 | # Read log files from invoke-ai Stable Diffusion repository 2 | # and embedded into PNG image chunk info 3 | 4 | import os 5 | import json 6 | import glob 7 | import time 8 | import datetime 9 | from PIL import Image, PngImagePlugin 10 | 11 | CMD_LOG = 'D:/SD-Backups/img-samples/dream_log.txt' 12 | WEB_LOG = 'D:/SD-Backups/img-samples/dream_web_log.txt' 13 | 14 | CDM_KEYS = {'-s': 'steps', 15 | '-W': 'width', 16 | '-H': 'height', 17 | '-C': 'cfgscale', 18 | '-A': 'sampler', 19 | '-S': 'seed', 20 | '-ID:': 'initimg', 21 | '-f': 'strength', 22 | '--grid': 'grid', 23 | '-N': 'batch'} 24 | 25 | IN_FOLDER = 'D:/SD-Backups/img-samples' 26 | OUT_FOLDER = 'D:/Stable Diffusion Auto1111/stable-diffusion-webui/outputs/frominvokeai2' 27 | 28 | # Get image information from command line log file 29 | try: 30 | with open(CMD_LOG) as file: 31 | log = file.readlines() 32 | except IOError: 33 | log = ['Log file not found\n'] 34 | raise SystemExit(0) 35 | 36 | # Process image information 37 | img_dict = {} 38 | for line in log: 39 | img_param = {} 40 | image_file = line.split(':')[0] 41 | image_file = os.path.basename(image_file) 42 | prompt = line.split('"')[1] 43 | arguments = line.split('"')[2] 44 | arguments = arguments.strip().split(' ') 45 | img_param['prompt'] = prompt 46 | for a in arguments: 47 | for k in CDM_KEYS: 48 | if a.startswith(k): 49 | key = CDM_KEYS[k] 50 | content = a[len(k):] 51 | if k == '-ID:': 52 | content = os.path.basename(content) 53 | img_param[key] = content 54 | img_dict[image_file] = (img_param, 'invoke-ai command line') 55 | 56 | # Get image information from web interface log file 57 | try: 58 | with open(WEB_LOG) as file: 59 | log = file.readlines() 60 | except IOError: 61 | log = ['Log file not found\n'] 62 | raise SystemExit(0) 63 | 64 | # Process image information 65 | for line in log: 66 | img_param = {} 67 | image_file = line.split(':')[0] 68 | image_file = os.path.basename(image_file) 69 | line_split = line.find(':') + 1 70 | line_dict = line[line_split:] 71 | line_dict = json.loads(line_dict) 72 | for k in line_dict: 73 | content = line_dict[k] 74 | if k == 'seed' and line_dict[k] == '-1': 75 | content = image_file.split('.')[1] 76 | img_param[k] = content.lower() 77 | img_dict[image_file] = (img_param, 'invoke-ai web interface') 78 | 79 | # Show processed information 80 | for img in img_dict: 81 | print(img) 82 | print(img_dict[img]) 83 | print() 84 | 85 | # Get and show file list 86 | folder_list = glob.glob(f'{IN_FOLDER}/*.png') 87 | print(folder_list) 88 | 89 | # Copy images with new embedded text 90 | for n, file in enumerate(folder_list, 1): 91 | 92 | # Open file 93 | im = Image.open(file) 94 | 95 | # Get modification date from original file 96 | date = os.path.getmtime(file) 97 | date = datetime.datetime.fromtimestamp(date) 98 | modTime = time.mktime(date.timetuple()) 99 | 100 | # Show file name and original embedded information 101 | file_name = os.path.basename(file) 102 | print(n) 103 | print(file_name) 104 | print(im.text) 105 | 106 | # Prepare information to embed 107 | info = PngImagePlugin.PngInfo() 108 | info.add_text(img_dict[file_name][1], json.dumps(img_dict[file_name][0])) 109 | 110 | # Save file 111 | save_file = os.path.join(OUT_FOLDER, file_name) 112 | im.save(save_file, 'PNG', pnginfo=info) 113 | im3 = Image.open(save_file) 114 | 115 | # Apply original date to new file 116 | os.utime(save_file, (modTime, modTime)) 117 | 118 | # Show new embedded information 119 | print(im3.text) 120 | print() 121 | -------------------------------------------------------------------------------- /Embedders/convertsdwebui.py: -------------------------------------------------------------------------------- 1 | # Read yaml files from sd-webui Stable Diffusion repository 2 | # and embedded into PNG image chunk info 3 | 4 | import os 5 | import json 6 | import glob 7 | import time 8 | import datetime 9 | from PIL import Image, PngImagePlugin 10 | import collections 11 | 12 | IN_FOLDER = 'D:/Stable Diffusion WebUI/stable-diffusion-webui/outputs' 13 | OUT_FOLDER = 'D:/Stable Diffusion Auto1111/stable-diffusion-webui/outputs/fromsdwebui' 14 | 15 | # Gel all files from folder and subfoldres 16 | files = glob.glob(IN_FOLDER + '/**/*.png', recursive=True) 17 | 18 | # Get yaml files associated with images 19 | img_dict = {} 20 | for n, image_path in enumerate(files): 21 | img_param = {} 22 | infotxt = image_path[:-3] + 'yaml' 23 | try: 24 | with open(infotxt) as file: 25 | yaml = file.readlines() 26 | except IOError: 27 | print(f'Yaml file not found: {infotxt}') 28 | yaml = {} 29 | 30 | # Process yamls files 31 | img_param = {} 32 | toggles = '' 33 | for n, line in enumerate(yaml): 34 | tag = line.split(' ')[0].strip() 35 | content = line[len(tag):].strip() 36 | tag = tag.rstrip(':') 37 | if tag == 'toggles' or tag == '-': 38 | if yaml[n + 1].startswith('-'): 39 | toggles += f'{yaml[n + 1][2:].strip()} ' 40 | continue 41 | if toggles: 42 | img_param['toggles'] = toggles.strip() 43 | toggles = '' 44 | continue 45 | img_param[tag] = content 46 | 47 | img_dict[image_path] = img_param 48 | 49 | print(img_dict) 50 | 51 | # Check for duplicated files 52 | file_check = [] 53 | for key in img_dict: 54 | filename = os.path.basename(key) 55 | file_check.append(filename) 56 | print(filename) 57 | 58 | if len(file_check) == len(set(file_check)): 59 | print('No duplicated file names') 60 | else: 61 | print('Duplicates', len(file_check), len(set(file_check)), len(file_check) - len(set(file_check))) 62 | print([item for item, count in collections.Counter(file_check).items() if count > 1]) 63 | 64 | # raise SystemExit(0) 65 | 66 | # Copying image with new embedded text 67 | for n, file in enumerate(files, 1): 68 | # Open file 69 | im = Image.open(file) 70 | 71 | # Show file name and original embedded information 72 | print(n) 73 | print(file) 74 | print(im.text) 75 | 76 | # Get modification date from original file 77 | date = os.path.getmtime(file) 78 | date = datetime.datetime.fromtimestamp(date) 79 | modTime = time.mktime(date.timetuple()) 80 | 81 | # Prepare information to embed 82 | info = PngImagePlugin.PngInfo() 83 | info.add_text('sd-webui', json.dumps(img_dict[file])) 84 | 85 | # Save file 86 | file_name = os.path.basename(file) 87 | save_file = os.path.join(OUT_FOLDER, file_name) 88 | im.save(save_file, 'PNG', pnginfo=info) 89 | im3 = Image.open(save_file) 90 | 91 | # Apply original date to new file 92 | os.utime(save_file, (modTime, modTime)) 93 | 94 | # Show new embedded information 95 | print(im3.text) 96 | print() 97 | -------------------------------------------------------------------------------- /Images/DiffusionBrowser_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farique1/diffusion-browser/f6ebff6140d5fc9d2c3982ebe50d203c8594983b/Images/DiffusionBrowser_config.png -------------------------------------------------------------------------------- /Images/DiffusionBrowser_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farique1/diffusion-browser/f6ebff6140d5fc9d2c3982ebe50d203c8594983b/Images/DiffusionBrowser_image.png -------------------------------------------------------------------------------- /Images/DiffusionBrowser_main_interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farique1/diffusion-browser/f6ebff6140d5fc9d2c3982ebe50d203c8594983b/Images/DiffusionBrowser_main_interface.png -------------------------------------------------------------------------------- /Images/DiffusionBrowser_paths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farique1/diffusion-browser/f6ebff6140d5fc9d2c3982ebe50d203c8594983b/Images/DiffusionBrowser_paths.png -------------------------------------------------------------------------------- /Images/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farique1/diffusion-browser/f6ebff6140d5fc9d2c3982ebe50d203c8594983b/Images/Logo.png -------------------------------------------------------------------------------- /Images/LogoGitHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farique1/diffusion-browser/f6ebff6140d5fc9d2c3982ebe50d203c8594983b/Images/LogoGitHub.png -------------------------------------------------------------------------------- /Projects/Default/difbrowser.ini: -------------------------------------------------------------------------------- 1 | [CONFIGS] 2 | number_of_columns = 9 3 | number_of_lines = 9 4 | grid_image_size = 75 5 | preview_image_size = 300 6 | button_height = 25 7 | font_name = Tahoma 8 | font_size = 10 9 | font_weight = normal 10 | background_color = #000033 11 | main_color = Teal 12 | accent_color_1 = goldenrod 13 | accent_color_2 = gray70 14 | alert_color = dark red 15 | 16 | -------------------------------------------------------------------------------- /Projects/Default/parameters.txt: -------------------------------------------------------------------------------- 1 | embedded info 2 | raw prompt 3 | raw negative prompt 4 | prompt 5 | negative prompt 6 | negative_prompt 7 | seed 8 | sampler 9 | sampler_name 10 | steps 11 | ddim_steps 12 | ddim_eta 13 | cfg scale 14 | cfg_scale 15 | cfgscale 16 | width 17 | height 18 | size 19 | initimg 20 | strength 21 | denoising strength 22 | denoising_strength 23 | first pass size 24 | mask blur 25 | n_iter 26 | iterations 27 | batch_size 28 | batch size 29 | batch pos 30 | grid 31 | batch 32 | fit 33 | progress_images 34 | toggles 35 | resize_mode 36 | gfpgan_strength 37 | upscale_level 38 | upscale_strength 39 | target 40 | model 41 | model hash 42 | clip skip 43 | model 1 44 | upscale 1 45 | visibility 1 46 | model 2 47 | upscale 2 48 | visibility 2 49 | version 50 | fooocus v2 expansion 51 | styles 52 | performance 53 | resolution 54 | sharpness 55 | guidance scale 56 | adm guidance 57 | base model 58 | refiner model 59 | refiner switch 60 | scheduler 61 | lora 1 62 | weight 1 63 | lora 2 64 | weight 2 65 | lora 3 66 | weight 3 67 | lora 4 68 | weight 4 69 | lora 5 70 | weight 5 71 | lora weight 1 72 | lora name 1 73 | lora weight 2 74 | lora name 2 75 | clipPoints 76 | metadata scheme 77 | lora_combined_1 78 | refiner_switch 79 | loras 80 | prompt_expansion 81 | adm_guidance 82 | full_negative_prompt 83 | full_prompt 84 | guidance_scale 85 | base_model_hash 86 | base_model 87 | refiner_model 88 | created_by 89 | user 90 | lora hashes 91 | metadata_scheme 92 | 93 | source 94 | real_size 95 | format 96 | created 97 | path -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Diffusion Browser 2 | 3 | # Diffusion Browser 4 | **v3.1** 5 | An easy way to view embedded image metadata of some AI generators. 6 | 7 | **Smarter** 8 | **Diffusion Browser** now builds a database with all images information and saves it for later use. 9 | 10 | **Faster** 11 | The images are loaded on the fly when the program is first opened and on a few other occasions. The loading is threaded for faster response. 12 | > Still working to make images load even faster. 13 | 14 | **More versatile** 15 | You can set several folders where to look for images and set subfolder lookup individually. 16 | 17 | **Planned** 18 | Allow loading and saving multiple projects 19 | Allow for multiple projects with different configurations 20 | Add hearts to the images 21 | Add stars to the images 22 | Add tags to the images 23 | Create image sets 24 | Mute folders to hide them without having to rebuild the database 25 | Manage the parameters list inside the program 26 | Refresh the parameters without having to rebuilding the database 27 | Allow the consoluidation of multiple parameters into a single label 28 | Image viewer improvements with next/previous buttons and more 29 | Show all data available for a given parameter 30 | Find images and select format by their content not their extensions 31 | Undock and dock the image and inforormation viewers 32 | Make the metadata reading functions more independent to facilitate future implementations 33 | More 34 | 35 | [**Changes**](#Changes). 36 | 37 | > On [Embedders](Embedders/README.md) there are scripts to embed information from some generators that do not embed them. 38 | Four versions of **Fooocus** embedders and probably outdated versions of **invoke-ai** and **sd-webui**. 39 | 40 | >**Diffusion Browser** uses the **[Pillow](https://pillow.readthedocs.io/en/stable/)** module. 41 | 42 | ## Main Interface 43 | ![# Interface](Images/DiffusionBrowser_main_interface.png) 44 | (From top left, clockwise): 45 | 46 | - *Number of images found.* 47 | 48 | - *Image paths pull down box.* 49 | Paths to look for for the images. 50 | Click to chose an specific path to view. 51 | "all paths" will look into all available paths. 52 | 53 | - *refresh button.* 54 | Refresh the grid to sync added/deleted images on the configured paths. 55 | New images will be sorted according to their timestamps. 56 | 57 | - *open path button.* 58 | Open the file explorer pointing to the currently selected path. 59 | 60 | - *paths button.* 61 | Opens the path requester. 62 | [**More below**](#Paths-requester). 63 | 64 | - *config button.* 65 | Change Diffusion Browser's settings. 66 | [**More below**](#Configuration). 67 | 68 | - *Image preview.* 69 | Display the currently selected image. 70 | `Click` to open a [window with the full sized image](#Full-sized-image-view). 71 | `Shift+click` or `right click` to open the image on the default system viewer. 72 | 73 | - *Information viewer.* 74 | Display the embedded image information along with some extra content. 75 | [**More below**](#The-information-viewer). 76 | 77 | - *save info button.* 78 | Save the information of the info window to a text file with the same name as the image on the same folder. 79 | `Right click` to chose a different name. 80 | 81 | - *show in folder button.* 82 | Open the file explorer pointing to the currently selected image. 83 | 84 | - *overlay button* 85 | Overlay the parameter chosen on the parameter box to the grid images. 86 | `all parameters` or an empty box will clear the overlay. 87 | `Right click` or `shift+click` will also clear the overlay. 88 | 89 | - *Sort button* 90 | Sort the grid images according to the parameter chosen on the parameter box. 91 | Click again to reverse the sort. 92 | `all parameters` or an empty box will sort by timestamp. 93 | `Right click` or `shift+click` will also sort by timestamp. 94 | 95 | - *Parameters pull down box* 96 | A pull down box containing all the parameters available. 97 | Used to narrow the search or expose matches and to select the sort or overlay information. 98 | If using the `path` parameter, a path on the search box will be normalized. 99 | `all parameters` or an empty box will reset the functions. 100 | 101 | - *Search entry box* 102 | Enter the search string here. 103 | `Return` will show only the images matching the search. 104 | `Shift+return` will expose the search match. 105 | `Control+return` will clear the search. 106 | `Control+shift+return` will clear the expose. 107 | 108 | - *search button* 109 | Will search the images based on the search box entry. 110 | `Shift+click`/`shift+return`/`shift+space` will clear the search without erasing the search box. 111 | `Control+click`/`control+return`/`control+space` will invert the search match. 112 | `Alt+click`/`alt+return`/`alt+space` will search for the exact match. 113 | `Alt+control+click`/`alt+control+return`/`alt+control+space` will search for the inverted exact match. 114 | 115 | - *expose button* 116 | Will expose the images based on the search box entry. 117 | All non-matching images will be grayed out. 118 | `Shift+click`/`shift+return`/`shift+space` will clear the expose without erasing the search box. 119 | `Control+click`/`control+return`/`control+space` will invert the expose match. 120 | `Alt+click`/`alt+return`/`alt+space` will expose the exact match. 121 | `Alt+control+click`/`alt+control+return`/`alt+control+space` will expose the inverted exact match. 122 | 123 | - *Image grid* 124 | Show all images found on the current paths and subfolders. 125 | Click to select an image. 126 | Once clicked, use the arrow keys to navigate the grid. 127 | `Double click`/`return`/`space` opens the full version on the internal viewer. 128 | `Shift+double click`/`shift+return`/`shift+pace` opens the full version on the default system viewer. 129 | You can select multiple images to perform bath operations. Most menu items and keyboard shortcuts will affect all selected images. 130 | `Shift+arrows` toggle selects next image. 131 | `Shift+click` select multiple images. 132 | `Control+click` toggle selected image. 133 | `Right click` opens a context menu with functions for this image instead of the selected one. 134 | `Clicking` the image name on the context menu will copy its path. 135 | The context menu contains: 136 | - The image name 137 | open image(s) internal 138 | open image(s) in system 139 | show image(s) in folder 140 | copy image(s) to folders 141 | copy image batch to folder 142 | save info 143 | save info as 144 | copy info to clipboard 145 | 146 | > **Some more keyboard shortcuts**: 147 | `Control+c`: Copy the selected image information to the clipboard. 148 | `Control+f`: Shows the current image on its folder. 149 | `Control+p`: Shows the current path. 150 | `Control+i`: Save the current info box content to a text file with the image name. 151 | `Control+shift+i`: Save the current info box content to a text file asking for a name. 152 | `Control+s`: Copy the selected image to another folder. 153 | `Shift+control+s`: Copy all selected images to a single folder. 154 | `Control+o`: Sort the images by the chosen parameter. 155 | `Control+shift+o`: Sort the images by timestamp. 156 | `Control+l`: Overlays the selected parameter on the grid images. 157 | `Control+shift+l`: Clears the overlayed parameters. 158 | `Control+r`: Refresh new/deleted images in the current path. 159 | `PageUp/PageDown`: Scroll the image grid by one page. 160 | `Home`: Go to the top of the grid. 161 | `End`: Go to the bottom of the grid. 162 | `Alt+up`: Go to the image one page above. 163 | `Alt+down`: Go to the image one page below. 164 | `Control+up`: Go to the first image. 165 | `Control+down`: Go to the last image. 166 | `Esc`: Cancel the progress when loading images. The images loaded so far are kept. The loading can be resumed with the refresh button. 167 | 168 | ## Full sized image view 169 | ![# Full image](Images/DiffusionBrowser_image.png) 170 | 171 | The window can be resized and will show the image seed and file name. 172 | Esc closes the window if selected. 173 | 174 | ## Paths requester 175 | ![# Paths](Images/DiffusionBrowser_paths.png) 176 | 177 | Paths can be added, changed, opened and deleted. 178 | Toggle subfolders to enable recursion on that path. 179 | Toggle active to enable or disable that path. 180 | `Double clicking` on their columns will also toggle them. 181 | `Double clicking` a path name will change it. 182 | `Return`/`space` on a selected path will toggle subfolders. 183 | `Shift+return`/`Shift+space` on a selected path will toggle active. 184 | You can shift select multiple paths to perform an action. 185 | Click the headers to sort. 186 | Accepting this requester with different paths will rebuild the program database. 187 | 188 | ## Configuration 189 | ![# Configuration](Images/DiffusionBrowser_config.png) 190 | 191 | **The following can be configured:** 192 | The number of grid columns. 193 | The number of grid rows. 194 | Each grid image size. 195 | The preview image size. 196 | The height of the buttons (to compensate for font sizes). 197 | The font name. 198 | The font size. 199 | The font weight. 200 | The Background color. 201 | The Main color. 202 | The 1st accent color. 203 | The 2nd accent color. 204 | The alert color. 205 | 206 | >*The get font buttons all take to the same font requester.* 207 | *The configuration will be saved on an `.ini` file.* 208 | *If the interface exceeds the screen size a warning will be given.* 209 | 210 | ## The information viewer 211 | Example: 212 | ```yaml 213 | prompt: infinite jest book cover 214 | seed: 3547671229 215 | sampler: Euler a 216 | steps: 20 217 | cfg scale: 7 218 | size: 512x512 219 | model hash: 7460a6fa 220 | 221 | source: automatic1111 222 | real_size: 512 x 512 223 | format: png 224 | created: 05-10-2022 15:32:53 225 | path: /.png 226 | ``` 227 | To help visualize the parameters and its values, they are presented on different colors. The information window also reorders the parameters on a more intuitive way and display further information. 228 | To this end, the parameters are identified and stored on a file called `parameters.txt` inside `\Projects\Default` for further use. 229 | You can edit this file to change the order they appear on the information window. 230 | When new images are cataloged, a message will be shown if new parameters are discovered. They will be automatically added to `parameters.txt` before the empty separator. They will also be copied to the clipboard. 231 | On the information window: 232 | Use the `mouse wheel` to scroll. 233 | Selected text will be automatically copied to the clipboard. 234 | `Right click` to copy all content. 235 | `Triple click` selects and copy a single parameter. 236 | The lines are reordered for a better presentation. 237 | If no compatible parameter is found, the raw information (if any) will be displayed under `embedded info:`. 238 | Some parameters have added numbers to keep their order. 239 | 240 | The default parameters are: 241 | ``` 242 | embedded info, raw prompt, raw negative prompt, prompt, negative prompt, 243 | negative_prompt, seed, sampler, sampler_name, steps, ddim_steps, ddim_eta, 244 | cfg scale, cfg_scale, cfgscale, width, height, size, initimg, strength, 245 | denoising strength, denoising_strength, first pass size, mask blur, n_iter, 246 | iterations, batch_size, batch size, batch pos, grid, batch, fit, progress_images, 247 | toggles, resize_mode, gfpgan_strength, upscale_level, upscale_strength, target, 248 | model, model hash, clip skip, model 1, upscale 1, visibility 1, model 2, upscale 2, 249 | visibility 2, version, fooocus v2 expansion, styles, performance, resolution, 250 | sharpness, guidance scale, adm guidance, base model, refiner model, refiner switch, 251 | scheduler, lora 1, weight 1, lora 2, weight 2, lora 3, weight 3, lora 4, weight 4, 252 | lora 5, weight 5, lora weight 1, lora name 1, lora weight 2, lora name 2, 253 | clipPoints, metadata scheme, lora_combined_1, refiner_switch, loras, 254 | prompt_expansion, adm_guidance, full_negative_prompt, full_prompt, guidance_scale, 255 | base_model_hash, base_model, refiner_model, created_by, user, lora hashes, 256 | metadata_scheme, , source, real_size, format, created, path 257 | ``` 258 | 259 | ## Changes 260 | v3.1 261 | - Added support for Fooocus and Fooocus a1111 native embedded metadata 262 | - Paths requester 263 | - Toggle active or inactive folders 264 | - Double clicking on subfolders or active on the paths requester will toggle them 265 | - Double clicking on the path name will change it 266 | - RETURN, SPACE on the paths requester will toggle subfolders 267 | - SHIFT+RETURN, SHIFT+SPACE on the paths requester will toggle active 268 | - Button to change the selected path 269 | - Image grid 270 | - Right click an image now selects it 271 | - SHIFT+UP became ALT+UP 272 | - SHIFT+DOWN became ALT+DOWN 273 | - Multi select images 274 | - SHIFT+UP add select up 275 | - SHIFT+DOWN add select down 276 | - SHIFT+LEFT add select left 277 | - SHIFT+RIGHT add select right 278 | - shift+click select more 279 | - control+click select toggle 280 | - Menu title copy all selected 281 | - Menu item perform on all images 282 | - Keyboard shortcuts affects all selected images 283 | - Removed seed from the internal image viewer header 284 | - Optimizations 285 | - Load images one by one instead of by rows (preparing for better optimizations) 286 | - Functions to consolidate selected and unselected button state 287 | - Several small tweaks on the GUI 288 | - Added Scrollbar to the information window 289 | - Themed the scrollbars 290 | - Exchanged open path and paths buttons positions 291 | - Moved refresh and open path buttons 292 | - Menu appearance 293 | - Some state color changes 294 | - Added save batch to folder menu item 295 | - SHIFT+CTRL+S activate batch copy 296 | - Paths combobox now show all paths containing images 297 | - Metadata errors now are displayed bunched together. 298 | - Metadata reading is more robust 299 | - Added a program icon 300 | 301 | v3.0 (beta) 302 | - Save image information database for later retrival 303 | - Image loaded on the fly, threaded for speed 304 | - Multiple paths with individual subfolder selection 305 | - Several new keyboard commands 306 | - search and expose by exact match 307 | 308 | v2.3 309 | - Sort by parameters 310 | - Show parameter overlay on the grid 311 | - Right click image preview to open in system default viewer 312 | - Page up and page down scroll grid by page 313 | - Right click on the info box copy all its information 314 | - New context menu items 315 | - New keyboard shortcuts 316 | - Save image parameters information to a file 317 | - Show parameters on Automatic1111 Extras modules ordered 318 | - Control+button invert search and expose matches 319 | - Right click+button clears expose/search/overlay/sort 320 | - Show raw image information if no compatible parameter found 321 | - Better information parsing 322 | - Better internal data representations 323 | - Better config handling 324 | - Several config bug fixes 325 | - Alert color if config entry is invalid 326 | - Small GUI fixes 327 | 328 | v2.2 329 | - ESC cancels image loading 330 | - Shift-clicking the preview image opens it in the default system viewer 331 | - Shift+double click/return/space on a grid image opens it on the default system viewer 332 | - Right click grid images opens a context menu with some functions on that image 333 | - Ctrl+c copy the selected grid image to a folder 334 | - Show folder now selects the folder 335 | - Show in folder now selects the image 336 | - Show folder, show in folder and view in system now working on Linux and Mac OS 337 | - Mouse wheel now working on Linux and Mac OS 338 | - Automatically add new parameters to `parameters.txt` 339 | - Better embedded information parsing 340 | - Catch errors loading bad image files 341 | - Main widow resizable to avoid problem with window managers 342 | - Fixed image viewer window aspect ratio bug 343 | 344 | v2.1 345 | - Small interface improvements 346 | - Better arrow keys navigation 347 | - Selected image now highlighted on the grid 348 | - Return/space/double click a grid image opens the full version 349 | - Esc closes the full image window if selected 350 | - Search and expose images 351 | - Do not show grid progress while loading anymore but it's faster 352 | - Refresh (!) now only updates the new/deleted images. 353 | - Better configuration handling 354 | - Resizable font requester and configuration windows 355 | - Bigger font requester 356 | - Font preview while browsing 357 | - The preview text on the font requester can be changed 358 | - Arrows to change the size on the font requester 359 | 360 | ## Linux stuff 361 | 362 | Installation (optional): 363 | - This will copy the program to a folder in the home directory and will put a desktop shortcut in the systems directory so every start menu and app launcher will find Diffusion Browser. 364 | Leave `Diffusion-Browser.desktop` and `linux-install.sh` in the main Diffusion Browser folder and run `linux-install.sh`. 365 | 366 | - To uninstall type: 367 | `rm -r $HOME/Diffusion-Browser` 368 | `rm ~/.local/share/applications/Diffusion-Browser.desktop` 369 | 370 | 371 | For KDE users if main program window is buggy: 372 | - Press ALT + F3 373 | - Select "More Actions" 374 | - Select "Configure Special Windoww Settings" 375 | - Add Property 376 | - Size 377 | - Enter desired Size 378 | 379 | ## Acknowledgments 380 | 381 | Many thanks to [RandomLegend](https://github.com/RandomLegend) not only for extensive testing, bug reporting and great feature ideas but also for actually contributing most of the Linux code and stuff. 382 | 383 | **Diffusion Browser** is offered as is, with no guaranties whatsoever, use at your own discretion. 384 | Enjoy and send feedback. 385 | Thanks. 386 | -------------------------------------------------------------------------------- /difbrow.pyw: -------------------------------------------------------------------------------- 1 | # Diffuse Browser v3.1 2 | # Fred Rique (c) 2022 - 2024. 3 | # github.com/farique1/diffusion-browser 4 | # An easy way to view embedded image metadata of most AI generators. 5 | 6 | # CHANGES 7 | # v3.0 8 | # Load image information and save for further use 9 | # Read image file on demand, threaded 10 | # Multiple paths 11 | # ALT+... > Search or expose exact 12 | # CONTROL+ALT+... > Search or expose exact inverted 13 | # HOME > Go to the top of the grid 14 | # END > Go to the bottom of the grid 15 | # SHIFT+UP > Go to the image one page above 16 | # SHIFT+DOWN > Go to the image one page below 17 | # CONTROL+UP > Go to the first image 18 | # CONTROL+DOWN > Go to the last image 19 | # Searching using PATH as a parameter will normalize the path 20 | # v3.1 21 | # Paths requester 22 | # Toggle active or inactive folders 23 | # Double clicking on subfolders or active on the paths requester will toggle them 24 | # Double clicking on the path name will change it 25 | # RETURN, SPACE on the paths requester will toggle subfolders 26 | # SHIFT+RETURN, SHIFT+SPACE on the paths requester will toggle active 27 | # Button to change the selected path 28 | # Image grid 29 | # Right click an image now selects it 30 | # SHIFT+UP became ALT+UP 31 | # SHIFT+DOWN became ALT+DOWN 32 | # Multi select images 33 | # SHIFT+UP add select up 34 | # SHIFT+DOWN add select down 35 | # SHIFT+LEFT add select left 36 | # SHIFT+RIGHT add select right 37 | # shift+click select more 38 | # control+click select toggle 39 | # Menu title copy all selected 40 | # Menu item perform on all images 41 | # Keyboard shortcuts affects all selected images 42 | # Removed seed from the internal image viewer header 43 | # Optimizations 44 | # Load images one by one instead of by rows 45 | # Functions to consolidate selected and unselected button state 46 | # Several small tweaks on the GUI 47 | # Added Scrollbar to the information window 48 | # Themed the scrollbars 49 | # Exchanged open path and paths buttons positions 50 | # Moved refresh and open path buttons 51 | # Menu appearance 52 | # Some state color changes 53 | # Added save batch to folder menu item 54 | # SHIFT+CTRL+S activate batch copy 55 | # Paths combobox show all paths containing images 56 | # Metadata errors bunched together for display. 57 | # Change readme: ctr+l=overlay, ctrl+i=save info 58 | # Metadata reading more robust 59 | # Added support for Fooocus and Fooocus a1111 native embedded metadata 60 | 61 | # FIX 62 | # Sometimes when searching for a word and after it sorting and overlaying, 63 | # the overlaying stops working on all shown images. (thesis: maybe it is 64 | # only overlaying the images that are "visible" on the grid if the grid 65 | # did not had been reduced by the search. Images that are farthest from 66 | # the first searched image than the height of the grid in images) 67 | 68 | # ADD 69 | # Use im.get_format_mimetype() or Image.MIME[img.format] or img.format 70 | # to determine the image type. Also to load images regardless of extension 71 | # Undock and dock the image and embedded info viewers (see teste.py) 72 | # Multiple projects 73 | # Images Heart 74 | # Images Starts 75 | # Images Tags 76 | # Images Sets 77 | # Mute folders to prevent them from appearing without changing the database 78 | # Rebuild the order of the parameters without needing to rebuild the database 79 | # Keep metadata reading functions independent, delivering a standard format 80 | # Allow consolidation of multiple parameters into a single label 81 | 82 | import re 83 | import os 84 | import bz2 85 | import glob 86 | import json 87 | import math 88 | import time 89 | import pickle 90 | import shutil 91 | import platform 92 | import datetime 93 | import threading 94 | import subprocess 95 | import collections 96 | import configparser 97 | import tkinter as tk 98 | from operator import itemgetter 99 | from collections import OrderedDict 100 | from tkinter.colorchooser import askcolor 101 | from tkinter import ttk, font, filedialog 102 | from PIL import Image, ImageTk, ImageOps, UnidentifiedImageError 103 | 104 | # Constants 105 | COL_NBR = 5 106 | ROW_NBR = 5 107 | GRID_IMG_SZ = 100 108 | INFO_IMG_SZ = 250 109 | BUTT_HEIGHT = 26 110 | FONT_NAME = 'Tahoma' 111 | FONT_SIZE = 10 112 | FONT_WEIGHT = 'normal' 113 | BG_COLOR = 'black' 114 | FONT_COLOR = 'teal' 115 | ACC_COLOR1 = 'goldenrod' 116 | ACC_COLOR2 = 'grey70' 117 | ALERT_COLOR = 'dark red' 118 | # TOP_PATH = 'D:/Stable Diffusion WebUI/stable-diffusion-webui/outputs/test' 119 | 120 | FONT = [FONT_NAME, FONT_SIZE, FONT_WEIGHT] 121 | 122 | BORDER = 1 123 | PROGRAM_NAME = 'Diffusion\nBrowser' 124 | ALL_PARAMETERS = 'all parameters' 125 | ALL_FOLDERS = 'all paths' 126 | SEARCH_HELP = 'enter search' 127 | TEXT_INFO_DEFAULT = ('Diffusion Browser v3.0\n' 128 | 'github.com/farique1/diffusion-browser\n' 129 | '(c) Fred Rique 2022 - 2024\n\n' 130 | 'Browse pictures and metadata generated by Stable Diffusion.\n' 131 | 'Works with embedded data from most generators in the style of Automatic1111.\n' 132 | 'Converter provided for Fooocus log.html') 133 | 134 | LOCAL_PATH = os.path.split(os.path.abspath(__file__))[0] 135 | PROJECT_PATH = 'Projects' 136 | current_project = 'default' 137 | INI_FILE = os.path.join(LOCAL_PATH, PROJECT_PATH, current_project, 'difbrowser.ini') 138 | PARAMETERS_FILE = os.path.join(LOCAL_PATH, PROJECT_PATH, current_project, 'parameters.txt') 139 | DATA_FILE = os.path.join(LOCAL_PATH, PROJECT_PATH, current_project, 'data.pickle') 140 | FOLDERS_FILE = os.path.join(LOCAL_PATH, PROJECT_PATH, current_project, 'folders.json') 141 | 142 | OS = platform.system() 143 | 144 | with open(PARAMETERS_FILE, 'r') as file: 145 | TEXT_PARS = file.read().splitlines() 146 | 147 | COMBO_VALUES = TEXT_PARS 148 | COMBO_VALUES.insert(0, ALL_PARAMETERS) 149 | 150 | # Variable initialization 151 | folders = [] 152 | image_list = [] 153 | new_pars = [] 154 | is_overlay = '' 155 | current_index = 0 156 | current_seed = '' 157 | current_image = '' 158 | multi_index = [] 159 | sort_reverse = True 160 | time_format = '%Y-%m-%d %H:%M:%S' 161 | prev_button = None 162 | 163 | # .ini file handling 164 | ini_path = os.path.join(LOCAL_PATH, INI_FILE) 165 | config_ini = configparser.ConfigParser() 166 | if os.path.isfile(ini_path): 167 | try: 168 | config_ini.read(ini_path) 169 | config_sec = config_ini['CONFIGS'] 170 | COL_NBR = int(config_sec.get('number_of_columns')) 171 | ROW_NBR = int(config_sec.get('number_of_lines')) 172 | GRID_IMG_SZ = int(config_sec.get('grid_image_size')) 173 | INFO_IMG_SZ = int(config_sec.get('preview_image_size')) 174 | BUTT_HEIGHT = int(config_sec.get('button_height')) 175 | FONT_NAME = config_sec.get('font_name') 176 | FONT_SIZE = int(config_sec.get('font_size')) 177 | FONT_WEIGHT = config_sec.get('font_weight') 178 | BG_COLOR = config_sec.get('background_color') 179 | FONT_COLOR = config_sec.get('main_color') 180 | ACC_COLOR1 = config_sec.get('accent_color_1') 181 | ACC_COLOR2 = config_sec.get('accent_color_2') 182 | ALERT_COLOR = config_sec.get('alert_color') 183 | # TOP_PATH = config_sec.get('default_path') 184 | 185 | FONT = [FONT_NAME, FONT_SIZE, FONT_WEIGHT] 186 | # TOP_PATH = os.path.normpath(TOP_PATH) 187 | 188 | except (ValueError, configparser.NoOptionError) as e: 189 | print(f'.INI file problem: {str(e)}') 190 | raise SystemExit(0) 191 | 192 | # Initialize 193 | root = tk.Tk() 194 | 195 | root.configure(background='black') 196 | root.title('Diffusion Browser') 197 | root.protocol("WM_DELETE_WINDOW", lambda: root.destroy()) 198 | root.bind_class("Button", "", lambda event: event.widget.invoke()) 199 | 200 | if OS == 'Linux': 201 | root.resizable(True, True) 202 | else: 203 | root.resizable(True, True) 204 | 205 | 206 | def resize_image(image, maxsize): 207 | '''Resize image maintaining aspect ratio and maximum size''' 208 | 209 | r1 = image.size[0] / maxsize[0] # width ratio 210 | r2 = image.size[1] / maxsize[1] # height ratio 211 | ratio = max(r1, r2) 212 | newsize = (int(image.size[0] / ratio), int(image.size[1] / ratio)) 213 | image = image.resize(newsize, Image.Resampling.LANCZOS) 214 | 215 | return image 216 | 217 | 218 | def get_canvas_boundaries(): 219 | try: 220 | canvas_height = canvas.winfo_height() 221 | y_srt = canvas.yview()[0] 222 | y_end = canvas.yview()[1] 223 | y_len = y_end - y_srt 224 | total_height = canvas_height / y_len 225 | canvas_y_top = int(total_height * y_srt / (GRID_IMG_SZ + BORDER * 2)) 226 | canvas_y_bot = int(total_height * y_end / (GRID_IMG_SZ + BORDER * 2)) + 1 227 | 228 | slice_start = canvas_y_top * COL_NBR 229 | slice_end = canvas_y_bot * COL_NBR + COL_NBR 230 | slice_end = min(slice_end, len(image_list)) 231 | 232 | return slice_start, slice_end 233 | 234 | except tk.TclError: 235 | raise SystemExit(0) 236 | 237 | 238 | def render_buttons(image_data, n): 239 | 240 | slice_start, slice_end = get_canvas_boundaries() 241 | if n < slice_start or n > slice_end: 242 | image_data['has_image'] = False 243 | return 244 | 245 | image_data['button'].update() 246 | 247 | try: 248 | image = Image.open(image_data['file']) 249 | image = resize_image(image, (GRID_IMG_SZ, GRID_IMG_SZ)) 250 | overlay = '' 251 | 252 | if is_overlay: 253 | image = image.convert("L") 254 | image = ImageOps.colorize(image, black=BG_COLOR, white=FONT_COLOR) 255 | embed_dict = image_data['dic_info'] 256 | overlay = embed_dict.get(is_overlay, '') 257 | 258 | image = ImageTk.PhotoImage(image) 259 | image_data['button'].config(image=image, text=overlay) 260 | image_data['button'].image = image 261 | except tk.TclError: 262 | raise SystemExit(0) 263 | 264 | 265 | def refresh_images(x, y): 266 | '''Refresh images on the grid buttons''' 267 | 268 | if not image_list: 269 | return 270 | 271 | vsb.set(x, y) 272 | 273 | slice_start, slice_end = get_canvas_boundaries() 274 | 275 | for n in range(slice_start, slice_end): 276 | image_data = image_list[n] 277 | if not image_data['has_image'] and len(threading.enumerate()) < 900: 278 | image_data['has_image'] = True 279 | t = threading.Thread(target=render_buttons, args=(image_data, n,)) 280 | t.daemon = True 281 | t.start() 282 | 283 | 284 | def on_mousewheel(event): 285 | '''Handles mouse wheel''' 286 | 287 | if OS == 'Linux': 288 | y_steps = 5 289 | if event.num == 4: 290 | y_steps *= -1 291 | elif OS == 'Darwin': 292 | y_steps = event.delta 293 | elif OS == 'Windows': 294 | y_steps = int(-1 * (event.delta / 120)) 295 | 296 | if 'buttons_frame' in str(event.widget): 297 | canvas.yview_scroll(y_steps, 'units') 298 | 299 | 300 | def button_select(button): 301 | button.config(bg=ACC_COLOR1, width=GRID_IMG_SZ - 6, height=GRID_IMG_SZ - 6, bd=3, relief='ridge') 302 | 303 | 304 | def button_unselect(button): 305 | button.config(bg=BG_COLOR, width=GRID_IMG_SZ, height=GRID_IMG_SZ, bd=0, relief='flat') 306 | 307 | 308 | def click_grid_image(idx, shift=False, control=False, right=False): 309 | '''Handles clicking on a image on the grid''' 310 | 311 | global current_seed 312 | global prev_button 313 | global current_index 314 | global multi_index 315 | 316 | button = image_list[idx]['button'] 317 | if control: 318 | if button.cget('bg') == BG_COLOR: 319 | button_select(button) 320 | multi_index.append(idx) 321 | button.focus_set() 322 | else: 323 | button_unselect(button) 324 | multi_index.remove(idx) 325 | return 326 | 327 | if current_index != idx and shift: 328 | if current_index < idx: 329 | idx_start, idx_end = current_index, idx 330 | else: 331 | idx_start, idx_end = idx, current_index 332 | 333 | for i in range(idx_start, idx_end + 1): 334 | if i not in multi_index: 335 | multi_index.append(i) 336 | button = image_list[i]['button'] 337 | button_select(button) 338 | button.focus_set() 339 | return 340 | 341 | if (not right and not shift and multi_index) or (right and button.cget('bg') == BG_COLOR): 342 | for i in multi_index: 343 | button = image_list[i]['button'] 344 | button_unselect(button) 345 | multi_index = [] 346 | 347 | if right and button.cget('bg') == ACC_COLOR1: 348 | return 349 | 350 | multi_index = [idx] 351 | 352 | button = image_list[idx]['button'] 353 | embed_text = image_list[idx]['txt_info'] 354 | image = Image.open(image_list[idx]['file']) 355 | 356 | image = resize_image(image, (INFO_IMG_SZ, INFO_IMG_SZ)) 357 | image = ImageTk.PhotoImage(image) 358 | img_info['image'] = image 359 | img_info.image = image 360 | img_info.config(bg=BG_COLOR) 361 | 362 | # Get tag information for colorizing 363 | matches = [] 364 | for i, line in enumerate(embed_text.splitlines(), 1): 365 | for tag in TEXT_PARS: 366 | tag_colon = f'{tag}:' 367 | if line.startswith(tag_colon): 368 | start = f'{str(i)}.{len(tag_colon)}' 369 | end = f'{str(i)}.{len(line)}' 370 | content = line[len(tag_colon):] 371 | matches.append((tag_colon, start, end, content)) 372 | 373 | # Draw text 374 | text_info['state'] = 'normal' 375 | text_info.delete('1.0', 'end') 376 | text_info.insert('insert', embed_text) 377 | for hit in matches: 378 | if hit[0].startswith('seed:'): 379 | current_seed = hit[3] 380 | color = ACC_COLOR2 381 | if hit[3].strip().replace('.', '').isdigit() \ 382 | or hit[3].strip().replace(' x ', '').isdigit() \ 383 | or hit[3].strip().replace(' ', '').isdigit(): 384 | color = ACC_COLOR1 385 | if hit[0].startswith('embedded info'): 386 | color = ACC_COLOR2 387 | text_info.tag_add(hit[0], hit[1], hit[2]) 388 | text_info.tag_config(hit[0], foreground=color) 389 | text_info['state'] = 'disable' 390 | 391 | if not prev_button: 392 | prev_button = button 393 | 394 | # image_keep = button.image 395 | button_unselect(prev_button) 396 | button_select(button) 397 | button.focus_set() 398 | # button.update() 399 | 400 | # Give a little time for Python to come to it's senses 401 | time.sleep(0.05) 402 | 403 | prev_button = button 404 | 405 | current_index = idx 406 | 407 | 408 | def grid_keys(event, delta, absolute=False, select=False): 409 | '''Navigate grid with the arrow keys. 410 | event: TK internal 411 | delta: Image amout to jump 412 | absolute: if the jump is relative or absolute (to the first or last image)''' 413 | 414 | global current_index 415 | 416 | prev_current_index = current_index 417 | 418 | if absolute: 419 | current_index = (len(image_list) - 1) * delta 420 | else: 421 | current_index = current_index + delta 422 | 423 | if (current_index < 0) or (current_index > len(image_list) - 1) or \ 424 | not image_list[current_index]['search']: 425 | current_index = prev_current_index 426 | return 427 | 428 | image_amount = len(image_list) 429 | image_y = math.floor(current_index / COL_NBR) 430 | rows = math.ceil(image_amount / COL_NBR) 431 | button = image_list[current_index]['button'] 432 | button_y = button.winfo_y() 433 | canvas_height = canvas.winfo_height() 434 | y_srt = canvas.yview()[0] 435 | y_end = canvas.yview()[1] 436 | y_len = y_end - y_srt 437 | total_height = int(canvas_height / y_len) 438 | canvas_y_top = int(total_height * y_srt) 439 | canvas_y_bot = int(total_height * y_end) 440 | canvas_position = image_y / rows 441 | img_len = 1 / rows 442 | 443 | # Only move if selection is outside the grid frame 444 | if (button_y > canvas_y_bot - GRID_IMG_SZ): 445 | canvas.yview_moveto(canvas_position - y_len + img_len) 446 | if (button_y <= canvas_y_top): 447 | canvas.yview_moveto(canvas_position) 448 | 449 | if select: 450 | click_grid_image(current_index, control=True) 451 | else: 452 | click_grid_image(current_index) 453 | 454 | # button.invoke() 455 | # button.focus_set() 456 | 457 | 458 | def maintain_aspect_ratio(event, original, c_full_img, aspect_ratio): 459 | '''Maintains aspect ratio when resizing the image window''' 460 | 461 | new_aspect_ratio = event.width / event.height 462 | if new_aspect_ratio > aspect_ratio: 463 | desired_width = event.width 464 | desired_height = int(event.width / aspect_ratio) 465 | else: 466 | desired_height = event.height 467 | desired_width = int(event.height * aspect_ratio) 468 | 469 | if event.width != desired_width or event.height != desired_height: 470 | try: 471 | event.widget.geometry(f'{desired_width}x{desired_height}') 472 | size = (desired_width, desired_height) 473 | resized = original.resize(size, Image.Resampling.LANCZOS) 474 | image = ImageTk.PhotoImage(resized) 475 | c_full_img.delete('IMG') 476 | c_full_img.create_image(0, 0, image=image, anchor='nw', tags='IMG') 477 | c_full_img.image = image 478 | except AttributeError: 479 | pass 480 | return 'break' 481 | 482 | 483 | def show_full_image_multi(i): 484 | '''Its is here so each window can have its own variable reference''' 485 | 486 | if not image_list: 487 | return 488 | 489 | image_window = tk.Toplevel() 490 | image_window.title(f'{image_list[i]["file"]}') 491 | 492 | original = Image.open(image_list[i]['file']) 493 | 494 | # Prevent showing images bigger than the screen size 495 | max_width = min(original.size[0], image_window.winfo_screenwidth()) 496 | max_height = min(original.size[1], image_window.winfo_screenheight()) 497 | original = resize_image(original, (max_width, max_height)) 498 | 499 | image = ImageTk.PhotoImage(original) 500 | 501 | x = root.winfo_x() 502 | y = root.winfo_y() + 30 + BUTT_HEIGHT 503 | dimensions = f'{image.width()}x{image.height()}+{x}+{y}' 504 | image_window.geometry(dimensions) 505 | 506 | frame = tk.Frame(image_window) 507 | frame.columnconfigure(0, weight=1) 508 | frame.rowconfigure(0, weight=1) 509 | 510 | c_full_img = tk.Canvas(image_window, bd=0, highlightthickness=0) 511 | c_full_img.create_image(0, 0, image=image, anchor='nw', tags='IMG') 512 | c_full_img.image = image 513 | c_full_img.grid(row=0, sticky='news') 514 | c_full_img.pack(fill='both', expand=1) 515 | 516 | image_window.update() 517 | width = image_window.winfo_width() 518 | height = image_window.winfo_height() 519 | image_window.bind('', lambda event: maintain_aspect_ratio(event, original, c_full_img, width / height)) 520 | image_window.bind('', lambda event: image_window.destroy()) 521 | 522 | image_window.focus_set() 523 | 524 | 525 | def show_full_image(idx): 526 | '''Handles clicking on the image preview''' 527 | 528 | if idx is None: 529 | return 530 | 531 | # Calling a new function each time so each window has its own variable reference 532 | for i in idx: 533 | show_full_image_multi(i) 534 | 535 | 536 | def show_image(idx): 537 | 538 | if idx is None: 539 | return 540 | 541 | for i in idx: 542 | path = image_list[i]['file'] 543 | 544 | if OS == 'Linux': 545 | default_app = subprocess.run(['xdg-mime', 'query', 'default', 'inode/directory'], 546 | stdout=subprocess.PIPE).stdout.decode('utf-8').strip() 547 | 548 | if default_app == 'org.kde.dolphin.desktop': 549 | subprocess.Popen(['dolphin', path]) 550 | else: 551 | default_app == 'nautilus.desktop' 552 | subprocess.Popen(['nautilus', path]) 553 | elif OS == 'Darwin': 554 | subprocess.Popen(["open", path]) 555 | else: 556 | subprocess.Popen(["explorer", '/open,', path]) 557 | 558 | 559 | def config_requester(): 560 | '''Main configuration window''' 561 | 562 | def test_weight(weight): 563 | weight = weight.strip() 564 | if weight != 'normal' and weight != 'bold' and weight != 'italic' and weight != '': 565 | conf_entries[7].config(bg=ALERT_COLOR) 566 | else: 567 | conf_entries[7].config(bg=ACC_COLOR1) 568 | 569 | def test_int(widget): 570 | entry = widget.get() 571 | if not entry.isnumeric(): 572 | widget.config(bg=ALERT_COLOR) 573 | else: 574 | widget.config(bg=ACC_COLOR1) 575 | 576 | def change_button_height(size): 577 | '''Update the button height configuration box''' 578 | 579 | test_int(conf_entries[6]) 580 | 581 | if conf_entries[6]['bg'] != ALERT_COLOR: 582 | conf_entries[4].delete(0, 'end') 583 | conf_entries[4].insert('insert', int(int(size) * 2.5)) 584 | 585 | def pick_color(r, cur_col): 586 | '''Open a color picker''' 587 | 588 | # Open an inactive window to be able to disable the main interface 589 | dummy_window = tk.Toplevel() 590 | dummy_window.withdraw() 591 | config.grab_release() 592 | dummy_window.grab_set() 593 | 594 | conf_entries[r - 1].delete(0, 'end') 595 | color = askcolor(color=cur_col, title=conf_labels[r - 1]['text'], parent=config)[1] 596 | 597 | if not color: 598 | color = cur_col 599 | conf_entries[r - 1].insert('insert', color) 600 | 601 | dummy_window.destroy() 602 | config.grab_set() 603 | 604 | change_color(r) 605 | 606 | def change_color(r): 607 | '''Change the selected color''' 608 | 609 | if r < 9 or r > 13: 610 | return 611 | try: 612 | bt_color_list[r - 9]['bg'] = conf_entries[r - 1].get() 613 | conf_entries[r - 1].config(bg=ACC_COLOR1) 614 | except tk.TclError: 615 | conf_entries[r - 1].config(bg=ALERT_COLOR) 616 | 617 | def accept_config(button, conf_entries): 618 | '''Close the configuration window applying changes''' 619 | 620 | global COL_NBR 621 | global ROW_NBR 622 | global GRID_IMG_SZ 623 | global INFO_IMG_SZ 624 | global BUTT_HEIGHT 625 | global FONT 626 | global BG_COLOR 627 | global FONT_COLOR 628 | global ACC_COLOR1 629 | global ACC_COLOR2 630 | global ALERT_COLOR 631 | # global TOP_PATH 632 | 633 | change_button_height(conf_entries[6].get()) 634 | 635 | button.focus_set() 636 | 637 | config.update() 638 | 639 | if conf_entries[0]['bg'] != ALERT_COLOR: 640 | COL_NBR = int(conf_entries[0].get()) 641 | if conf_entries[1]['bg'] != ALERT_COLOR: 642 | ROW_NBR = int(conf_entries[1].get()) 643 | if conf_entries[2]['bg'] != ALERT_COLOR: 644 | GRID_IMG_SZ = int(conf_entries[2].get()) 645 | if conf_entries[3]['bg'] != ALERT_COLOR: 646 | INFO_IMG_SZ = int(conf_entries[3].get()) 647 | if conf_entries[4]['bg'] != ALERT_COLOR: 648 | BUTT_HEIGHT = int(conf_entries[4].get()) 649 | if conf_entries[6]['bg'] != ALERT_COLOR \ 650 | and conf_entries[7]['bg'] != ALERT_COLOR: 651 | FONT = (conf_entries[5].get(), 652 | int(conf_entries[6].get()), 653 | conf_entries[7].get()) 654 | BG_COLOR = bt_color_list[0]['bg'] 655 | FONT_COLOR = bt_color_list[1]['bg'] 656 | ACC_COLOR1 = bt_color_list[2]['bg'] 657 | ACC_COLOR2 = bt_color_list[3]['bg'] 658 | ALERT_COLOR = bt_color_list[4]['bg'] 659 | # if conf_entries[13]['bg'] != ALERT_COLOR: 660 | # TOP_PATH = conf_entries[13].get() 661 | 662 | FONT_NAME = FONT[0] 663 | FONT_SIZE = FONT[1] 664 | FONT_WEIGHT = FONT[2] 665 | 666 | t_scr_width = root.winfo_screenwidth() * 0.9 667 | t_scr_height = root.winfo_screenheight() * 0.9 668 | 669 | # Check if the interface will fit on the current screen size 670 | if (COL_NBR * GRID_IMG_SZ + INFO_IMG_SZ) > t_scr_width \ 671 | or (ROW_NBR * GRID_IMG_SZ) > t_scr_height \ 672 | or INFO_IMG_SZ > t_scr_height: 673 | tk.messagebox.showinfo(title='Bad configuration', 674 | message='Interface elements too big or too many.\n' 675 | 'Will not fit within 90% of the screen.', 676 | parent=config) 677 | return 678 | 679 | if not config_ini.has_section('CONFIGS'): 680 | config_ini.add_section('CONFIGS') 681 | config_ini.set('CONFIGS', 'number_of_columns', str(COL_NBR)) 682 | config_ini.set('CONFIGS', 'number_of_lines', str(ROW_NBR)) 683 | config_ini.set('CONFIGS', 'grid_image_size', str(GRID_IMG_SZ)) 684 | config_ini.set('CONFIGS', 'preview_image_size', str(INFO_IMG_SZ)) 685 | config_ini.set('CONFIGS', 'button_height', str(BUTT_HEIGHT)) 686 | config_ini.set('CONFIGS', 'font_name', FONT_NAME) 687 | config_ini.set('CONFIGS', 'font_size', str(FONT_SIZE)) 688 | config_ini.set('CONFIGS', 'font_weight', FONT_WEIGHT) 689 | config_ini.set('CONFIGS', 'background_color', BG_COLOR) 690 | config_ini.set('CONFIGS', 'main_color', FONT_COLOR) 691 | config_ini.set('CONFIGS', 'accent_color_1', ACC_COLOR1) 692 | config_ini.set('CONFIGS', 'accent_color_2', ACC_COLOR2) 693 | config_ini.set('CONFIGS', 'alert_color', ALERT_COLOR) 694 | # config_ini.set('CONFIGS', 'default_path', TOP_PATH) 695 | 696 | with open(ini_path, 'w') as configfile: 697 | config_ini.write(configfile) 698 | 699 | config.destroy() 700 | # update_grid() 701 | reset_interface() 702 | 703 | def font_requester(r, cur_col): 704 | '''Create a font requester''' 705 | 706 | def siz_min_pls(delta, entry): 707 | '''Buttons to change the font size''' 708 | 709 | size = int(entry.get()) 710 | size += delta 711 | if size < 1: 712 | size = 1 713 | entry.delete(0, 'end') 714 | entry.insert('insert', size) 715 | 716 | change_font([font_temp[0], int(size), font_temp[2]]) 717 | 718 | def font_weight(weight, weight_list): 719 | '''Handles clicking on the font weight buttons''' 720 | 721 | global font_temp 722 | for item in weight_list: 723 | item[0]['bg'] = FONT_COLOR 724 | item[0]['fg'] = BG_COLOR 725 | 726 | weight_list[weight][0]['bg'] = BG_COLOR 727 | weight_list[weight][0]['fg'] = FONT_COLOR 728 | 729 | font_temp[2] = weight_list[weight][1] 730 | font_temp[1] = int(size_entry.get()) 731 | 732 | change_font(font_temp) 733 | 734 | def change_font(font_arg): 735 | '''Change the current font''' 736 | 737 | global font_temp 738 | font_temp = font_arg 739 | font_preview.config(font=font_temp) 740 | 741 | def accept_font(): 742 | '''Close the font requester accepting the changes''' 743 | 744 | conf_entries[4].delete(0, 'end') 745 | conf_entries[4].insert('insert', int(int(size_entry.get()) * 2.5)) 746 | conf_entries[5].delete(0, 'end') 747 | conf_entries[5].insert('insert', font_temp[0]) 748 | conf_entries[6].delete(0, 'end') 749 | conf_entries[6].insert('insert', size_entry.get()) 750 | conf_entries[7].delete(0, 'end') 751 | conf_entries[7].insert('insert', font_temp[2]) 752 | config.grab_set() 753 | config.focus_set() 754 | folders_req.destroy() 755 | 756 | dummy.focus_set() 757 | config.update() 758 | 759 | if conf_entries[5]['bg'] == ALERT_COLOR \ 760 | or conf_entries[6]['bg'] == ALERT_COLOR \ 761 | or conf_entries[7]['bg'] == ALERT_COLOR: 762 | return 763 | 764 | # global font_box 765 | global font_preview 766 | global size_entry 767 | global font_temp 768 | global folders_req 769 | 770 | font_temp = [conf_entries[5].get(), 771 | int(conf_entries[6].get()), 772 | conf_entries[7].get()] 773 | 774 | folders_req = tk.Toplevel() 775 | folders_req.title('Font') 776 | 777 | available_fonts = font.families() 778 | available_fonts = sorted(available_fonts) 779 | 780 | font_box = tk.Listbox(folders_req, highlightthickness=0, relief='flat', name='font_list', 781 | bg=ACC_COLOR1, fg=BG_COLOR, selectbackground=FONT_COLOR) 782 | font_box.grid(row=0, columnspan=3, sticky='news') 783 | font_box.option_add('font', FONT) 784 | 785 | sb = ttk.Scrollbar(folders_req, orient='vertical') 786 | sb.grid(row=0, column=3, sticky='news') 787 | 788 | font_box.configure(yscrollcommand=sb.set) 789 | sb.config(command=font_box.yview) 790 | 791 | config.grab_release() 792 | folders_req.grab_set() 793 | folders_req.focus_set() 794 | 795 | for fonts in available_fonts: 796 | font_box.insert('end', fonts) 797 | 798 | # Duplicate the last element to prevent down key from overflowing the listbox items 799 | available_fonts.append(available_fonts[-1]) 800 | 801 | font_box.bind("", lambda e: change_font( 802 | [available_fonts[font_box.curselection()[0]], int(size_entry.get()), font_temp[2]])) 803 | 804 | font_box.bind("", lambda e: change_font( 805 | [available_fonts[font_box.curselection()[0] - 1], int(size_entry.get()), font_temp[2]])) 806 | 807 | font_box.bind("", lambda e: change_font( 808 | [available_fonts[font_box.curselection()[0] + 1], int(size_entry.get()), font_temp[2]])) 809 | 810 | weight_list = [] 811 | brd_norm_butt = tk.Frame(folders_req, bg=BG_COLOR) 812 | brd_norm_butt.grid(row=1, column=0, sticky='nsew') 813 | norm_butt = tk.Button(brd_norm_butt, text="normal", bg=FONT_COLOR, fg=BG_COLOR, 814 | activebackground=ACC_COLOR1, bd=0, command=lambda: font_weight(0, weight_list)) 815 | norm_butt.pack(expand=True, fill='both', pady=1, padx=1) 816 | weight_list.append([norm_butt, 'normal']) 817 | 818 | brd_norm_bold = tk.Frame(folders_req, bg=BG_COLOR) 819 | brd_norm_bold.grid(row=1, column=1, sticky='nsew') 820 | bold_butt = tk.Button(brd_norm_bold, text="bold", bg=FONT_COLOR, fg=BG_COLOR, 821 | activebackground=ACC_COLOR1, bd=0, command=lambda: font_weight(1, weight_list)) 822 | bold_butt.pack(expand=True, fill='both', pady=1, padx=1) 823 | weight_list.append([bold_butt, 'bold']) 824 | 825 | brd_norm_ital = tk.Frame(folders_req, bg=BG_COLOR) 826 | brd_norm_ital.grid(row=1, column=2, columnspan=2, sticky='nsew') 827 | ital_butt = tk.Button(brd_norm_ital, text="italic", bg=FONT_COLOR, fg=BG_COLOR, 828 | activebackground=ACC_COLOR1, bd=0, command=lambda: font_weight(2, weight_list)) 829 | ital_butt.pack(expand=True, fill='both', pady=1, padx=1) 830 | weight_list.append([ital_butt, 'italic']) 831 | 832 | size_entry = tk.Entry(folders_req, text="cancel", bd=0, bg=ACC_COLOR1, fg=BG_COLOR) 833 | size_entry.delete(0, 'end') 834 | size_entry.insert('insert', conf_entries[6].get()) 835 | size_entry.bind('', lambda e: change_font([font_temp[0], int(size_entry.get()), font_temp[2]])) 836 | size_entry.bind('', lambda e: change_font([font_temp[0], int(size_entry.get()), font_temp[2]])) 837 | size_entry.bind('', lambda e: change_font([font_temp[0], int(size_entry.get()), font_temp[2]])) 838 | size_entry.grid(row=2, column=0, columnspan=2, sticky='nsew') 839 | 840 | brd_siz_frm = tk.Frame(folders_req, bg=BG_COLOR) 841 | brd_siz_frm.grid(row=2, column=2, columnspan=2, sticky='nsew') 842 | 843 | brd_siz_min = tk.Frame(brd_siz_frm, bg=BG_COLOR) 844 | brd_siz_min.grid(row=0, column=0, sticky='nsew') 845 | size_min = tk.Button(brd_siz_min, text="<", bd=0, bg=FONT_COLOR, fg=BG_COLOR, 846 | command=lambda: siz_min_pls(-1, size_entry)) 847 | size_min.pack(expand=True, fill='both', pady=1, padx=1) 848 | 849 | brd_siz_pls = tk.Frame(brd_siz_frm, bg=BG_COLOR) 850 | brd_siz_pls.grid(row=0, column=1, sticky='nsew') 851 | size_pls = tk.Button(brd_siz_pls, text=">", bd=0, bg=FONT_COLOR, fg=BG_COLOR, 852 | command=lambda: siz_min_pls(1, size_entry)) 853 | size_pls.pack(expand=True, fill='both', pady=1, padx=1) 854 | 855 | brd_siz_frm.columnconfigure(0, weight=1) 856 | brd_siz_frm.columnconfigure(1, weight=1) 857 | 858 | brd_ok_butt = tk.Frame(folders_req, bg=BG_COLOR) 859 | brd_ok_butt.grid(row=3, column=0, columnspan=2, sticky='nsew') 860 | ok_butt = tk.Button(brd_ok_butt, text="OK", bd=0, bg=FONT_COLOR, fg=BG_COLOR, 861 | activebackground=ACC_COLOR1, command=accept_font) 862 | ok_butt.pack(expand=True, fill='both', pady=1, padx=1) 863 | 864 | brd_cancel_butt = tk.Frame(folders_req, bg=BG_COLOR) 865 | brd_cancel_butt.grid(row=3, column=2, columnspan=2, sticky='nsew') 866 | cancel_butt = tk.Button(brd_cancel_butt, text="cancel", bd=0, bg=FONT_COLOR, fg=BG_COLOR, 867 | activebackground=ACC_COLOR1, command=folders_req.destroy) 868 | cancel_butt.pack(expand=True, fill='both', pady=1, padx=1) 869 | 870 | font_preview = tk.Entry(folders_req, justify='center', bd=0, 871 | bg=BG_COLOR, fg=FONT_COLOR, font=(FONT[0], FONT[1], FONT[2])) 872 | font_preview.insert('insert', 'Diffusion') 873 | font_preview.grid(row=4, columnspan=4, sticky='nsew') 874 | 875 | folders_req.rowconfigure(0, weight=1) 876 | folders_req.columnconfigure(0, weight=1) 877 | folders_req.columnconfigure(1, weight=1) 878 | folders_req.columnconfigure(2, weight=1) 879 | folders_req.columnconfigure(3, weight=0) 880 | 881 | folders_req.resizable(True, True) 882 | folders_req.update_idletasks() 883 | font_req_width = int(config.winfo_width() / 2) 884 | folders_req.geometry(f'{font_req_width}x{config.winfo_height()}+{config.winfo_x()}+{config.winfo_y()}') 885 | 886 | global config 887 | global conf_entries 888 | global conf_labels 889 | global bt_color_list 890 | global dummy 891 | 892 | # Create window 893 | config = tk.Toplevel() 894 | config.title('Configuration') 895 | config.grab_set() 896 | config.focus_set() 897 | config.option_add('*font', FONT) 898 | config.resizable(True, True) 899 | config_frame = tk.Frame(config, bg=BG_COLOR) 900 | config_frame.pack(expand=True, fill='both') 901 | 902 | # Blank label to separate interface fro window top 903 | dummy = tk.Label(config_frame, text=' ', bg=BG_COLOR, fg=FONT_COLOR) 904 | dummy.grid(row=0) 905 | 906 | config_frame.grid_columnconfigure(0, weight=0) 907 | # config_frame.grid_columnconfigure(1, weight=0) 908 | config_frame.grid_columnconfigure(1, weight=1) 909 | 910 | # Interface widgets content 911 | conf_cont = [['number of columns', COL_NBR, None, None], 912 | ['number of rows', ROW_NBR, None, None], 913 | ['gird image size', GRID_IMG_SZ, None, None], 914 | ['preview image size', INFO_IMG_SZ, None, None], 915 | ['button height', BUTT_HEIGHT, None, None], 916 | ['font name', FONT[0], 'get', None, font_requester], 917 | ['font size', FONT[1], 'get', None, font_requester], 918 | ['font weight', FONT[2], 'get', None, font_requester], 919 | ['background color', BG_COLOR, 'pick', BG_COLOR, pick_color], 920 | ['main color', FONT_COLOR, 'pick', FONT_COLOR, pick_color], 921 | ['accent color 1', ACC_COLOR1, 'pick', ACC_COLOR1, pick_color], 922 | ['accent color 2', ACC_COLOR2, 'pick', ACC_COLOR2, pick_color], 923 | ['alert color', ALERT_COLOR, 'pick', ALERT_COLOR, pick_color]] 924 | # ['Default path', TOP_PATH, 'get', None, change_config_path]] 925 | 926 | brd_bt_color_list = [] 927 | bt_color_list = [] 928 | conf_entries = [] 929 | conf_labels = [] 930 | for r, cont in enumerate(conf_cont, 1): 931 | label = tk.Label(config_frame, text=cont[0], bg=BG_COLOR, fg=FONT_COLOR) 932 | label.grid(row=r, column=0, sticky='e', padx=(20, 0)) 933 | conf_labels.append(label) 934 | 935 | brd_bt_tbox = tk.Frame(config_frame, bg=BG_COLOR) 936 | brd_bt_tbox.grid(row=r, column=1, sticky='wens') 937 | tbox = tk.Entry(brd_bt_tbox, bg=ACC_COLOR1, fg=BG_COLOR, width=30, bd=0, name=str(r), 938 | selectbackground=FONT_COLOR, selectforeground=ACC_COLOR2) 939 | tbox.insert('insert', cont[1]) 940 | tbox.pack(expand=True, fill='both', pady=1, padx=1) 941 | conf_entries.append(tbox) 942 | 943 | config_frame.rowconfigure(r, weight=1) 944 | 945 | if r > 8 and r < 14: 946 | tbox.bind('', lambda event, nbr=r: change_color(nbr)) 947 | tbox.bind('', lambda event, nbr=r: change_color(nbr)) 948 | 949 | if (r > 0 and r < 6): 950 | tbox.bind('', lambda event, widget=tbox: test_int(widget)) 951 | tbox.bind('', lambda event, widget=tbox: test_int(widget)) 952 | 953 | if cont[2]: 954 | brd_bt_action = tk.Frame(config_frame, bg=BG_COLOR) 955 | brd_bt_action.grid(row=r, column=2, sticky='wens') 956 | action = tk.Button(brd_bt_action, text=cont[2], bd=0, 957 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 958 | action.bind('', lambda event, func=cont[4], nbr=r, cur_col=cont[3]: func(nbr, cur_col)) 959 | action.pack(expand=True, fill='both', pady=1, padx=1) 960 | 961 | if cont[3]: 962 | brd_bt_color = tk.Frame(config_frame, bg=BG_COLOR) 963 | brd_bt_color.grid(row=r, column=3, sticky='wens', padx=(0, 20)) 964 | color = tk.Button(brd_bt_color, text=' ', bd=0, bg=cont[3], activebackground=cont[3]) 965 | color.pack(expand=True, fill='both', pady=1, padx=1) 966 | color.bind('', lambda event, func=cont[4], nbr=r, cur_col=cont[3]: func(nbr, cur_col)) 967 | brd_bt_color_list.append(brd_bt_color) 968 | bt_color_list.append(color) 969 | 970 | brd_bt_color_list[0]['bg'] = FONT_COLOR 971 | 972 | # Align path text to the right 973 | # conf_entries[13].xview_moveto(1) 974 | 975 | conf_entries[6].bind('', lambda e: change_button_height(conf_entries[6].get())) 976 | conf_entries[6].bind('', lambda e: change_button_height(conf_entries[6].get())) 977 | 978 | conf_entries[7].bind('', lambda e: test_weight(conf_entries[7].get())) 979 | conf_entries[7].bind('', lambda e: test_weight(conf_entries[7].get())) 980 | 981 | # conf_entries[r - 1].bind('', lambda e, nbr=r: test_path(nbr, conf_entries[r - 1].get())) 982 | # conf_entries[r - 1].bind('', lambda e, nbr=r: test_path(nbr, conf_entries[r - 1].get())) 983 | 984 | btn_frame = tk.Frame(config_frame, bg=BG_COLOR) 985 | btn_frame.grid(row=r + 1, columnspan=4, sticky='ew', pady=(20, 20)) 986 | btn_frame.grid_columnconfigure(0, weight=1) 987 | btn_frame.grid_columnconfigure(1, weight=1) 988 | 989 | brd_bt_btn_accept = tk.Frame(btn_frame, bg=BG_COLOR) 990 | brd_bt_btn_accept.grid(row=0, column=0, sticky='wens', padx=(20, 0)) 991 | btn_accept = tk.Button(brd_bt_btn_accept, text='OK (restart)', 992 | bg=FONT_COLOR, fg=BG_COLOR, bd=0, activebackground=ACC_COLOR1) 993 | btn_accept['command'] = lambda conf_entries=conf_entries: accept_config(btn_accept, conf_entries) 994 | btn_accept.pack(expand=True, fill='both', pady=1, padx=1) 995 | 996 | brd_btn_cancel = tk.Frame(btn_frame, bg=BG_COLOR) 997 | brd_btn_cancel.grid(row=0, column=1, sticky='wens', padx=(0, 20)) 998 | btn_cancel = tk.Button(brd_btn_cancel, text='cancel', bd=0, command=config.destroy, 999 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 1000 | btn_cancel.pack(expand=True, fill='both', pady=1, padx=1) 1001 | 1002 | config.update() 1003 | x = root.winfo_x() + root.winfo_width() - config.winfo_width() 1004 | y = root.winfo_y() + 30 + BUTT_HEIGHT 1005 | config.geometry(f'+{x}+{y}') 1006 | 1007 | 1008 | def folders_requester(): 1009 | '''Create a folders requester''' 1010 | 1011 | def tree_sort_column(widget, col, reverse): 1012 | l = [(widget.set(k, col), k) for k in widget.get_children('')] 1013 | l.sort(reverse=reverse) 1014 | 1015 | for index, (_, k) in enumerate(l): 1016 | widget.move(k, '', index) 1017 | 1018 | widget.heading(col, command=lambda: tree_sort_column(widget, col, not reverse)) 1019 | 1020 | def tree_double_click(event, widget, parent): 1021 | column_subf = widget.column('subf', 'width') 1022 | column_path = widget.column('path', 'width') 1023 | column_active = widget.column('active', 'width') 1024 | 1025 | if event.x <= column_subf: 1026 | toggle_subfolders(widget) 1027 | elif event.x <= column_path + column_subf: 1028 | change_folder(widget, parent) 1029 | elif event.x <= column_active + column_path + column_subf: 1030 | toggle_active_folders(widget) 1031 | 1032 | def add_folder(widget, parent): 1033 | '''Add path''' 1034 | 1035 | folder_selected = filedialog.askdirectory(parent=parent) 1036 | 1037 | if folder_selected: 1038 | folder_selected = os.path.normpath(folder_selected) 1039 | widget.insert('', 'end', text=('', folder_selected, ''), value=('', folder_selected, '')) 1040 | 1041 | def change_folder(widget, parent): 1042 | '''Change path''' 1043 | if not widget.selection(): 1044 | return 1045 | 1046 | folder_selected = filedialog.askdirectory(parent=parent) 1047 | 1048 | if folder_selected: 1049 | folder_selected = os.path.normpath(folder_selected) 1050 | item = widget.selection()[0] 1051 | widget.item(item, text=('', folder_selected, ''), value=('', folder_selected, '')) 1052 | 1053 | def toggle_subfolders(widget): 1054 | if not widget.selection(): 1055 | return 1056 | 1057 | for item in widget.selection(): 1058 | item_values = widget.item(item)['values'] 1059 | subfolders = 'YES' if item_values[0] == '' else '' 1060 | widget.item(item, text=(subfolders, item_values[1], item_values[2]), value=(subfolders, item_values[1], item_values[2])) 1061 | 1062 | def toggle_active_folders(widget): 1063 | if not widget.selection(): 1064 | return 1065 | 1066 | for item in widget.selection(): 1067 | item_values = widget.item(item)['values'] 1068 | active = 'NO' if item_values[2] == '' else '' 1069 | widget.item(item, text=(item_values[0], item_values[1], active), value=(item_values[0], item_values[1], active)) 1070 | 1071 | def delete_folders(widget, parent): 1072 | if not widget.selection(): 1073 | return 1074 | quantity = len(widget.selection()) 1075 | message = f'{widget.item(widget.focus())["values"][1]} ' if quantity == 1 else f'{len(widget.selection())} items' 1076 | query = tk.messagebox.askquestion(title='Delete item', message=f'Really delete {message}?', parent=parent) 1077 | if query == 'yes': 1078 | for item in widget.selection(): 1079 | widget.delete(item) 1080 | 1081 | def save_folders(widget, parent): 1082 | global folders 1083 | 1084 | folders = [] 1085 | for item in widget.get_children(): 1086 | child = widget.item(item)["values"] 1087 | subfolders = '1' if child[0] == 'YES' else '0' 1088 | active = '0' if child[2] == 'NO' else '1' 1089 | folders.append((subfolders, child[1], active)) 1090 | 1091 | json_object = json.dumps(folders) 1092 | 1093 | with open(FOLDERS_FILE, "w") as f: 1094 | f.write(json_object) 1095 | 1096 | parent.destroy() 1097 | 1098 | update_grid() 1099 | 1100 | folders_req = tk.Toplevel() 1101 | folders_req.title('Paths') 1102 | 1103 | root.grab_release() 1104 | folders_req.grab_set() 1105 | folders_req.focus_set() 1106 | 1107 | folders_req.rowconfigure(0, weight=1) 1108 | folders_req.columnconfigure(0, weight=1) 1109 | folders_req.columnconfigure(1, weight=0) 1110 | 1111 | folders_tree = ttk.Treeview(folders_req, column=('subf', 'path', 'active'), show='headings', height=100) 1112 | folders_tree.heading('#0', text=' \n\n') 1113 | folders_tree.heading('subf', text='subf\n', command=lambda: tree_sort_column(folders_tree, 'subf', False)) 1114 | folders_tree.heading('path', text='paths\n', command=lambda: tree_sort_column(folders_tree, 'path', False)) 1115 | folders_tree.heading('active', text='active\n', command=lambda: tree_sort_column(folders_tree, 'active', False)) 1116 | folders_tree.column('subf', width=FONT[1] * 3, stretch='no') 1117 | folders_tree.column('active', width=FONT[1] * 4, stretch='no') 1118 | folders_tree.option_add('*font', FONT) 1119 | folders_tree.grid(row=0, sticky='news') 1120 | 1121 | folders_tree.bind("", lambda event: tree_double_click(event, folders_tree, folders_req)) 1122 | folders_tree.bind("", lambda event: toggle_subfolders(folders_tree)) 1123 | folders_tree.bind("", lambda event: toggle_subfolders(folders_tree)) 1124 | folders_tree.bind("", lambda event: toggle_active_folders(folders_tree)) 1125 | folders_tree.bind("", lambda event: toggle_active_folders(folders_tree)) 1126 | 1127 | sb_folders = ttk.Scrollbar(folders_req, orient='vertical') 1128 | sb_folders.grid(row=0, column=1, sticky='news') 1129 | folders_tree.configure(yscrollcommand=sb_folders.set) 1130 | sb_folders.config(command=folders_tree.yview) 1131 | 1132 | for folder in folders: 1133 | recursive = 'YES' if folder[0] == '1' else '' 1134 | active = 'NO' if folder[2] == '0' else '' 1135 | folders_tree.insert('', 'end', text=(recursive, folder[1], active), value=(recursive, folder[1], active)) 1136 | 1137 | brd_upper_butt = tk.Frame(folders_req, bg=BG_COLOR) 1138 | brd_upper_butt.grid(row=2, column=0, columnspan=2, sticky='nsew') 1139 | 1140 | brd_upper_butt.columnconfigure(0, weight=1) 1141 | brd_upper_butt.columnconfigure(1, weight=1) 1142 | brd_upper_butt.columnconfigure(2, weight=1) 1143 | brd_upper_butt.columnconfigure(3, weight=1) 1144 | brd_upper_butt.columnconfigure(4, weight=1) 1145 | brd_upper_butt.columnconfigure(5, weight=1) 1146 | 1147 | brd_get_butt = tk.Frame(brd_upper_butt, bg=BG_COLOR) 1148 | brd_get_butt.grid(row=0, column=0, sticky='nsew') 1149 | get_butt = tk.Button(brd_get_butt, text="add", bg=FONT_COLOR, fg=BG_COLOR, 1150 | activebackground=ACC_COLOR1, bd=0) 1151 | get_butt.bind('', lambda event: add_folder(folders_tree, folders_req)) 1152 | get_butt.pack(expand=True, fill='both', pady=1, padx=1) 1153 | 1154 | brd_change_butt = tk.Frame(brd_upper_butt, bg=BG_COLOR) 1155 | brd_change_butt.grid(row=0, column=1, sticky='nsew') 1156 | change_butt = tk.Button(brd_change_butt, text="change", bg=FONT_COLOR, fg=BG_COLOR, 1157 | activebackground=ACC_COLOR1, bd=0, command=lambda: change_folder(folders_tree, folders_req)) 1158 | change_butt.pack(expand=True, fill='both', pady=1, padx=1) 1159 | 1160 | brd_open_butt = tk.Frame(brd_upper_butt, bg=BG_COLOR) 1161 | brd_open_butt.grid(row=0, column=2, sticky='nsew') 1162 | open_butt = tk.Button(brd_open_butt, text="open", bg=FONT_COLOR, fg=BG_COLOR, 1163 | activebackground=ACC_COLOR1, bd=0, command=lambda: explore_folder(folders_tree.item(folders_tree.focus()))) 1164 | open_butt.pack(expand=True, fill='both', pady=1, padx=1) 1165 | 1166 | brd_subf_butt = tk.Frame(brd_upper_butt, bg=BG_COLOR) 1167 | brd_subf_butt.grid(row=0, column=3, sticky='nsew') 1168 | subf_butt = tk.Button(brd_subf_butt, text="toggle subfolders", bg=FONT_COLOR, fg=BG_COLOR, 1169 | activebackground=ACC_COLOR1, bd=0, command=lambda: toggle_subfolders(folders_tree)) 1170 | subf_butt.pack(expand=True, fill='both', pady=1, padx=1) 1171 | 1172 | brd_active_butt = tk.Frame(brd_upper_butt, bg=BG_COLOR) 1173 | brd_active_butt.grid(row=0, column=4, sticky='nsew') 1174 | active_butt = tk.Button(brd_active_butt, text="toggle active", bg=FONT_COLOR, fg=BG_COLOR, 1175 | activebackground=ACC_COLOR1, bd=0, command=lambda: toggle_active_folders(folders_tree)) 1176 | active_butt.pack(expand=True, fill='both', pady=1, padx=1) 1177 | 1178 | brd_del_butt = tk.Frame(brd_upper_butt, bg=BG_COLOR) 1179 | brd_del_butt.grid(row=0, column=5, sticky='nsew') 1180 | del_butt = tk.Button(brd_del_butt, text="delete", bg=ALERT_COLOR, fg=BG_COLOR, 1181 | activebackground=ACC_COLOR1, bd=0, command=lambda: delete_folders(folders_tree, folders_req)) 1182 | del_butt.pack(expand=True, fill='both', pady=1, padx=1) 1183 | 1184 | brd_lower_butt = tk.Frame(folders_req, bg=BG_COLOR) 1185 | brd_lower_butt.grid(row=3, column=0, columnspan=2, sticky='nsew') 1186 | 1187 | brd_lower_butt.columnconfigure(0, weight=1) 1188 | brd_lower_butt.columnconfigure(1, weight=1) 1189 | 1190 | brd_ok_butt = tk.Frame(brd_lower_butt, bg=BG_COLOR) 1191 | brd_ok_butt.grid(row=0, column=0, sticky='nsew') 1192 | ok_butt = tk.Button(brd_ok_butt, text='OK (rebuild)', bd=0, bg=FONT_COLOR, fg=BG_COLOR, 1193 | activebackground=ACC_COLOR1, command=lambda: save_folders(folders_tree, folders_req)) 1194 | ok_butt.pack(expand=True, fill='both', pady=1, padx=1) 1195 | 1196 | brd_cancel_butt = tk.Frame(brd_lower_butt, bg=BG_COLOR) 1197 | brd_cancel_butt.grid(row=0, column=1, sticky='nsew') 1198 | cancel_butt = tk.Button(brd_cancel_butt, text='cancel', bd=0, bg=FONT_COLOR, fg=BG_COLOR, 1199 | activebackground=ACC_COLOR1, command=folders_req.destroy) 1200 | cancel_butt.pack(expand=True, fill='both', pady=1, padx=1) 1201 | 1202 | folders_req.resizable(True, True) 1203 | folders_req.update_idletasks() 1204 | folders_req.update() 1205 | x = root.winfo_x() + root.winfo_width() - 800 1206 | y = root.winfo_y() + 30 + BUTT_HEIGHT 1207 | folders_req.geometry(f'800x400+{x}+{y}') 1208 | 1209 | 1210 | def copy_to_clipboard(event, info=''): 1211 | '''Copy to clipboard''' 1212 | try: 1213 | if event: 1214 | if event.num == 1: 1215 | info = text_info.get('sel.first', 'sel.last') 1216 | else: 1217 | text_info.focus_set() 1218 | text_info.tag_add('sel', "1.0", 'end') 1219 | info = text_info.get('1.0', 'end') 1220 | text_info.update() 1221 | time.sleep(0.1) 1222 | text_info.tag_remove('sel', "1.0", 'end') 1223 | root.clipboard_clear() 1224 | root.clipboard_append(info) 1225 | root.update() 1226 | except tk.TclError: 1227 | return 1228 | 1229 | 1230 | def explore_folder(path, select=False): 1231 | '''Open explorer on the current location''' 1232 | 1233 | if not image_list: 1234 | return 1235 | 1236 | if not path or path == 0: 1237 | return 1238 | 1239 | if isinstance(path, dict): 1240 | if not path['values']: 1241 | return 1242 | 1243 | path = [path['values'][1]] 1244 | 1245 | for p in path: 1246 | 1247 | if isinstance(p, int): 1248 | p = image_list[p]['file'] 1249 | 1250 | p = os.path.normpath(p) 1251 | 1252 | if OS == 'Linux': 1253 | option = '--select' if select else '' 1254 | default_app = subprocess.run(['xdg-mime', 'query', 'default', 'inode/directory'], 1255 | stdout=subprocess.PIPE).stdout.decode('utf-8').strip() 1256 | 1257 | if default_app == 'org.kde.dolphin.desktop': 1258 | subprocess.Popen(['dolphin', option, p]) 1259 | elif default_app == 'nautilus.desktop': 1260 | subprocess.Popen(['nautilus', option, p]) 1261 | else: 1262 | subprocess.Popen(['xdg-open', option, p]) 1263 | 1264 | elif OS == 'Darwin': 1265 | option = '-R' if select else '' 1266 | subprocess.Popen(["open", option, p]) 1267 | 1268 | else: 1269 | option = '/select,' if select else '/open,' 1270 | subprocess.Popen(["explorer ", option, p]) 1271 | 1272 | 1273 | def save_info(idx, ask=False): 1274 | '''Save the image information to a text file''' 1275 | 1276 | if not idx: 1277 | return 1278 | 1279 | for i in idx: 1280 | embed_text = image_list[i]['txt_info'] 1281 | 1282 | orig_file = image_list[i]['file'] 1283 | orig_file = os.path.splitext(orig_file)[0] 1284 | orig_file = f'{orig_file}.txt' 1285 | orig_name = os.path.basename(orig_file) 1286 | 1287 | if ask: 1288 | orig_file = filedialog.asksaveasfile(initialfile=orig_name) 1289 | if not orig_file: 1290 | continue 1291 | orig_file = orig_file.name 1292 | 1293 | with open(orig_file, 'w') as f: 1294 | f.write(embed_text) 1295 | 1296 | 1297 | def reset_interface(): 1298 | '''Resets the program''' 1299 | 1300 | frame_all.destroy() 1301 | main() 1302 | 1303 | 1304 | def clear_info(): 1305 | '''Clear current image and text information''' 1306 | 1307 | img_info.config(bg=FONT_COLOR, text=PROGRAM_NAME, image='') 1308 | text_info['state'] = 'normal' 1309 | text_info.delete('1.0', 'end') 1310 | text_info.insert('insert', TEXT_INFO_DEFAULT) 1311 | text_info['state'] = 'disable' 1312 | 1313 | 1314 | def parse_key_value(text): 1315 | # Regular expression pattern to extract key-value pairs 1316 | pattern = r'([^:]+):\s*("[^"\\]*(?:\\.[^"\\]*)*"|[^,\n]+)' 1317 | text = text.replace('\\"', "'") 1318 | matches = re.findall(pattern, text) 1319 | key_values = {} 1320 | for match in matches: 1321 | key = match[0].strip() # Strip leading/trailing spaces 1322 | value = match[1].strip('"') # Strip quotes if present 1323 | key_values[key] = value 1324 | return key_values 1325 | 1326 | 1327 | def read_image_info(original_image, image_path): 1328 | '''Read the images embedded information''' 1329 | 1330 | def flatten_list(orig_list): 1331 | if orig_list == []: 1332 | return orig_list 1333 | if isinstance(orig_list[0], list): 1334 | return flatten_list(orig_list[0]) + flatten_list(orig_list[1:]) 1335 | return orig_list[:1] + flatten_list(orig_list[1:]) 1336 | 1337 | global new_pars 1338 | 1339 | source = '' 1340 | embed = OrderedDict() 1341 | embed_par = OrderedDict() 1342 | image_format = original_image.format.upper() 1343 | metadata_error = None 1344 | 1345 | if image_format == 'PNG': 1346 | embed = original_image.text 1347 | embed_raw = ''.join(str(embed)) 1348 | 1349 | elif image_format == 'JPEG': 1350 | img_exif = original_image._getexif() 1351 | embed_raw = ''.join(str(img_exif)) 1352 | 1353 | if img_exif is not None: 1354 | embed = list(img_exif.items())[1][1] 1355 | 1356 | if not isinstance(embed, int) and not isinstance(embed, str): 1357 | embed = [chr(d) for d in embed if d > 0] 1358 | embed = ''.join(embed) 1359 | embed = embed.removeprefix('UNICODE') 1360 | 1361 | if embed.startswith('Upscale:'): 1362 | embed = {'extras': embed} 1363 | else: 1364 | embed = {'parameters': embed} 1365 | 1366 | if embed: 1367 | try: 1368 | if isinstance(embed, int) or isinstance(embed, str): 1369 | embed = {} 1370 | 1371 | is_fooocus = '' 1372 | if 'fooocus_scheme' in embed.keys(): 1373 | is_fooocus = 'pure' 1374 | if embed['fooocus_scheme'] == 'a1111': 1375 | embed = {'parameters': embed['parameters']} 1376 | is_fooocus = 'a1111' 1377 | 1378 | if is_fooocus == 'pure': 1379 | source = 'fooocus' 1380 | parameters = json.loads(embed['parameters']) 1381 | for par in parameters: 1382 | title = par 1383 | title = title.strip().lower() 1384 | content = parameters[par] 1385 | if isinstance(content, list): 1386 | content = flatten_list(content) 1387 | content = map(str, content) 1388 | content = ', '.join(content) 1389 | if isinstance(content, int) or isinstance(content, float): 1390 | content = str(content) 1391 | content = content.strip().lower() 1392 | embed_par[title] = content 1393 | 1394 | elif 'parameters' in embed.keys(): 1395 | source = 'fooocus' if is_fooocus == 'a1111' else 'automatic1111' 1396 | parameters = embed['parameters'] 1397 | prompt = parameters.partition('Steps: ')[0] 1398 | negative_prompt = prompt.partition('Negative prompt: ')[2] 1399 | parameters = 'Steps: ' + parameters.partition('Steps: ')[2] 1400 | 1401 | if negative_prompt: 1402 | prompt = prompt.partition('Negative prompt: ')[0] 1403 | embed_par['prompt'] = prompt.strip().replace('\n', ', ') 1404 | embed_par['negative prompt'] = negative_prompt.strip().replace('\n', ', ') 1405 | else: 1406 | embed_par['prompt'] = prompt.strip() 1407 | 1408 | parameters = parse_key_value(parameters) 1409 | 1410 | for par in parameters: 1411 | title = par 1412 | title = title.strip().lstrip(', ').lower() 1413 | content = parameters[par] 1414 | content = content.strip().lower() 1415 | embed_par[title] = content 1416 | 1417 | elif 'extras' in embed.keys(): 1418 | source = 'automatic1111 extras' 1419 | modules = embed['extras'].split('\n') 1420 | embed_par = {} 1421 | for c, module in enumerate(modules): 1422 | parameters = parse_key_value(module) 1423 | for par in parameters: 1424 | if par: 1425 | title = par 1426 | title = f'{title.strip().lstrip(", ").lower()} {str(c + 1)}' 1427 | content = parameters[par] 1428 | content = content.strip().lower() 1429 | embed_par[title] = content 1430 | else: 1431 | try: 1432 | source = list(embed.keys())[0] 1433 | embed_par = json.loads(embed[source]) 1434 | except (json.decoder.JSONDecodeError, IndexError): 1435 | embed_par['embedded info'] = embed_raw 1436 | # Sorry for that but these metadata are a mess, never know what may come out of them. 1437 | except Exception as e: 1438 | embed_par['embedded info'] = embed_raw 1439 | metadata_error = (image_path, str(e)) 1440 | 1441 | else: 1442 | embed_par['embedded info'] = 'no information' 1443 | 1444 | # Additional information 1445 | real_size = original_image.size[0], original_image.size[1] 1446 | 1447 | img_format = os.path.basename(image_path) 1448 | img_format = img_format.split('.')[-1] 1449 | 1450 | file_time = os.path.getmtime(image_path) 1451 | file_time = datetime.datetime.fromtimestamp(file_time) 1452 | file_time = file_time.strftime(time_format) 1453 | 1454 | embed_par[' '] = '\n' 1455 | if source: 1456 | embed_par['source'] = source.lower() 1457 | embed_par['real_size'] = f'{real_size[0]} x {real_size[1]}' 1458 | embed_par['format'] = img_format 1459 | embed_par['created'] = str(file_time) 1460 | embed_par['path'] = os.path.normpath(image_path) 1461 | 1462 | dif_pars = list(set(embed_par.keys()) - set(TEXT_PARS)) 1463 | for par in dif_pars: 1464 | if par not in new_pars and par.strip() != '': 1465 | new_pars.append(par) 1466 | 1467 | temp_text_pars = TEXT_PARS 1468 | pos = len(temp_text_pars) - 6 1469 | temp_text_pars[pos:pos] = new_pars 1470 | temp_par = OrderedDict() 1471 | for item in temp_text_pars: 1472 | if item in embed_par: 1473 | temp_par[item] = embed_par[item] 1474 | embed_par = temp_par 1475 | 1476 | embed_list = [] 1477 | for key in embed_par: 1478 | if key != ' ': 1479 | embed_list.append(f'{key}: {embed_par[key]}\n') 1480 | else: 1481 | embed_list.append('\n') 1482 | 1483 | embed_txt = ''.join(embed_list) 1484 | 1485 | return embed_txt, embed_par, metadata_error 1486 | 1487 | 1488 | def progress_bar(amount, message, start=0): 1489 | # Create progress bar 1490 | restart = tk.Toplevel() 1491 | restart.title(f'Processing') 1492 | restart.resizable(False, False) 1493 | restart.bind('', lambda event: restart.destroy()) 1494 | restart_frame = tk.Frame(restart, bg=BG_COLOR) 1495 | restart_frame.pack(expand=True, fill='both') 1496 | 1497 | load_img = tk.Label(restart_frame, bg=BG_COLOR, fg=FONT_COLOR, 1498 | font=(FONT[0], int(FONT[1] * 1.2), FONT[2]), 1499 | text=message) 1500 | load_img.pack(pady=0, padx=50) 1501 | 1502 | loading = tk.Label(restart_frame, bg=BG_COLOR, fg=ACC_COLOR1, 1503 | font=(FONT[0], int(FONT[1] * 1.2), FONT[2]), 1504 | text=f'{start} of {amount}') 1505 | loading.pack(pady=0, padx=50) 1506 | 1507 | bar_lenght = GRID_IMG_SZ * (COL_NBR + BORDER * 2) / 2 1508 | style = ttk.Style() 1509 | style.configure('TProgressbar', relief='flat', borderwidth=0, background=FONT_COLOR, foreground=BG_COLOR) 1510 | progress = ttk.Progressbar(restart_frame, orient='horizontal', length=bar_lenght, mode='determinate') 1511 | progress.pack(pady=10, padx=50) 1512 | 1513 | # Update restart to get width and height 1514 | restart.update() 1515 | 1516 | x = root.winfo_x() + int((((GRID_IMG_SZ + BORDER * 2) * COL_NBR) - restart.winfo_width()) / 2) 1517 | y = root.winfo_y() + BUTT_HEIGHT + int((((GRID_IMG_SZ + BORDER * 2) * ROW_NBR) - restart.winfo_height()) / 2) 1518 | restart.geometry(f'+{x}+{y}') 1519 | 1520 | return loading, progress, restart 1521 | 1522 | 1523 | def save_image(event, idx, single=False): 1524 | 1525 | if idx == 0: 1526 | return 1527 | 1528 | if single: 1529 | dest_path = filedialog.askdirectory() 1530 | if not dest_path: 1531 | return 1532 | 1533 | for i in idx: 1534 | orig_file = image_list[i]['file'] 1535 | orig_name = os.path.basename(orig_file) 1536 | 1537 | if single: 1538 | dest_file = os.path.join(dest_path, orig_name) 1539 | else: 1540 | dest_file = filedialog.asksaveasfile(initialfile=orig_name) 1541 | if not dest_file: 1542 | continue 1543 | dest_file = dest_file.name 1544 | 1545 | shutil.copy2(orig_file, dest_file) 1546 | 1547 | 1548 | def overlay_info(parameter_string): 1549 | '''Overlay information on grid image''' 1550 | # if len(threading.enumerate()) > ROW_NBR: 1551 | # return 1552 | 1553 | global is_overlay 1554 | 1555 | for image_data in image_list: 1556 | image_data['has_image'] = False 1557 | 1558 | parameter_string = parameter_string.strip() 1559 | 1560 | if parameter_string != ALL_PARAMETERS and parameter_string != '': 1561 | is_overlay = parameter_string 1562 | else: 1563 | is_overlay = '' 1564 | 1565 | refresh_images(vsb.get()[0], vsb.get()[1]) 1566 | 1567 | 1568 | def expose_images(search_string, parameter_string, invert=False, exact=False): 1569 | '''Disable non matching images''' 1570 | # if len(threading.enumerate()) > ROW_NBR: 1571 | # return 1572 | 1573 | search_string = enter_search(search_string) 1574 | parameter_string = parameter_string.strip() 1575 | 1576 | for image_data in image_list: 1577 | embed_dict = image_data['dic_info'] 1578 | embed_text = image_data['txt_info'] 1579 | 1580 | if parameter_string != ALL_PARAMETERS and parameter_string != '': 1581 | embed_text = embed_dict.get(parameter_string, '') 1582 | 1583 | image_data['button'].config(bg=BG_COLOR, state='normal') 1584 | 1585 | if exact: 1586 | if invert: 1587 | if search_string.lower() == embed_text.lower(): 1588 | image_data['button'].config(bg=ACC_COLOR2, state='disabled') 1589 | else: 1590 | if search_string.lower() != embed_text.lower(): 1591 | image_data['button'].config(bg=ACC_COLOR2, state='disabled') 1592 | else: 1593 | if invert: 1594 | if search_string.lower() in embed_text.lower(): 1595 | image_data['button'].config(bg=ACC_COLOR2, state='disabled') 1596 | else: 1597 | if search_string.lower() not in embed_text.lower(): 1598 | image_data['button'].config(bg=ACC_COLOR2, state='disabled') 1599 | 1600 | 1601 | def enter_search(search_string): 1602 | '''When entering the search box''' 1603 | 1604 | if search_string == SEARCH_HELP: 1605 | entry_search.configure(fg=BG_COLOR) 1606 | entry_search.delete(0, 'end') 1607 | return '' 1608 | 1609 | return search_string 1610 | 1611 | 1612 | def search_images(search_string, parameter_string, invert=False, exact=False): 1613 | '''Sow images matching a search string''' 1614 | # if len(threading.enumerate()) > ROW_NBR: 1615 | # return 1616 | 1617 | global image_list 1618 | 1619 | if not image_list: 1620 | return 1621 | 1622 | if parameter_string == 'path': 1623 | search_string = os.path.normpath(search_string) 1624 | 1625 | search_string = enter_search(search_string) 1626 | parameter_string = parameter_string.strip() 1627 | 1628 | reverse_label = f'not ' if invert else '' 1629 | search_label = 'all' if search_string == '' else search_string 1630 | parameter_label = 'all' if parameter_string == '' else parameter_string 1631 | exact_label = ', exact' if exact else '' 1632 | loading, progress, restart = progress_bar(amount=1, message='Searching', start=1) 1633 | loading['text'] = f'{reverse_label}{search_label} in {parameter_label}{exact_label}' 1634 | progress['value'] = 100 1635 | progress.update() 1636 | 1637 | for image_data in image_list: 1638 | 1639 | embed_text = image_data['txt_info'] 1640 | embed_dict = image_data['dic_info'] 1641 | 1642 | embed_search = embed_text 1643 | 1644 | image_data['search'] = False 1645 | 1646 | if parameter_string != ALL_PARAMETERS and parameter_string != '': 1647 | embed_search = embed_dict.get(parameter_string, '') 1648 | 1649 | if exact: 1650 | if invert: 1651 | if search_string.lower() != embed_search.lower(): 1652 | image_data['search'] = True 1653 | else: 1654 | if search_string.lower() == embed_search.lower(): 1655 | image_data['search'] = True 1656 | else: 1657 | if invert: 1658 | if search_string.lower() not in embed_search.lower(): 1659 | image_data['search'] = True 1660 | else: 1661 | if search_string.lower() in embed_search.lower(): 1662 | image_data['search'] = True 1663 | 1664 | image_list.sort(key=itemgetter('search'), reverse=True) 1665 | 1666 | if parameter_string != 'path': 1667 | lbl_path.current(0) 1668 | 1669 | modify_grid() 1670 | clear_info() 1671 | 1672 | restart.destroy() 1673 | 1674 | 1675 | def sort_images(parameter_string, reverse=None): 1676 | '''Sort the images''' 1677 | # if len(threading.enumerate()) > ROW_NBR: 1678 | # return 1679 | 1680 | global image_list 1681 | global sort_reverse 1682 | 1683 | parameter_string = parameter_string.strip() 1684 | 1685 | if parameter_string == ALL_PARAMETERS or parameter_string == '': 1686 | parameter_string = 'created' 1687 | 1688 | if reverse is not None: 1689 | sort_reverse = not reverse 1690 | 1691 | reverse_label = f', reverse' if sort_reverse else '' 1692 | loading, progress, restart = progress_bar(amount=1, message='Sorting', start=1) 1693 | loading['text'] = f'{parameter_string}{reverse_label}' 1694 | progress['value'] = 100 1695 | progress.update() 1696 | 1697 | for c, image_data in enumerate(image_list): 1698 | 1699 | embed_dict = image_data['dic_info'] 1700 | 1701 | sort = embed_dict.get(parameter_string, '') 1702 | if parameter_string == 'created': 1703 | sort = datetime.datetime.strptime(sort, time_format) 1704 | image_list[c]['sort'] = sort 1705 | 1706 | # if all items are int make them all int() 1707 | if all(str(items['sort']).isdigit() or items['sort'] == '' for items in image_list): 1708 | for c, image_data in enumerate(image_list): 1709 | image_list[c]['sort'] = int(image_data['sort']) if image_data['sort'] != '' else 0 1710 | 1711 | sort_reverse = not sort_reverse 1712 | image_list.sort(key=itemgetter('sort'), reverse=sort_reverse) 1713 | 1714 | modify_grid() 1715 | clear_info() 1716 | 1717 | restart.destroy() 1718 | 1719 | 1720 | def modify_grid(): 1721 | global prev_button 1722 | global multi_index 1723 | global current_index 1724 | 1725 | if not image_list: 1726 | return 1727 | 1728 | images = 0 1729 | bx = 0 1730 | by = 0 1731 | for idx, image_data in enumerate(image_list): 1732 | 1733 | button = image_data['button'] 1734 | 1735 | if not image_data['search']: 1736 | button.grid_forget() 1737 | continue 1738 | 1739 | button.grid(row=bx, column=by) 1740 | button = modify_button(button, idx) 1741 | 1742 | by += 1 1743 | if by % COL_NBR == 0: 1744 | by = 0 1745 | bx += 1 1746 | 1747 | images += 1 1748 | 1749 | button_unselect(button) 1750 | 1751 | lbl_files.config(text=f'{images} images') 1752 | 1753 | button = None 1754 | prev_button = None 1755 | current_index = 0 1756 | multi_index = [] 1757 | 1758 | # prev_button = button 1759 | 1760 | canvas.yview_moveto('0') 1761 | refresh_images(vsb.get()[0], vsb.get()[1]) 1762 | 1763 | canvas_height = max((images // COL_NBR) * (GRID_IMG_SZ + BORDER * 2) + GRID_IMG_SZ, canvas.winfo_height()) 1764 | canvas.configure(scrollregion=(0, 0, canvas.winfo_width(), canvas_height)) 1765 | 1766 | 1767 | def search_paths(): 1768 | path = lbl_path.get() 1769 | 1770 | if path == ALL_FOLDERS or path == '': 1771 | path = '' 1772 | 1773 | lbl_path.selection_clear() 1774 | search_images(path, 'path') 1775 | 1776 | 1777 | def generate_image_list(files): 1778 | global image_list_master 1779 | global COMBO_VALUES 1780 | global TEXT_PARS 1781 | 1782 | t = len(files) 1783 | loading, progress, restart = progress_bar(t, 'Loading data, please wait.') 1784 | 1785 | # Get images data 1786 | image_list_temp = [] 1787 | image_error = [] 1788 | metadata_errors = [] 1789 | 1790 | for idx, file in enumerate(files): 1791 | try: 1792 | original_image = Image.open(file) 1793 | except UnidentifiedImageError: 1794 | image_error.append(file) 1795 | continue 1796 | embed_text, embed_dict, metadata_error = read_image_info(original_image, file) 1797 | if metadata_error: 1798 | metadata_errors.append(metadata_error) 1799 | 1800 | item_dict = {'button': None, 1801 | 'has_image': False, 1802 | 'txt_info': embed_text, 1803 | 'dic_info': embed_dict, 1804 | 'file': os.path.normpath(file), 1805 | 'search': True, 1806 | 'sort': ''} 1807 | 1808 | image_list_temp.append(item_dict) 1809 | 1810 | prog = int(idx / t * 100) 1811 | try: 1812 | loading['text'] = f'{idx} of {t}' 1813 | progress['value'] = prog 1814 | except tk.TclError: 1815 | break 1816 | progress.update() 1817 | 1818 | lbl_files.config(text=f'{idx} images') 1819 | 1820 | image_list_master = image_list_temp 1821 | 1822 | lbl_files.config(text=f'{idx} images') 1823 | restart.destroy() 1824 | 1825 | if metadata_errors: 1826 | metadata_errors_copy = '' 1827 | metadata_errors_clipboard = '' 1828 | for n, error in enumerate(metadata_errors): 1829 | error_str = ' - '.join(error) 1830 | error_str_clipboard = '", Error: "'.join(error) 1831 | if n < 10: 1832 | metadata_errors_copy += f'{error_str}\n' 1833 | elif n == 10: 1834 | metadata_errors_copy += 'and more...\n' 1835 | 1836 | metadata_errors_clipboard += f'Path: "{error_str_clipboard}"\n' 1837 | 1838 | tense = [''] if len(metadata_errors) <= 1 else ['s'] 1839 | 1840 | copy_to_clipboard(None, metadata_errors_clipboard) 1841 | 1842 | message = (f'Failed to parse metadata from {len(metadata_errors)} image{tense[0]}.\n\n' 1843 | f'{metadata_errors_copy}\n' 1844 | f'Full information copied to the clipboard.\n\n') 1845 | tk.messagebox.showinfo(title='Metadata error.', message=message, parent=root) 1846 | 1847 | if image_error: 1848 | image_error_copy = '\n'.join(image_error) 1849 | 1850 | tense = [''] if len(image_error) <= 1 else ['s'] 1851 | 1852 | copy_to_clipboard(None, image_error_copy) 1853 | 1854 | message = (f'Found {len(image_error)} image{tense[0]} with error.\n\n' 1855 | f'{image_error_copy}\n\n' 1856 | f'Path{tense[0]} copied to the clipboard.') 1857 | tk.messagebox.showinfo(title='Image error.', message=message, parent=root) 1858 | 1859 | if new_pars: 1860 | pars_copy = '\n'.join(new_pars) 1861 | 1862 | tense = ['', 'it', 'its', 'it', 'was'] if len(new_pars) <= 1 else ['s', 'them', 'their', 'they', 'were'] 1863 | 1864 | message = (f'Found {len(new_pars)} new parameter{tense[0]}.\n\n' 1865 | f'{pars_copy}\n\n' 1866 | f'{tense[3].capitalize()} {tense[4]} added to the parameters.txt.\n' 1867 | f'You can change {tense[2]} order on the file.\n') 1868 | 1869 | tk.messagebox.showinfo(title='New parameter found.', message=message, parent=root) 1870 | 1871 | copy_to_clipboard(None, pars_copy) 1872 | 1873 | with open(PARAMETERS_FILE, 'r') as file: 1874 | TEXT_PARS = file.read().splitlines() 1875 | 1876 | pos = len(TEXT_PARS) - 6 1877 | TEXT_PARS[pos:pos] = new_pars 1878 | 1879 | with open(PARAMETERS_FILE, 'w') as file: 1880 | file.write('\n'.join(TEXT_PARS)) 1881 | 1882 | COMBO_VALUES = TEXT_PARS 1883 | COMBO_VALUES.insert(0, ALL_PARAMETERS) 1884 | 1885 | return image_list_temp 1886 | 1887 | 1888 | def update_grid(): 1889 | '''Update new / deleted images files''' 1890 | 1891 | global image_list 1892 | 1893 | old_files = [image_data['file'] for image_data in image_list] 1894 | new_files = get_image_paths() 1895 | 1896 | if collections.Counter(old_files) == collections.Counter(new_files): 1897 | return True 1898 | 1899 | deleted_files = list(set(old_files) - set(new_files)) 1900 | new_files = list(set(new_files) - set(old_files)) 1901 | 1902 | # Make a new list without the images on the deleted list 1903 | # Also flag the ones not deleted as not having images so they can refresh 1904 | # Reset the search and sort parameters 1905 | image_list_temp = [] 1906 | for image_data in image_list: 1907 | if image_data['file'] not in deleted_files: 1908 | image_data['has_image'] = False 1909 | image_data['search'] = True 1910 | image_data['sort'] = '' 1911 | image_list_temp.append(image_data) 1912 | else: 1913 | image_data['button'].destroy() 1914 | 1915 | image_list = image_list_temp 1916 | 1917 | if new_files: 1918 | image_list_temp = generate_image_list(new_files) 1919 | image_list.extend(image_list_temp) 1920 | 1921 | image_list_temp = [] 1922 | for image_data in image_list: 1923 | image_list_temp.append(image_data.copy()) 1924 | image_list_temp[-1]['button'] = None 1925 | 1926 | with bz2.BZ2File(DATA_FILE, 'wb') as f: 1927 | pickle.dump(image_list_temp, f) 1928 | 1929 | del image_list_temp 1930 | 1931 | create_grid() 1932 | sort_images('', reverse=True) 1933 | clear_info() 1934 | 1935 | lbl_files.config(text=f'{len(image_list)} images') 1936 | 1937 | 1938 | def create_grid(): 1939 | '''Create grid buttons''' 1940 | global image_list 1941 | 1942 | if not image_list: 1943 | return 1944 | 1945 | for idx, image_data in enumerate(image_list): 1946 | if image_data['button'] is None: 1947 | button = create_button(blank_image, idx) 1948 | button.config(bg=BG_COLOR) # Needed to make disabled show correct bg color without affecting border at start 1949 | image_data['button'] = button 1950 | 1951 | 1952 | def create_button(image, index): 1953 | 1954 | button = tk.Button(frame_buttons, width=GRID_IMG_SZ, height=GRID_IMG_SZ, borderwidth=0, 1955 | relief='flat', name=str(index), image=image, highlightthickness=BORDER, 1956 | activebackground=BG_COLOR, bg=ACC_COLOR2, fg=ACC_COLOR1, bd=0, 1957 | compound="center", wraplength=GRID_IMG_SZ, 1958 | justify='center', padx=0, pady=0) 1959 | 1960 | return button 1961 | 1962 | 1963 | def modify_button(button, index): 1964 | 1965 | # button['command'] = lambda index=index: click_grid_image(index) 1966 | button.bind('', lambda event, index=index: click_grid_image(index)) 1967 | button.bind('', lambda event, index=index: click_grid_image(index, shift=True)) 1968 | button.bind('', lambda event, index=index: click_grid_image(index, control=True)) 1969 | 1970 | button.bind('', lambda event: grid_keys(event, -1)) 1971 | button.bind('', lambda event: grid_keys(event, 1)) 1972 | button.bind('', lambda event: grid_keys(event, -COL_NBR)) 1973 | button.bind('', lambda event: grid_keys(event, COL_NBR)) 1974 | 1975 | button.bind('', lambda event: grid_keys(event, -1, select=True)) 1976 | button.bind('', lambda event: grid_keys(event, 1, select=True)) 1977 | button.bind('', lambda event: grid_keys(event, -COL_NBR, select=True)) 1978 | button.bind('', lambda event: grid_keys(event, COL_NBR, select=True)) 1979 | 1980 | root.bind('', lambda event: grid_keys(event, -COL_NBR * ROW_NBR + COL_NBR)) 1981 | root.bind('', lambda event: grid_keys(event, COL_NBR * ROW_NBR - COL_NBR)) 1982 | 1983 | root.bind('', lambda event: grid_keys(event, 0, True)) 1984 | root.bind('', lambda event: grid_keys(event, 1, True)) 1985 | 1986 | button.bind('', lambda event, index=index: show_full_image([index])) 1987 | button.bind('', lambda event: show_full_image(multi_index)) 1988 | button.bind('', lambda event: show_full_image(multi_index)) 1989 | 1990 | button.bind('', lambda event, index=index: show_image([index])) 1991 | button.bind('', lambda event: show_image(multi_index)) 1992 | button.bind('', lambda event: show_image(multi_index)) 1993 | 1994 | button.bind("", lambda event, index=index: menu_popup(event, index)) 1995 | button.bind("", lambda event, index=index: menu_popup(event, index)) 1996 | 1997 | button.bind('', lambda event: save_image(event, multi_index)) 1998 | button.bind('', lambda event: save_image(event, multi_index)) 1999 | button.bind('', lambda event: save_image(event, multi_index, single=True)) 2000 | button.bind('', lambda event: save_image(event, multi_index, single=True)) 2001 | 2002 | button.bind('', lambda event: explore_folder(multi_index, select=True)) 2003 | button.bind('', lambda event: explore_folder(multi_index, select=True)) 2004 | 2005 | button.bind('', lambda event=2: copy_to_clipboard(event, current_index)) 2006 | button.bind('', lambda event=2: copy_to_clipboard(event, current_index)) 2007 | 2008 | button.bind('', lambda event: save_info(multi_index)) 2009 | button.bind('', lambda event: save_info(multi_index)) 2010 | button.bind('', lambda event: save_info(multi_index, ask=True)) 2011 | button.bind('', lambda event: save_info(multi_index, ask=True)) 2012 | 2013 | return button 2014 | 2015 | 2016 | def menu_popup(event, idx): 2017 | '''Open popup menu and make index global''' 2018 | global popup_index 2019 | 2020 | # button = image_list[idx]['button'] 2021 | if idx in multi_index: 2022 | click_grid_image(idx, right=True) 2023 | else: 2024 | click_grid_image(idx) 2025 | 2026 | # button.focus_set() 2027 | 2028 | pop_menu.delete(0) 2029 | if len(multi_index) > 1: 2030 | label = f'{len(multi_index)} selected' 2031 | else: 2032 | label = os.path.basename(image_list[idx]['file'])[:20] + '...' 2033 | 2034 | pop_menu.insert_cascade(0, label=label, foreground=FONT_COLOR, background=BG_COLOR, 2035 | activebackground=BG_COLOR, activeforeground=ACC_COLOR1, command=lambda: menu_items(7)) 2036 | 2037 | try: 2038 | pop_menu.tk_popup(event.x_root, event.y_root) 2039 | finally: 2040 | pop_menu.grab_release() 2041 | 2042 | 2043 | def menu_items(item): 2044 | '''Deal with menu choices''' 2045 | 2046 | if item == 7: 2047 | filename = '' 2048 | for i in multi_index: 2049 | filename += f'{image_list[i]["file"]}\n' 2050 | copy_to_clipboard(None, filename) 2051 | 2052 | elif item == 0: 2053 | show_full_image(multi_index) 2054 | 2055 | elif item == 1: 2056 | show_image(multi_index) 2057 | 2058 | elif item == 2: 2059 | explore_folder(multi_index, select=True) 2060 | 2061 | elif item == 6: 2062 | save_image(None, multi_index) 2063 | 2064 | elif item == 8: 2065 | save_image(None, multi_index, single=True) 2066 | 2067 | elif item == 4: 2068 | save_info(multi_index) 2069 | 2070 | elif item == 5: 2071 | save_info(multi_index, ask=True) 2072 | 2073 | elif item == 3: 2074 | embed_txt = '' 2075 | for i in multi_index: 2076 | embed_txt += f'{image_list[i]["txt_info"]}\n' 2077 | copy_to_clipboard(None, embed_txt) 2078 | 2079 | 2080 | def get_image_paths(): 2081 | '''Get images from the path''' 2082 | 2083 | global folders 2084 | 2085 | files = [] 2086 | folders = [] 2087 | 2088 | if not os.path.isfile(FOLDERS_FILE): 2089 | return None 2090 | 2091 | paths_user = [] 2092 | paths_uniques = [] 2093 | paths_upper_all = [] 2094 | paths_upper_clean = [] 2095 | paths_all = [] 2096 | try: 2097 | with open(FOLDERS_FILE, 'r') as file: 2098 | folders = json.load(file) 2099 | except (AttributeError, EOFError, ImportError, IndexError) as e: 2100 | tk.messagebox.showinfo(title='Error loading folders.', message=e, parent=root) 2101 | return None 2102 | finally: 2103 | 2104 | loading, progress, restart = progress_bar(amount=1, message='Scanning images', start=1) 2105 | loading['text'] = f'' 2106 | progress['value'] = 100 2107 | progress.update() 2108 | 2109 | ext = ['.png', '.jpg'] 2110 | for folder in folders: 2111 | path = folder[1] 2112 | recursive = True if folder[0] == '1' else False 2113 | active = True if folder[2] == '1' else False 2114 | if os.path.isdir(path) and active: 2115 | paths_user.append(os.path.normpath(path)) 2116 | for e in ext: 2117 | if recursive: 2118 | file = glob.glob(os.path.normpath(path + '/**/*' + e), recursive=True) 2119 | else: 2120 | file = glob.glob(os.path.normpath(path + '/*' + e), recursive=False) 2121 | files.extend(file) 2122 | 2123 | restart.destroy() 2124 | 2125 | paths_all.extend(paths_user) 2126 | 2127 | for p in files: 2128 | image_path = os.path.dirname(p) 2129 | if image_path not in paths_uniques: 2130 | paths_uniques.append(image_path) 2131 | paths_upper_all.append(os.path.dirname(image_path)) 2132 | 2133 | paths_all.extend(paths_uniques) 2134 | 2135 | while paths_upper_all: 2136 | path_upper_clean = paths_upper_all.pop() 2137 | if path_upper_clean in paths_upper_all: 2138 | paths_upper_clean.append(path_upper_clean) 2139 | paths_upper_all = [p for p in paths_upper_all if p != path_upper_clean] 2140 | 2141 | paths_all.extend(paths_upper_clean) 2142 | 2143 | if paths_all: 2144 | paths_all = list(set(paths_all)) 2145 | paths_all.sort() 2146 | 2147 | paths_all.insert(0, ALL_FOLDERS) 2148 | lbl_path.config(value=paths_all) 2149 | lbl_path.current(0) 2150 | 2151 | return files 2152 | 2153 | 2154 | def load_image_list(): 2155 | global image_list 2156 | global image_list_master 2157 | 2158 | if os.path.isfile(DATA_FILE): 2159 | try: 2160 | with bz2.BZ2File(DATA_FILE, 'rb') as f: 2161 | image_list = pickle.load(f) 2162 | except (pickle.UnpicklingError, 2163 | AttributeError, EOFError, ImportError, IndexError, 2164 | Exception) as e: 2165 | tk.messagebox.showinfo(title='Error loading data file.', message=e, parent=root) 2166 | return False 2167 | 2168 | else: 2169 | 2170 | files = get_image_paths() 2171 | 2172 | if files is None: 2173 | return False 2174 | image_list = generate_image_list(files) 2175 | with bz2.BZ2File(DATA_FILE, 'wb') as f: 2176 | pickle.dump(image_list, f) 2177 | 2178 | return True 2179 | 2180 | 2181 | def first_run(): 2182 | global sort_reverse 2183 | global is_overlay 2184 | 2185 | is_overlay = '' 2186 | 2187 | loading, progress, restart = progress_bar(amount=2, message='Initializing', start=1) 2188 | 2189 | progress['value'] = 50 2190 | progress.update() 2191 | 2192 | load_success = load_image_list() 2193 | 2194 | if load_success: 2195 | create_grid() 2196 | skipped = update_grid() 2197 | if skipped: 2198 | sort_images('', reverse=True) 2199 | 2200 | loading['text'] = f'2 of 2' 2201 | progress['value'] = 100 2202 | progress.update() 2203 | 2204 | # # Update buttons frames idle tasks to let tkinter calculate buttons sizes 2205 | # frame_buttons.update_idletasks() 2206 | 2207 | # Set the canvas scrolling region 2208 | canvas.config(scrollregion=canvas.bbox('all')) 2209 | 2210 | restart.destroy() 2211 | 2212 | 2213 | def main(): 2214 | '''Create the main window''' 2215 | 2216 | global vsb 2217 | global canvas 2218 | global img_info 2219 | global text_info 2220 | global lbl_files 2221 | global frame_all 2222 | global frame_buttons 2223 | global entry_search 2224 | global pop_menu 2225 | global lbl_key 2226 | global lbl_help 2227 | global lbl_path 2228 | global image_list 2229 | global image_list_master 2230 | global current_index 2231 | global blank_image 2232 | 2233 | # Create a blank image 2234 | blank_image = Image.new(color=(0, 0, 0), mode="RGB", size=(0, 0)) 2235 | blank_image.putalpha(0) 2236 | blank_image = ImageTk.PhotoImage(blank_image, name='blank_image') 2237 | 2238 | current_index = 0 2239 | 2240 | # Must be here to refresh when changing config 2241 | root.option_add('*font', FONT) 2242 | 2243 | root.bind('', lambda event: canvas.yview_scroll(-1, 'pages')) 2244 | root.bind('', lambda event: canvas.yview_scroll(1, 'pages')) 2245 | 2246 | root.bind('', lambda event: canvas.yview_moveto(0)) 2247 | root.bind('', lambda event: canvas.yview_moveto(1)) 2248 | 2249 | root.bind('', lambda event: explore_folder({'values': ['', lbl_path.get()]})) 2250 | root.bind('', lambda event: explore_folder({'values': ['', lbl_path.get()]})) 2251 | 2252 | root.bind('', lambda event: update_grid()) 2253 | root.bind('', lambda event: update_grid()) 2254 | 2255 | root.bind('', lambda event: entry_search.focus_set()) 2256 | root.bind('', lambda event: entry_search.focus_set()) 2257 | 2258 | root.bind('', lambda event: combo_params.focus_set()) 2259 | root.bind('', lambda event: combo_params.focus_set()) 2260 | 2261 | root.bind('', lambda event: sort_images(combo_params.get())) 2262 | root.bind('', lambda event: sort_images(combo_params.get())) 2263 | root.bind('', lambda event: sort_images('')) 2264 | root.bind('', lambda event: sort_images('')) 2265 | 2266 | root.bind('', lambda event=2: overlay_info(combo_params.get())) 2267 | root.bind('', lambda event=2: overlay_info(combo_params.get())) 2268 | root.bind('', lambda event=2: overlay_info('')) 2269 | root.bind('', lambda event=2: overlay_info('')) 2270 | 2271 | style = ttk.Style() 2272 | style.theme_use('default') 2273 | style.configure('TCombobox', relief='flat', background=FONT_COLOR, foreground=BG_COLOR, 2274 | selectbackground=FONT_COLOR, selectforeground=ACC_COLOR2, 2275 | arrowcolor=BG_COLOR, darkcolor=ACC_COLOR1, borderwidth=0) 2276 | style.map('TCombobox', fieldbackground=[('disabled', ACC_COLOR1), ('!disabled', ACC_COLOR1)]) 2277 | style.map('TCombobox', background=[('disabled', ACC_COLOR1), ('pressed', ACC_COLOR2), ('active', ACC_COLOR1)]) 2278 | root.option_add('*TCombobox*Listbox*Background', FONT_COLOR) 2279 | root.option_add('*TCombobox*Listbox*Foreground', BG_COLOR) 2280 | root.option_add('*TCombobox*Listbox*selectBackground', ACC_COLOR1) 2281 | root.option_add('*TCombobox*Listbox*selectForeground', BG_COLOR) 2282 | 2283 | style.configure('Treeview', borderwidth=0, background=ACC_COLOR1, foreground=BG_COLOR, fieldbackground=ACC_COLOR1) 2284 | style.map('Treeview', background=[('selected', FONT_COLOR)], foreground=[('selected', BG_COLOR)]) 2285 | style.configure('Treeview.Heading', background=FONT_COLOR, relief='solid', borderwidth=1, height=BUTT_HEIGHT) 2286 | style.map('Treeview.Heading', background=[('pressed', ACC_COLOR2), ('active', ACC_COLOR1)]) 2287 | 2288 | style.configure("Vertical.TScrollbar", troughcolor=BG_COLOR, background=FONT_COLOR, borderwidth=0, 2289 | arrowcolor=BG_COLOR, selectbackground=ACC_COLOR1, selectforeground=ACC_COLOR1, relief='flat') 2290 | style.map('Vertical.TScrollbar', background=[('disabled', BG_COLOR), ('active', ACC_COLOR1)]) 2291 | style.map('Vertical.TScrollbar', arrowcolor=[('disabled', BG_COLOR), ('active', BG_COLOR)]) 2292 | 2293 | # Frame to hold the entire interface 2294 | frame_all = tk.Frame(root, bg=BG_COLOR) 2295 | frame_all.pack(fill=None, expand=False) 2296 | 2297 | # Top frame for labels and buttons 2298 | frame_top = tk.Frame(frame_all, bg=BG_COLOR, height=BUTT_HEIGHT) 2299 | frame_top.grid(row=0, column=0, sticky='news') 2300 | frame_top.grid_propagate(False) 2301 | 2302 | frame_top.grid_rowconfigure(0, weight=1) 2303 | frame_top.grid_columnconfigure(0, weight=1) 2304 | frame_top.grid_columnconfigure(1, weight=0) 2305 | 2306 | # Top left sub frame for labels 2307 | frame_top_l = tk.Frame(frame_top, bg=BG_COLOR) 2308 | frame_top_l.grid(row=0, column=0, sticky='ewns') 2309 | 2310 | # frame_top_l.grid_rowconfigure(0, weight=1) 2311 | frame_top_l.grid_columnconfigure(0, weight=0) 2312 | frame_top_l.grid_columnconfigure(1, weight=1) 2313 | 2314 | # Top right sub frame for buttons 2315 | frame_top_r = tk.Frame(frame_top, width=INFO_IMG_SZ, bg=BG_COLOR) 2316 | frame_top_r.grid(row=0, column=1, sticky='ewns') 2317 | frame_top_r.grid_propagate(False) 2318 | 2319 | # frame_top_r.grid_rowconfigure(0, weight=1) 2320 | frame_top_r.grid_columnconfigure(0, weight=1) 2321 | frame_top_r.grid_columnconfigure(1, weight=1) 2322 | # frame_top_r.grid_columnconfigure(2, weight=2) 2323 | # frame_top_r.grid_columnconfigure(3, weight=2) 2324 | 2325 | # Number of files label, top left frame 2326 | lbl_files = tk.Label(frame_top_l, bg=BG_COLOR, fg=ACC_COLOR1, text=f'images') 2327 | lbl_files.grid(row=0, column=0, sticky='w', padx=(10, 10)) 2328 | 2329 | # Current path label, top left frame 2330 | brd_lbl_path = tk.Frame(frame_top_l, bg=BG_COLOR) 2331 | brd_lbl_path.grid(row=0, column=1, sticky='nsew') 2332 | lbl_path = ttk.Combobox(brd_lbl_path, name='folders_combo', state='readonly') 2333 | lbl_path.bind("<>", lambda event: search_paths()) 2334 | lbl_path.pack(expand=True, fill='both', pady=1, padx=1) 2335 | 2336 | # Refresh button, top right frame 2337 | brd_bt_refr = tk.Frame(frame_top_l, bg=BG_COLOR) 2338 | brd_bt_refr.grid(row=0, column=2, sticky='snwe') 2339 | butt_refr = tk.Button(brd_bt_refr, text='refresh', bd=0, command=update_grid, 2340 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2341 | butt_refr.pack(expand=True, fill='both', pady=1, padx=1) 2342 | 2343 | # Open path button, top right frame 2344 | brd_bt_open = tk.Frame(frame_top_l, bg=BG_COLOR) 2345 | brd_bt_open.grid(row=0, column=3, sticky='snwe') 2346 | butt_open = tk.Button(brd_bt_open, text='open path', bd=0, command=lambda: explore_folder({'values': ['', lbl_path.get()]}), 2347 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2348 | butt_open.pack(expand=True, fill='both', pady=1, padx=1) 2349 | 2350 | # Path button, top right frame 2351 | brd_bt_path = tk.Frame(frame_top_r, bg=BG_COLOR) 2352 | brd_bt_path.grid(row=0, column=0, sticky='snwe') 2353 | butt_path = tk.Button(brd_bt_path, text='paths', bd=0, command=folders_requester, 2354 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2355 | butt_path.pack(expand=True, fill='both', pady=1, padx=1) 2356 | 2357 | # Config button, top right frame 2358 | brd_bt_conf = tk.Frame(frame_top_r, bg=BG_COLOR) 2359 | brd_bt_conf.grid(row=0, column=1, sticky='snwe') 2360 | butt_conf = tk.Button(brd_bt_conf, text='config', bd=0, command=config_requester, 2361 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2362 | butt_conf.pack(expand=True, fill='both', pady=1, padx=1) 2363 | 2364 | # Frame for the grid and info 2365 | frame_main = tk.Frame(frame_all, bg=BG_COLOR) 2366 | frame_main.grid(sticky='news') 2367 | frame_main.grid(row=1, column=0, sticky='nw') 2368 | 2369 | # Frame for the info 2370 | frame_info_height = GRID_IMG_SZ * ROW_NBR + (BORDER * 2 * ROW_NBR) 2371 | info_frame = tk.Frame(frame_main, bg=BG_COLOR, width=INFO_IMG_SZ, height=frame_info_height) 2372 | info_frame.grid(row=0, column=1, sticky='nw') 2373 | 2374 | # Button to show the selected image 2375 | img_info = tk.Button(info_frame, bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1, 2376 | font=(FONT[0], FONT[1] * 3, 'bold'), borderwidth=0, text=PROGRAM_NAME) 2377 | img_info.bind('', lambda event: show_full_image([current_index])) 2378 | img_info.bind('', lambda event: show_image([current_index])) 2379 | img_info.bind('', lambda event: show_image([current_index])) 2380 | img_info.bind('', lambda event: show_image([current_index])) 2381 | img_info.place(x=1, y=1, height=INFO_IMG_SZ - 2, width=INFO_IMG_SZ - 2) 2382 | 2383 | # Text box to show the selected image info 2384 | text_info_height = GRID_IMG_SZ * ROW_NBR - INFO_IMG_SZ + (BORDER * 2 * ROW_NBR) 2385 | text_info = tk.Text(info_frame, name='text_info', 2386 | bg=BG_COLOR, fg=FONT_COLOR, 2387 | selectbackground=FONT_COLOR, selectforeground=BG_COLOR, 2388 | borderwidth=0, padx=10, pady=10) 2389 | text_info.bind('', lambda event: copy_to_clipboard(event)) 2390 | text_info.bind('', lambda event: copy_to_clipboard(event)) 2391 | text_info.bind('', lambda event: copy_to_clipboard(event)) 2392 | text_info.insert('insert', TEXT_INFO_DEFAULT) 2393 | text_info.place(x=0, y=INFO_IMG_SZ, height=text_info_height, width=INFO_IMG_SZ - 15) 2394 | text_info['state'] = 'disable' 2395 | 2396 | # Text box info scrollbar 2397 | sb_txt_info = ttk.Scrollbar(info_frame, orient='vertical') 2398 | sb_txt_info.place(x=INFO_IMG_SZ - 15, y=INFO_IMG_SZ, height=text_info_height) 2399 | text_info.configure(yscrollcommand=sb_txt_info.set) 2400 | sb_txt_info.config(command=text_info.yview) 2401 | 2402 | # Frame for the image grid 2403 | frame_canvas = tk.Frame(frame_main, bg=BG_COLOR) 2404 | frame_canvas.grid(row=0, column=0, sticky='nw', pady=(1, 0)) 2405 | frame_canvas.grid_rowconfigure(0, weight=1) 2406 | frame_canvas.grid_columnconfigure(0, weight=1) 2407 | frame_canvas.grid_propagate(False) 2408 | 2409 | # Add a canvas in that frame 2410 | canvas = tk.Canvas(frame_canvas, bg=BG_COLOR, borderwidth=0, highlightthickness=0) 2411 | canvas.bind_all('', on_mousewheel) 2412 | canvas.bind_all("", on_mousewheel) 2413 | canvas.bind_all("", on_mousewheel) 2414 | 2415 | canvas.grid(row=0, column=0, sticky='news') 2416 | 2417 | # Create a frame to contain the buttons 2418 | frame_buttons = tk.Frame(canvas, bg=BG_COLOR, name='buttons_frame') 2419 | canvas.create_window((0, 0), window=frame_buttons, anchor='nw') 2420 | 2421 | # Link a scrollbar to the canvas 2422 | vsb = ttk.Scrollbar(frame_canvas, orient='vertical', command=canvas.yview) 2423 | vsb.grid(row=0, column=1, sticky='ns', padx=(0, 1), pady=(0, 0)) 2424 | canvas.configure(yscrollcommand=refresh_images) 2425 | 2426 | # Resize the canvas and frame 2427 | grid_width = (COL_NBR * GRID_IMG_SZ + (BORDER * 4 * COL_NBR)) + vsb.winfo_width() - 4 2428 | grid_height = ROW_NBR * GRID_IMG_SZ + (BORDER * 2 * ROW_NBR) 2429 | frame_canvas.config(width=grid_width, height=grid_height) 2430 | 2431 | # Frame for the search and info panel bottom 2432 | bottom_frame = tk.Frame(frame_all, bg=BG_COLOR, height=BUTT_HEIGHT) 2433 | bottom_frame.grid(row=2, column=0, sticky='news') 2434 | 2435 | # Frame for the search 2436 | search_frame = tk.Frame(bottom_frame, bg=BG_COLOR) 2437 | search_frame.place(x=0, height=BUTT_HEIGHT, width=grid_width) 2438 | 2439 | # Button to expose 2440 | brd_bt_expose = tk.Frame(search_frame, bg=BG_COLOR) 2441 | brd_bt_expose.grid(row=0, column=0, sticky='news') 2442 | butt_expose = tk.Button(brd_bt_expose, text='expose', bd=0, 2443 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2444 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get())) 2445 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get())) 2446 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get())) 2447 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), invert=True)) 2448 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), invert=True)) 2449 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), invert=True)) 2450 | butt_expose.bind('', lambda event: expose_images('', '')) 2451 | butt_expose.bind('', lambda event: expose_images('', '')) 2452 | butt_expose.bind('', lambda event: expose_images('', '')) 2453 | butt_expose.bind('', lambda event: expose_images('', '')) 2454 | butt_expose.bind('', lambda event: expose_images('', '')) 2455 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), exact=True)) 2456 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), exact=True)) 2457 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), exact=True)) 2458 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), exact=True, invert=True)) 2459 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), exact=True, invert=True)) 2460 | butt_expose.bind('', lambda event: expose_images(entry_search.get(), combo_params.get(), exact=True, invert=True)) 2461 | butt_expose.pack(expand=True, fill='both', pady=1, padx=1) 2462 | 2463 | # Button to search 2464 | brd_bt_search = tk.Frame(search_frame, bg=BG_COLOR) 2465 | brd_bt_search.grid(row=0, column=1, sticky='news') 2466 | butt_search = tk.Button(brd_bt_search, text='search', bd=0, 2467 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2468 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get())) 2469 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get())) 2470 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get())) 2471 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), invert=True)) 2472 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), invert=True)) 2473 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), invert=True)) 2474 | butt_search.bind('', lambda event: search_images('', '')) 2475 | butt_search.bind('', lambda event: search_images('', '')) 2476 | butt_search.bind('', lambda event: search_images('', '')) 2477 | butt_search.bind('', lambda event: search_images('', '')) 2478 | butt_search.bind('', lambda event: search_images('', '')) 2479 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), exact=True)) 2480 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), exact=True)) 2481 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), exact=True)) 2482 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), exact=True, invert=True)) 2483 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), exact=True, invert=True)) 2484 | butt_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get(), exact=True, invert=True)) 2485 | butt_search.pack(expand=True, fill='both', pady=1, padx=1) 2486 | 2487 | # Search entry box 2488 | brd_ent_search = tk.Frame(search_frame, bg=BG_COLOR) 2489 | brd_ent_search.grid(row=0, column=2, sticky='nwes') 2490 | entry_search = tk.Entry(brd_ent_search, bg=ACC_COLOR1, fg=FONT_COLOR, bd=0, name='entry_search', 2491 | selectbackground=FONT_COLOR, selectforeground=ACC_COLOR2) 2492 | entry_search.insert('insert', SEARCH_HELP) 2493 | entry_search.bind('', lambda event: enter_search(entry_search.get())) 2494 | entry_search.bind('', lambda event: search_images(entry_search.get(), combo_params.get())) 2495 | entry_search.bind('', lambda event: expose_images(entry_search.get(), combo_params.get())) 2496 | entry_search.bind('', lambda event: search_images('', '')) 2497 | entry_search.bind('', lambda event: expose_images('', '')) 2498 | entry_search.pack(expand=True, fill='both', pady=1, padx=1) 2499 | 2500 | # Search parameters 2501 | brd_cb_param = tk.Frame(search_frame, bg=BG_COLOR) 2502 | brd_cb_param.grid(row=0, column=3, sticky='snew') 2503 | combo_params = ttk.Combobox(brd_cb_param, values=TEXT_PARS, name='combo_box', width=10) 2504 | combo_params.current(0) 2505 | combo_params.pack(expand=True, fill='both', pady=1, padx=1) 2506 | combo_params.bind("<>", lambda event: combo_params.selection_clear()) 2507 | 2508 | # Button to sort 2509 | brd_bt_sort = tk.Frame(search_frame, bg=BG_COLOR) 2510 | brd_bt_sort.grid(row=0, column=4, sticky='news') 2511 | butt_sort = tk.Button(brd_bt_sort, text='sort', bd=0, 2512 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2513 | butt_sort.bind('', lambda event: sort_images(combo_params.get())) 2514 | butt_sort.bind('', lambda event: sort_images(combo_params.get())) 2515 | butt_sort.bind('', lambda event: sort_images(combo_params.get())) 2516 | butt_sort.bind('', lambda event: sort_images('')) 2517 | butt_sort.bind('', lambda event: sort_images('')) 2518 | butt_sort.bind('', lambda event: sort_images('')) 2519 | butt_sort.bind('', lambda event: sort_images('')) 2520 | butt_sort.bind('', lambda event: sort_images('')) 2521 | butt_sort.pack(expand=True, fill='both', pady=1, padx=1) 2522 | 2523 | # Button to show overlay 2524 | brd_bt_overlay = tk.Frame(search_frame, bg=BG_COLOR) 2525 | brd_bt_overlay.grid(row=0, column=5, sticky='news') 2526 | butt_overlay = tk.Button(brd_bt_overlay, text='overlay', bd=0, 2527 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2528 | butt_overlay.bind('', lambda event: overlay_info(combo_params.get())) 2529 | butt_overlay.bind('', lambda event: overlay_info(combo_params.get())) 2530 | butt_overlay.bind('', lambda event: overlay_info(combo_params.get())) 2531 | butt_overlay.bind('', lambda event: overlay_info('')) 2532 | butt_overlay.bind('', lambda event: overlay_info('')) 2533 | butt_overlay.bind('', lambda event: overlay_info('')) 2534 | butt_overlay.bind('', lambda event: overlay_info('')) 2535 | butt_overlay.bind('', lambda event: overlay_info('')) 2536 | butt_overlay.pack(expand=True, fill='both', pady=1, padx=1) 2537 | 2538 | search_frame.grid_rowconfigure(0, weight=1) 2539 | search_frame.grid_columnconfigure(0, weight=0) 2540 | search_frame.grid_columnconfigure(1, weight=0) 2541 | search_frame.grid_columnconfigure(2, weight=2) 2542 | search_frame.grid_columnconfigure(3, weight=1) 2543 | search_frame.grid_columnconfigure(4, weight=0) 2544 | search_frame.grid_columnconfigure(5, weight=0) 2545 | 2546 | # Frame for the info panel bottom 2547 | info_panel_x = grid_width 2548 | info_panel_frame = tk.Frame(bottom_frame, bg=BG_COLOR) 2549 | info_panel_frame.place(x=info_panel_x, height=BUTT_HEIGHT, width=INFO_IMG_SZ) 2550 | 2551 | # Button to open folder containing image 2552 | brd_bt_folder = tk.Frame(info_panel_frame, bg=BG_COLOR) 2553 | brd_bt_folder.grid(row=0, column=0, sticky='news') 2554 | butt_folder = tk.Button(brd_bt_folder, text='show in folder', bd=0, 2555 | command=lambda: explore_folder([current_index], select=True), 2556 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2557 | butt_folder.pack(expand=True, fill='both', pady=1, padx=1) 2558 | 2559 | # Button to save information 2560 | brd_bt_save_info = tk.Frame(info_panel_frame, bg=BG_COLOR) 2561 | brd_bt_save_info.grid(row=0, column=1, sticky='news') 2562 | butt_save_info = tk.Button(brd_bt_save_info, text='save info', bd=0, 2563 | bg=FONT_COLOR, fg=BG_COLOR, activebackground=ACC_COLOR1) 2564 | butt_save_info.pack(expand=True, fill='both', pady=1, padx=1) 2565 | butt_save_info.bind('', lambda event: save_info(multi_index)) 2566 | butt_save_info.bind('', lambda event: save_info(multi_index)) 2567 | butt_save_info.bind('', lambda event: save_info(multi_index)) 2568 | butt_save_info.bind('', lambda event: save_info(multi_index, ask=True)) 2569 | butt_save_info.bind('', lambda event: save_info(multi_index, ask=True)) 2570 | butt_save_info.bind('', lambda event: save_info(multi_index, ask=True)) 2571 | butt_save_info.bind('', lambda event: save_info(multi_index, ask=True)) 2572 | butt_save_info.bind('', lambda event: save_info(multi_index, ask=True)) 2573 | 2574 | info_panel_frame.grid_rowconfigure(0, weight=1) 2575 | info_panel_frame.grid_columnconfigure(0, weight=1) 2576 | info_panel_frame.grid_columnconfigure(1, weight=1) 2577 | 2578 | # Popup menu 2579 | pop_menu = tk.Menu(root, tearoff=0, relief='flat', bg=FONT_COLOR, 2580 | activebackground=ACC_COLOR1, activeforeground=BG_COLOR, disabledforeground=ACC_COLOR2, 2581 | disabled=ACC_COLOR2, borderwidth=0, activeborderwidth=0) 2582 | pop_menu.add_separator() 2583 | pop_menu.add_separator() 2584 | pop_menu.add_command(label='open image(s) internal', foreground=BG_COLOR, command=lambda: menu_items(0)) 2585 | pop_menu.add_command(label='open image(s) in system', foreground=BG_COLOR, command=lambda: menu_items(1)) 2586 | pop_menu.add_separator() 2587 | pop_menu.add_command(label='show image(s) in folder', foreground=BG_COLOR, command=lambda: menu_items(2)) 2588 | pop_menu.add_separator() 2589 | pop_menu.add_command(label='copy image(s) to folders', foreground=BG_COLOR, command=lambda: menu_items(6)) 2590 | pop_menu.add_command(label='copy image batch to folder', foreground=BG_COLOR, command=lambda: menu_items(8)) 2591 | pop_menu.add_separator() 2592 | pop_menu.add_command(label='save info', foreground=BG_COLOR, command=lambda: menu_items(4)) 2593 | pop_menu.add_command(label='save info as', foreground=BG_COLOR, command=lambda: menu_items(5)) 2594 | pop_menu.add_separator() 2595 | pop_menu.add_command(label='copy info to clipboard', foreground=BG_COLOR, command=lambda: menu_items(3)) 2596 | 2597 | # Adjustments for MacOS 2598 | # lbl_path.config(highlightbackground=BG_COLOR) 2599 | butt_path.config(highlightbackground=BG_COLOR) 2600 | butt_refr.config(highlightbackground=BG_COLOR) 2601 | butt_open.config(highlightbackground=BG_COLOR) 2602 | butt_conf.config(highlightbackground=BG_COLOR) 2603 | butt_folder.config(highlightbackground=BG_COLOR) 2604 | butt_save_info.config(highlightbackground=BG_COLOR) 2605 | butt_search.config(highlightbackground=BG_COLOR) 2606 | butt_expose.config(highlightbackground=BG_COLOR) 2607 | text_info.config(highlightbackground=BG_COLOR) 2608 | entry_search.config(highlightbackground=BG_COLOR) 2609 | 2610 | # Launch the GUI 2611 | ico = Image.open(os.path.join(LOCAL_PATH, 'Images', 'Logo.png')) 2612 | ico = ImageTk.PhotoImage(ico) 2613 | root.wm_iconphoto(False, ico) 2614 | 2615 | root.after(50, lambda: first_run()) 2616 | root.mainloop() 2617 | 2618 | 2619 | if __name__ == '__main__': 2620 | main() 2621 | -------------------------------------------------------------------------------- /linux-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create the destination directory 4 | mkdir $HOME/Diffusion-Browser 5 | 6 | # Store the current directory in a variable 7 | current_dir=$(pwd) 8 | 9 | # Store the home directory of the user in a variable 10 | home_dir=$HOME 11 | 12 | # Use the 'cp' command to copy the contents of the current directory to the home directory 13 | # The '-r' flag specifies to copy the contents recursively, so that any subdirectories and their contents are also copied 14 | cp -r $current_dir/* $home_dir/Diffusion-Browser 15 | 16 | # Create the .desktop file 17 | cd ~/.local/share/applications 18 | cp ~/Diffusion-Browser/Diffusion-Browser.desktop ~/.local/share/applications 19 | 20 | 21 | # Print a message to indicate that the copy is complete 22 | echo "Installation complete!" 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==9.2.0 --------------------------------------------------------------------------------