├── .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 |
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 | 
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 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------
|