├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── requirements.txt └── src ├── _xdialog_fix.py ├── ai.py ├── font.py ├── fonts └── InterTight-Regular.ttf ├── gui.py ├── icon.ico ├── main.py └── tools.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | venv/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | GENERATED_IMGS/ 10 | test_images/ 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/DearPyGui_ImageController"] 2 | path = src/DearPyGui_ImageController 3 | url = https://github.com/IvanNazaruk/DearPyGui-ImageController 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 IvanNazaruk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DifferentDimensionMe-GUI 2 | GUI for sending multiple images to a neural network site ([DifferentDimensionMe](https://h5.tu.qq.com/web/ai-2d/cartoon/index)). Written in Python using DearPyGui. 3 | It works only if there is a face on the image and from Chinese IP (Сan use VPN services) 4 | 5 | https://user-images.githubusercontent.com/46572469/205410562-c03236f4-c329-4b62-88fc-74b34954e5d6.mp4 6 | 7 | # How to run 8 | 1. `git clone --recursive https://github.com/IvanNazaruk/DifferentDimensionMe-GUI` 9 | 2. `cd DifferentDimensionMe-GUI` 10 | 3. `pip install -r requirements.txt` 11 | 4. `cd src` 12 | 5. `python main.py` 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dearpygui 2 | requests 3 | xdialog 4 | pillow 5 | -------------------------------------------------------------------------------- /src/_xdialog_fix.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import platform 3 | 4 | import xdialog 5 | 6 | if platform.system() == 'Windows': 7 | from xdialog.windows_dialogs import comdlg32, BUFFER_SIZE, split_null_list 8 | from xdialog.windows_structs import tagOFNW 9 | 10 | 11 | def open_file(title, filetypes, multiple=False): 12 | file = ctypes.create_unicode_buffer(BUFFER_SIZE) 13 | pfile = ctypes.cast(file, ctypes.c_wchar_p) 14 | 15 | # Default options 16 | opts = tagOFNW( 17 | lStructSize=ctypes.sizeof(tagOFNW), 18 | 19 | lpstrFile=pfile, 20 | nMaxFile=BUFFER_SIZE, 21 | 22 | lpstrTitle=title, 23 | Flags=0x00081808 + (0x200 if multiple else 0) 24 | ) 25 | 26 | # Filetypes 27 | if filetypes: 28 | out = [] 29 | for s, t in filetypes: 30 | out.append(f'{s} ({t})\0{";".join(t.split())}\0') 31 | 32 | string = ''.join(out) + '\0' 33 | buffer = ctypes.create_unicode_buffer(string, len(string) + 2) 34 | opts.lpstrFilter = ctypes.addressof(buffer) # Extra NULL byte just in case 35 | opts.lpstrDefExt = ctypes.addressof(buffer) # Extra NULL byte just in case 36 | 37 | # Call file dialog 38 | ok = comdlg32.GetOpenFileNameW(ctypes.byref(opts)) 39 | 40 | # Return data 41 | if multiple: 42 | if ok: 43 | # Windows splits the parent folder, followed by files, by null characters. 44 | gen = split_null_list(pfile) 45 | parent = next(gen) 46 | return [parent + "\\" + f for f in gen] or [parent] 47 | else: 48 | return [] 49 | else: 50 | if ok: 51 | return file.value 52 | else: 53 | return '' 54 | 55 | 56 | xdialog.dialogs.open_file = open_file 57 | -------------------------------------------------------------------------------- /src/ai.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import hashlib 5 | import io 6 | import json 7 | import os 8 | import uuid 9 | 10 | import requests 11 | from PIL import Image 12 | 13 | requests_session = requests.Session() 14 | 15 | 16 | def get_base64_image(path: str | io.BytesIO) -> str: 17 | if isinstance(path, io.BytesIO): 18 | return base64.b64encode(path.getvalue()).decode() 19 | 20 | with open(path, "rb") as img_file: 21 | b64_string = base64.b64encode(img_file.read()).decode() 22 | return b64_string 23 | 24 | 25 | def _get_x_sign_value(request_json): 26 | string_json = json.dumps(request_json) 27 | return hashlib.md5(f'https://h5.tu.qq.com{len(string_json)}HQ31X02e'.encode()).hexdigest() 28 | 29 | 30 | def get_ai_image(b64_image_string: str, version=2): 31 | request_url = "https://ai.tu.qq.com/trpc.shadow_cv.ai_processor_cgi.AIProcessorCgi/Process" 32 | request_json = { 33 | "busiId": "ai_painting_anime_img_entry", 34 | "images": [b64_image_string], 35 | "extra": '{\"face_rects\":[],\"version\":' + f'{version}' + ',\"platform\":\"web\",\"data_report\":{\"parent_trace_id\":\"' + f'{uuid.uuid4()}' + '\", \"root_channel\":\"\",\"level\":0}}' 36 | } 37 | request_headers = { 38 | "Accept": "application/json, text/plain, */*", 39 | "Accept-Encoding": "gzip, deflate, br", 40 | "Accept-Language": "ru,uk-UA;q=0.8,uk;q=0.6,en-US;q=0.4,en;q=0.2", 41 | "Cache-Control": "no-cache", 42 | "Connection": "keep-alive", 43 | "Content-Length": "448320", 44 | "Content-Type": "application/json", 45 | "Cookie": "pac_uid=0_ce27c744a8be3; iip=0; pgv_info=ssid=s2755604992; pgv_pvid=9635260140; ariaDefaultTheme=undefined", 46 | "Host": "ai.tu.qq.com", 47 | "Origin": "https://h5.tu.qq.com", 48 | "Pragma": "no-cache", 49 | "Referer": "https://h5.tu.qq.com/", 50 | "Sec-Fetch-Dest": "empty", 51 | "Sec-Fetch-Mode": "no-cors", 52 | "Sec-Fetch-Site": "same-site", 53 | 'x-sign-value': _get_x_sign_value(request_json), 54 | 'x-sign-version': 'v1', 55 | } 56 | response = requests_session.post( 57 | url=request_url, 58 | headers=request_headers, 59 | json=request_json, 60 | ) 61 | 62 | text = response.text 63 | try: 64 | text = json.loads(text)['extra'] 65 | text = json.loads(text) 66 | text = list(set(text['img_urls'])) 67 | except json.decoder.JSONDecodeError: 68 | print(text) 69 | raise KeyError 70 | except KeyError: 71 | print(text) 72 | raise KeyError 73 | except Exception: 74 | print(text) 75 | raise Exception 76 | return text[0] 77 | 78 | 79 | def crop_image(image: Image.Image) -> Image.Image: 80 | left = 20 81 | top = 22 82 | right = image.width - 22 83 | bottom = image.height - 210 84 | if image.width == 1_000: # Vertical Image 85 | left += 472 86 | left += 16 87 | else: # Horizontal Image 88 | top += 504 89 | top += 17 90 | cropped_image = image.crop((left, top, right, bottom)) 91 | return cropped_image 92 | 93 | 94 | def download_image(url) -> (Image.Image, str): 95 | response = requests_session.get(url) 96 | if response.status_code != 200: 97 | raise ValueError 98 | path: str = f'GENERATED_IMGS\\{str(uuid.uuid4())}.jpg' 99 | path = os.path.join(os.path.abspath(os.getcwd()), path) 100 | os.makedirs(os.path.dirname(path), exist_ok=True) 101 | with open(path, 'wb') as f: 102 | f.write(response.content) 103 | image = Image.open(path) 104 | cropped_image = crop_image(image) 105 | image.close() 106 | return cropped_image, path 107 | -------------------------------------------------------------------------------- /src/font.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ctypes 4 | import sys 5 | 6 | import dearpygui.dearpygui as dpg 7 | 8 | import DearPyGui_ImageController as dpg_img 9 | 10 | font_size = 25 11 | default_path = './fonts/InterTight-Regular.ttf' 12 | 13 | font_registry = None 14 | 15 | 16 | def add_font(file, size: int | float, parent=0, **kwargs) -> int: 17 | if not isinstance(size, (int, float)): 18 | raise ValueError(f'font size must be an integer or float. Not {type(size)}') 19 | 20 | with dpg.font(file, size, parent=parent, **kwargs) as font: 21 | dpg.add_font_range_hint(dpg.mvFontRangeHint_Default, parent=font) 22 | # dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic, parent=font) 23 | return font 24 | 25 | 26 | def load() -> int: 27 | ''' 28 | :return: default font 29 | ''' 30 | global font_registry 31 | if sys.platform.startswith('win'): 32 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(u'CompanyName.ProductName.SubProduct.VersionInformation') 33 | ctypes.windll.shcore.SetProcessDpiAwareness(1) 34 | 35 | dpg_img.set_texture_registry(dpg.add_texture_registry(show=False)) 36 | dpg_img.default_controller.max_inactive_time = 3 37 | dpg_img.default_controller.unloading_check_sleep_time = 1 38 | font_registry = dpg.add_font_registry() 39 | 40 | return add_font(default_path, font_size, parent=font_registry) 41 | -------------------------------------------------------------------------------- /src/fonts/InterTight-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanNazaruk/DifferentDimensionMe-GUI/c2f049c7fec39e9ec928ca115777ea0249d8cb97/src/fonts/InterTight-Regular.ttf -------------------------------------------------------------------------------- /src/gui.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import subprocess 4 | import threading 5 | import time 6 | import traceback 7 | from io import BytesIO 8 | 9 | import dearpygui.dearpygui as dpg 10 | import xdialog 11 | from PIL import Image, ImageGrab, BmpImagePlugin 12 | 13 | import DearPyGui_ImageController as dpg_img 14 | import font 15 | from tools import ViewportResizeManager, dpg_callback, LoadedImage, RequestQueue, CallWhenDPGStarted, image_to_clipboard 16 | 17 | 18 | class AutoImageWrapper: 19 | theme = 0 20 | table: int 21 | all_items: list[int] 22 | item_width: int 23 | items_in_last_row: int 24 | _now_count_items_in_row: int 25 | spacers_list: list 26 | 27 | def __new__(cls, *args, **kwargs): 28 | if cls.theme == 0: 29 | with dpg.theme() as cls.theme: 30 | with dpg.theme_component(dpg.mvAll): 31 | dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0, 0, category=dpg.mvThemeCat_Core) 32 | dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 0, 0, category=dpg.mvThemeCat_Core) 33 | dpg.add_theme_style(dpg.mvStyleVar_CellPadding, 0, 0, category=dpg.mvThemeCat_Core) 34 | dpg.add_theme_style(dpg.mvStyleVar_ItemSpacing, 0, 0, category=dpg.mvThemeCat_Core) 35 | return object.__new__(cls) 36 | 37 | def __init__(self, item_width: int): 38 | self.item_width = item_width 39 | self.items_in_last_row = 0 40 | self._now_count_items_in_row = 0 41 | self.all_items = [] 42 | self.spacers_list = [] 43 | 44 | def delete(self): 45 | ViewportResizeManager.remove_callback(self.resize_tag) 46 | dpg.delete_item(self.window) 47 | 48 | def update(self): 49 | if self.item_width <= 0: 50 | return 51 | viewport_width = dpg.get_item_rect_size(self.window)[0] 52 | count_items_in_row = viewport_width // self.item_width 53 | if count_items_in_row <= 0: 54 | count_items_in_row = 1 55 | if count_items_in_row > len(self.all_items): 56 | count_items_in_row = len(self.all_items) 57 | 58 | between_spacer_width: int = (viewport_width - count_items_in_row * self.item_width) / (count_items_in_row + 1) # noqa 59 | 60 | dpg.configure_item(self.spacer, width=between_spacer_width, show=between_spacer_width > 0) 61 | if self._now_count_items_in_row == count_items_in_row: 62 | for spacer in self.spacers_list: 63 | dpg.configure_item(spacer, width=between_spacer_width) 64 | return 65 | 66 | self._now_count_items_in_row = count_items_in_row 67 | self.spacers_list = [] 68 | for item in self.all_items: 69 | dpg.move_item(item, parent=self.stage) 70 | 71 | dpg.delete_item(self.table, children_only=True) 72 | for i in range(count_items_in_row): 73 | dpg.add_table_column(width_fixed=True, parent=self.table) 74 | 75 | for i, item in enumerate(self.all_items): 76 | if not i % count_items_in_row: 77 | row = dpg.add_table_row(parent=self.table) 78 | 79 | with dpg.group(parent=row, horizontal=True) as cell_group: # noqa 80 | dpg.move_item(self.all_items[i], parent=cell_group) 81 | if not (i + 1) % count_items_in_row: 82 | dpg.configure_item(cell_group, horizontal=False) 83 | dpg.add_spacer(height=10, parent=cell_group) 84 | continue 85 | spacer = dpg.add_spacer(width=between_spacer_width, parent=cell_group) 86 | self.spacers_list.append(spacer) 87 | 88 | def window_resize(self): 89 | self.update() 90 | 91 | def append_items(self, dpg_items: int | list[int]): 92 | if isinstance(dpg_items, list): 93 | self.all_items.extend(dpg_items) 94 | else: 95 | self.all_items.append(dpg_items) 96 | self._now_count_items_in_row = -1 97 | self.update() 98 | 99 | def create(self, parent=0): 100 | self.resize_tag = ViewportResizeManager.add_callback(self.window_resize) 101 | with dpg.child_window(parent=parent, width=-1) as self.window: 102 | dpg.bind_item_theme(self.window, self.theme) 103 | with dpg.group(horizontal=True) as self.main_group: 104 | self.spacer = dpg.add_spacer(width=0, height=0, show=False, parent=self.main_group) 105 | self.table = dpg.add_table(resizable=False, header_row=False, parent=self.main_group) 106 | self.stage = dpg.add_stage() 107 | if CallWhenDPGStarted.STARTUP_DONE: 108 | dpg.split_frame() 109 | self.update() 110 | else: 111 | CallWhenDPGStarted.append( 112 | self.update 113 | ) 114 | 115 | 116 | class AIImageViewer: 117 | image_viewer: dpg_img.ImageViewer 118 | image_filepath: str = '' 119 | 120 | is_error: bool = False 121 | loaded: bool = False 122 | 123 | click_handler: int = None 124 | deleted: bool = False 125 | 126 | class StatusColors: 127 | wait_queue = (150, 0, 200) 128 | wait_request = (60, 230, 230) 129 | wait_load_image = (0, 255, 0) 130 | error = (255, 0, 0) 131 | 132 | @staticmethod 133 | def get_loading_indicator_radius(width: int, height: int): 134 | radius = width if width < height else height 135 | radius = radius / font.font_size 136 | return radius 137 | 138 | def __init__(self, loaded_image: LoadedImage, version=2): 139 | self.request_image = loaded_image 140 | self.version = version 141 | 142 | self.image_viewer = dpg_img.ImageViewer() 143 | 144 | def start_request(self): 145 | if self.deleted: 146 | return 147 | self.is_error = False 148 | RequestQueue.append(self.request_image, 149 | version=self.version, 150 | start_callback=self.started, 151 | error_callback=self.error, 152 | done_callback=self.done) 153 | dpg.configure_item(self.loading_indicator, 154 | speed=1, 155 | color=self.StatusColors.wait_queue) 156 | 157 | def create(self, width: int, height: int, parent: int | str = 0): 158 | if self.deleted: 159 | return 160 | self.parent_window = parent 161 | self.width = width 162 | self.height = height 163 | 164 | with dpg.group(parent=parent) as self.group: 165 | self.loading_indicator = dpg.add_loading_indicator(radius=self.get_loading_indicator_radius(width, height), 166 | color=self.StatusColors.wait_queue, 167 | secondary_color=(255, 255, 255), 168 | parent=self.group) 169 | self.start_request() 170 | 171 | def started(self): 172 | if self.deleted: 173 | return 174 | dpg.configure_item(self.loading_indicator, 175 | color=self.StatusColors.wait_request) 176 | 177 | def error(self, msg: str): 178 | if self.deleted: 179 | return 180 | self.is_error = True 181 | dpg.configure_item(self.loading_indicator, 182 | color=self.StatusColors.error, 183 | speed=0) 184 | 185 | def done(self, image: Image.Image, filepath: str): 186 | if self.deleted: 187 | return 188 | dpg.configure_item(self.loading_indicator, 189 | color=self.StatusColors.wait_load_image) 190 | 191 | self.image_filepath = filepath 192 | 193 | self.image_viewer.load(image) 194 | self.loaded = True 195 | 196 | with dpg.item_handler_registry() as self.click_handler: 197 | dpg.add_item_clicked_handler(callback=lambda _, data: self.click(mouse_button=data[0])) 198 | self.image_viewer.set_image_handler(self.click_handler) 199 | 200 | self.set_width(self.width) 201 | self.image_viewer.create(parent=self.parent_window) 202 | 203 | dpg.delete_item(self.loading_indicator) 204 | 205 | def set_width(self, width: int = None): 206 | if self.deleted: 207 | return 208 | self.width = width 209 | if not self.loaded: 210 | if width is None: 211 | width, height = self.request_image.width, self.request_image.height 212 | else: 213 | height = self.request_image.height * (width / self.request_image.width) 214 | dpg.configure_item(self.parent_window, width=width, height=height) 215 | dpg.configure_item(self.loading_indicator, 216 | radius=self.get_loading_indicator_radius(width, height)) 217 | else: 218 | self.image_viewer.set_width(width) 219 | width, height = self.image_viewer.get_size() 220 | dpg.configure_item(self.parent_window, width=width, height=height) 221 | 222 | def click(self, mouse_button): 223 | if self.deleted: 224 | return 225 | if not self.loaded: 226 | return 227 | if mouse_button == 1: # Right button 228 | if self.image_viewer.image: 229 | image_to_clipboard(self.image_viewer.image) 230 | else: # Left or Middle button 231 | subprocess.Popen(rf'explorer /select,"{self.image_filepath}"') 232 | 233 | def delete(self): 234 | if self.deleted: 235 | return 236 | self.deleted = True 237 | 238 | self.image_viewer.delete() 239 | self.image_viewer = None # noqa 240 | dpg_img.HandlerDeleter.add(self.click_handler) 241 | self.click_handler = None # noqa 242 | 243 | try: 244 | dpg.delete_item(self.group) 245 | except Exception: 246 | pass 247 | finally: 248 | self.group = None 249 | 250 | 251 | class AIImageManager: 252 | group: int 253 | image_wrapper: AutoImageWrapper 254 | loaded_image: LoadedImage 255 | image_viewers_list: list[AIImageViewer] 256 | 257 | def create(self): 258 | self.image_viewers_list = [] 259 | self.loaded_image = None 260 | self.image_wrapper = AutoImageWrapper(1_000) 261 | with dpg.group() as self.group: 262 | self.image_wrapper.create() 263 | 264 | def clear(self): 265 | RequestQueue.clear() 266 | 267 | self.image_wrapper.delete() 268 | del self.image_wrapper 269 | 270 | for image_viewer in self.image_viewers_list: 271 | image_viewer.delete() 272 | self.image_viewers_list.clear() 273 | 274 | dpg.delete_item(self.group, children_only=True) 275 | 276 | def start_load(self, loaded_image: LoadedImage, image_width: int, count: int, version: int = 2): 277 | self.clear() 278 | self.image_wrapper = AutoImageWrapper( 279 | item_width=image_width 280 | ) 281 | self.image_wrapper.create(parent=self.group) 282 | self.loaded_image = loaded_image 283 | 284 | image_height = self.loaded_image.get_height(width=image_width) 285 | 286 | items_list = [] 287 | self.image_viewers_list = [] 288 | for i in range(count): 289 | with dpg.child_window(parent=self.image_wrapper.stage, 290 | width=image_width, 291 | height=image_height) as child_window: 292 | image_viewer = AIImageViewer(loaded_image, version=version) 293 | image_viewer.create(width=image_width, 294 | height=image_height, 295 | parent=child_window) 296 | self.image_viewers_list.append(image_viewer) 297 | 298 | items_list.append(child_window) 299 | self.image_wrapper.append_items(items_list) 300 | 301 | def set_width(self, width: int): 302 | if self.loaded_image is None: 303 | return 304 | self.image_wrapper.item_width = width 305 | for image_viewer in self.image_viewers_list: 306 | image_viewer.set_width(width=width) 307 | self.image_wrapper.update() 308 | 309 | 310 | class MainWindow: 311 | window: int 312 | image_path_label: int 313 | AIImagerLoader: AIImageManager 314 | loaded_image: LoadedImage 315 | tooltip_image: dpg_img.ImageViewer 316 | 317 | choose_width_options = ['image width', 'self width:'] 318 | 319 | def __init__(self): 320 | self.AIImagerLoader = AIImageManager() 321 | self.loaded_image = None 322 | self.tooltip_image = dpg_img.ImageViewer(unload_width=1, unload_height=1) 323 | 324 | def set_tooltip_image(self, image: Image.Image | None = None): 325 | self.tooltip_image.load(image) 326 | 327 | @dpg_callback() 328 | def open_image_file(self): 329 | dpg.configure_item(self.open_image_button, enabled=False) 330 | dpg.configure_item(self.start_button, enabled=False) 331 | 332 | filetypes = [ 333 | '*.jpg', '*.jpeg', '*.jfif', '*.pjpeg', '*.pjp', # JPEG 334 | '*.png', # PNG 335 | '*.bmp', # BMP 336 | '.ico', '.cur', # ICO 337 | ] 338 | image_path = xdialog.open_file("Select image", 339 | filetypes=[("Image file", " ".join(filetypes)), 340 | ("Any file", "*")], 341 | multiple=False) 342 | dpg.set_value(self.image_path_label, "") 343 | if image_path: 344 | try: 345 | dpg.configure_item(self.image_path_label, color=(60, 230, 230)) 346 | dpg.set_value(self.image_path_label, 'Loading...') 347 | 348 | self.loaded_image = LoadedImage(image_path) 349 | self.set_tooltip_image(self.loaded_image.image) 350 | 351 | dpg.set_value(self.image_path_label, image_path) 352 | dpg.configure_item(self.image_path_label, color=(100, 255, 100)) 353 | except Exception as e: 354 | traceback.print_exc() 355 | dpg.set_value(self.image_path_label, f'ERROR: {e}') 356 | dpg.configure_item(self.image_path_label, color=(255, 0, 0)) 357 | else: 358 | self.set_tooltip_image() 359 | dpg.configure_item(self.start_button, enabled=bool(image_path)) 360 | dpg.configure_item(self.open_image_button, enabled=True) 361 | 362 | @dpg_callback() 363 | def try_paste_image(self): 364 | dpg.configure_item(self.open_image_button, enabled=False) 365 | dpg.configure_item(self.start_button, enabled=False) 366 | 367 | image = ImageGrab.grabclipboard() 368 | if isinstance(image, list): 369 | if len(image) > 0: 370 | image = image[0] 371 | else: 372 | image = None 373 | 374 | if isinstance(image, str): 375 | try: 376 | image = Image.open(image) 377 | except Exception: 378 | traceback.print_exc() 379 | image = None 380 | dpg.set_value(self.image_path_label, "") 381 | if isinstance(image, BmpImagePlugin.DibImageFile) or isinstance(image, Image.Image): 382 | try: 383 | output = BytesIO() 384 | image.save(output, format="png") 385 | self.loaded_image = LoadedImage(output) 386 | self.loaded_image.path = None 387 | self.set_tooltip_image(self.loaded_image.image) 388 | dpg.set_value(self.image_path_label, "{CLIPBOARD_IMAGE}") 389 | dpg.configure_item(self.image_path_label, color=(100, 255, 100)) 390 | except Exception as e: 391 | traceback.print_exc() 392 | dpg.set_value(self.image_path_label, f'ERROR: {e}') 393 | dpg.configure_item(self.image_path_label, color=(255, 0, 0)) 394 | else: 395 | self.set_tooltip_image() 396 | dpg.configure_item(self.start_button, enabled=bool(image)) 397 | dpg.configure_item(self.open_image_button, enabled=True) 398 | 399 | def check_clipboard_paste(self, _, key): 400 | if key != 86: # key != 'V' 401 | return 402 | if dpg.is_key_down(17): # ctrl 403 | self.try_paste_image(self=self) 404 | 405 | def get_show_image_width(self): 406 | if dpg.get_value(self.choise_width) == self.choose_width_options[0]: 407 | image_width = self.loaded_image.width 408 | else: 409 | image_width = dpg.get_value(self.image_width_input) 410 | try: 411 | image_width = int(image_width) 412 | if image_width <= 0: 413 | raise ValueError 414 | except Exception: 415 | dpg.set_value(self.image_width_input, self.loaded_image.width) 416 | image_width = self.loaded_image.width 417 | min_value = dpg.get_item_configuration(self.image_width_input)['min_value'] 418 | if image_width < min_value: 419 | image_width = min_value 420 | dpg.set_value(self.image_width_input, self.loaded_image.width) 421 | return image_width 422 | 423 | def start(self): 424 | dpg.configure_item(self.open_image_button, enabled=False) 425 | dpg.configure_item(self.start_button, enabled=False) 426 | 427 | ai_images_count = dpg.get_value(self.generate_image_count) 428 | if ai_images_count <= 0: 429 | ai_images_count = 1 430 | dpg.set_value(self.generate_image_count, ai_images_count) 431 | 432 | ai_version = 2 433 | try: 434 | ai_version = int(dpg.get_value(self.ai_version_input)) 435 | except Exception: 436 | dpg.set_value(self.ai_version_input, 2) 437 | 438 | image_width = self.get_show_image_width() 439 | self.AIImagerLoader.start_load(loaded_image=self.loaded_image, 440 | image_width=image_width, 441 | count=ai_images_count, 442 | version=ai_version) 443 | 444 | dpg.configure_item(self.open_image_button, enabled=True) 445 | dpg.configure_item(self.start_button, enabled=True) 446 | 447 | def update_width(self): 448 | dpg.configure_item(self.open_image_button, enabled=False) 449 | dpg.configure_item(self.start_button, enabled=False) 450 | dpg.configure_item(self.update_width_button, enabled=False) 451 | 452 | if self.loaded_image is not None: 453 | image_width = self.get_show_image_width() 454 | self.AIImagerLoader.set_width(image_width) 455 | 456 | dpg.configure_item(self.update_width_button, enabled=True) 457 | dpg.configure_item(self.open_image_button, enabled=True) 458 | dpg.configure_item(self.start_button, enabled=bool(self.loaded_image)) 459 | 460 | @dpg_callback() 461 | def auto_update_width(self): 462 | if dpg.get_value(self.auto_update_width_checkbox): 463 | self.update_width() 464 | 465 | def try_again_errors_images(self): 466 | try: 467 | for image_viewer in self.AIImagerLoader.image_viewers_list: 468 | if image_viewer.is_error: 469 | image_viewer.start_request() 470 | except Exception: 471 | traceback.print_exc() 472 | 473 | def set_download_threads_count(self): 474 | try: 475 | count = int(dpg.get_value(self.download_threads_count)) 476 | if count <= 0: 477 | count = 1 478 | dpg.set_value(self.download_threads_count, count) 479 | RequestQueue.set_count(count) 480 | except ValueError: 481 | pass 482 | 483 | def update_threads_info_worker(self): 484 | while 1: 485 | time.sleep(1) 486 | working_threads = [*RequestQueue.workers.values()].count(True) 487 | dpg.set_value(self.threads_info_label, 488 | f'Current running/working threads: {len(RequestQueue.workers)}/{working_threads} | ' 489 | f'Current request queue: {RequestQueue.queue.qsize()}') 490 | if dpg.get_value(self.auto_try_again_errors): 491 | self.try_again_errors_images() 492 | 493 | def set_show_settings(self, flag: bool): 494 | dpg.configure_item(self.settings_group, show=flag) 495 | 496 | def create(self): 497 | with dpg.window() as self.window: 498 | with dpg.table(resizable=False, header_row=False): 499 | dpg.add_table_column(width_fixed=True) 500 | dpg.add_table_column() 501 | dpg.add_table_column(width_stretch=False, width_fixed=True) 502 | dpg.add_table_column(width_stretch=False, width_fixed=True) 503 | dpg.add_table_column(width_stretch=False, width_fixed=True) 504 | 505 | with dpg.table_row(): 506 | self.open_image_button = dpg.add_button( 507 | label='Open the image file', 508 | callback=lambda *, self=self: self.open_image_file(self=self) # TODO: fix this 509 | ) 510 | with dpg.group(): 511 | self.image_path_label = dpg.add_text('') 512 | with dpg.tooltip(self.image_path_label) as self.text_tooltip: 513 | self.tooltip_image.create() 514 | with dpg.group(horizontal=True): 515 | dpg.add_text('AI version:') 516 | self.ai_version_input = dpg.add_input_int(default_value=2, width=25, step=0) 517 | dpg.add_checkbox(label='Show settings', default_value=True, callback=lambda _, value: self.set_show_settings(value)) 518 | self.start_button = dpg.add_button(label='Start', enabled=False, callback=self.start) 519 | with dpg.group() as self.settings_group: 520 | with dpg.group(horizontal=True): 521 | dpg.add_text('Number of generated images') 522 | self.generate_image_count = dpg.add_slider_int(min_value=1, max_value=100, 523 | default_value=24, 524 | width=-1) 525 | with dpg.group(horizontal=True): 526 | self.update_width_button = dpg.add_button(label='Update', callback=self.update_width) 527 | self.auto_update_width_checkbox = dpg.add_checkbox(label='Auto', default_value=True) 528 | self.choise_width = dpg.add_radio_button(self.choose_width_options, 529 | default_value=self.choose_width_options[1], 530 | horizontal=True, 531 | callback=lambda *, self=self: self.auto_update_width(self=self)) # TODO: fix this 532 | self.image_width_input = dpg.add_drag_int(min_value=50, max_value=10_000, 533 | default_value=150, 534 | width=-1, 535 | callback=lambda *, self=self: self.auto_update_width(self=self)) # TODO: fix this 536 | with dpg.group(horizontal=True): 537 | dpg.add_text('Number of threads of downloaders') 538 | self.download_threads_count = dpg.add_slider_int(min_value=1, max_value=100, 539 | default_value=RequestQueue.count, 540 | width=-1, 541 | callback=self.set_download_threads_count) 542 | with dpg.group(horizontal=True): 543 | self.threads_info_label = dpg.add_text(f'Current running/working threads: {len(RequestQueue.workers)}/{0} | ' 544 | f'Current request queue: {RequestQueue.queue.qsize()}') 545 | dpg.add_button(label='Try download errors images', callback=self.try_again_errors_images) 546 | self.auto_try_again_errors = dpg.add_checkbox(label='Auto', default_value=True) 547 | with dpg.handler_registry(): 548 | dpg.add_key_press_handler(key=-1, callback=self.check_clipboard_paste) 549 | threading.Thread(target=self.update_threads_info_worker, daemon=True).start() 550 | self.AIImagerLoader.create() 551 | -------------------------------------------------------------------------------- /src/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanNazaruk/DifferentDimensionMe-GUI/c2f049c7fec39e9ec928ca115777ea0249d8cb97/src/icon.ico -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import dearpygui.dearpygui as dpg 2 | 3 | import _xdialog_fix # noqa 4 | import font 5 | import gui 6 | 7 | dpg.create_context() 8 | dpg.create_viewport(title='DifferentDimensionMe-GUI by @ivannazaruk', width=1000, height=800, small_icon='icon.ico', large_icon='icon.ico', clear_color=(71, 71, 72)) 9 | dpg.bind_font(font.load()) 10 | 11 | with dpg.theme() as global_theme: 12 | with dpg.theme_component(dpg.mvAll): 13 | dpg.add_theme_color(dpg.mvThemeCol_CheckMark, (0, 255, 255), category=dpg.mvThemeCat_Core) 14 | dpg.add_theme_color(dpg.mvThemeCol_SliderGrab, (0, 255, 255), category=dpg.mvThemeCat_Core) 15 | with dpg.theme_component(dpg.mvButton, enabled_state=True): 16 | dpg.add_theme_color(dpg.mvThemeCol_Button, (51, 51, 55), category=dpg.mvThemeCat_Core) 17 | dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (29, 151, 236, 103), category=dpg.mvThemeCat_Core) 18 | dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, (0, 119, 200, 153), category=dpg.mvThemeCat_Core) 19 | with dpg.theme_component(dpg.mvButton, enabled_state=False): 20 | dpg.add_theme_color(dpg.mvThemeCol_Text, (255, 255, 255, 125), category=dpg.mvThemeCat_Core) 21 | dpg.add_theme_color(dpg.mvThemeCol_Button, (0, 0, 0, 0), category=dpg.mvThemeCat_Core) 22 | dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (0, 0, 0, 0), category=dpg.mvThemeCat_Core) 23 | dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, (0, 0, 0, 0), category=dpg.mvThemeCat_Core) 24 | dpg.bind_theme(global_theme) 25 | 26 | MainWindow = gui.MainWindow() 27 | MainWindow.create() 28 | 29 | dpg.set_primary_window(MainWindow.window, True) 30 | 31 | dpg.setup_dearpygui() 32 | dpg.show_viewport() 33 | dpg.start_dearpygui() 34 | dpg.destroy_context() 35 | -------------------------------------------------------------------------------- /src/tools.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import queue 4 | import threading 5 | import time 6 | import traceback 7 | from ctypes import * 8 | from ctypes.wintypes import * 9 | from io import BytesIO 10 | from typing import Callable 11 | 12 | import dearpygui.dearpygui as dpg 13 | from PIL import Image 14 | 15 | import ai 16 | 17 | 18 | class LoadedImage: 19 | path: str 20 | image: Image.Image 21 | width: int 22 | height: int 23 | base64: str 24 | 25 | def __init__(self, path: str | BytesIO): 26 | self.path = path 27 | self.image = Image.open(self.path) 28 | self.width = self.image.width 29 | self.height = self.image.height 30 | self.base64 = ai.get_base64_image(self.path) 31 | 32 | def get_height(self, width: int): 33 | image_height = self.height * (width / self.width) 34 | image_height = int(image_height) 35 | return image_height 36 | 37 | 38 | def dpg_callback(sender: bool = False, app_data: bool = False, user_data: bool = False): 39 | def decorator(function): 40 | BLOCKER = False 41 | function_queue: list[Callable, tuple, dict] = None 42 | 43 | def wrapper(sender_var=None, app_data_var=None, user_data_var=None, *args, **kwargs): 44 | nonlocal BLOCKER 45 | nonlocal function_queue 46 | if BLOCKER: 47 | function_queue = [function, args, kwargs] 48 | return 49 | BLOCKER = True 50 | args = list(args) 51 | if user_data: 52 | args.insert(0, user_data_var) 53 | if app_data: 54 | args.insert(0, app_data_var) 55 | if sender: 56 | args.insert(0, sender_var) 57 | args = tuple(args) 58 | 59 | def run(function, args, kwargs): 60 | nonlocal BLOCKER 61 | nonlocal function_queue 62 | while True: 63 | try: 64 | function(*args, **kwargs) 65 | except Exception: 66 | traceback.print_exc() 67 | if function_queue is None: 68 | BLOCKER = False 69 | return 70 | 71 | function, args, kwargs = function_queue 72 | function_queue = None 73 | 74 | threading.Thread(target=run, args=(function, args, kwargs,), daemon=True).start() 75 | 76 | return wrapper 77 | 78 | return decorator 79 | 80 | 81 | class CallWhenDPGStarted: 82 | __thread = None 83 | STARTUP_DONE = False 84 | functions_queue = [] 85 | 86 | def __new__(cls, func, *args, **kwargs): 87 | cls.append(func, *args, **kwargs) 88 | return None 89 | 90 | @classmethod 91 | def run_worker(cls): 92 | if cls.__thread is None: 93 | cls.__thread = True 94 | threading.Thread(target=cls._worker, daemon=True).start() 95 | 96 | @classmethod 97 | def append(cls, func, *args, **kwargs): 98 | cls.run_worker() 99 | if not cls.STARTUP_DONE: 100 | cls.functions_queue.append( 101 | [func, args, kwargs] 102 | ) 103 | return 104 | try: 105 | func(*args, **kwargs) 106 | except Exception: 107 | traceback.print_exc() 108 | 109 | @classmethod 110 | def _worker(cls): 111 | while dpg.get_frame_count() <= 1: 112 | time.sleep(0.01) 113 | dpg.split_frame() 114 | cls.STARTUP_DONE = True 115 | for func, args, kwargs in cls.functions_queue: 116 | try: 117 | func(*args, **kwargs) 118 | except Exception: 119 | traceback.print_exc() 120 | del cls.functions_queue 121 | 122 | 123 | class ViewportResizeManager: 124 | _STARTED = False 125 | 126 | _list_of_callbacks = dict() 127 | 128 | @classmethod 129 | def _setup(cls): 130 | dpg.set_viewport_resize_callback(cls._resize_callback) 131 | 132 | @staticmethod 133 | @dpg_callback() 134 | def _resize_callback(): 135 | for function_tag in ViewportResizeManager._list_of_callbacks.keys(): 136 | ViewportResizeManager._list_of_callbacks[function_tag]() 137 | 138 | @classmethod 139 | def add_callback(cls, function, tag: str | int = None) -> str | int: 140 | if not cls._STARTED: 141 | cls._STARTED = True 142 | cls._setup() 143 | if tag is None: 144 | tag: int = dpg.generate_uuid() 145 | cls._list_of_callbacks[tag] = function 146 | return tag 147 | 148 | @classmethod 149 | def remove_callback(cls, tag): 150 | del cls._list_of_callbacks[tag] 151 | 152 | 153 | class RequestQueue: 154 | count = 8 155 | queue = queue.Queue() 156 | 157 | workers = {} 158 | 159 | @classmethod 160 | def clear(cls): 161 | cls.queue.queue.clear() 162 | 163 | @classmethod 164 | def append(cls, 165 | loaded_image: LoadedImage, 166 | version: int, 167 | start_callback: Callable = None, 168 | error_callback: Callable[[str], None] = None, 169 | done_callback: Callable[[Image.Image], None] = None, 170 | ): 171 | cls.queue.put( 172 | (loaded_image, version, start_callback, error_callback, done_callback) 173 | ) 174 | 175 | @classmethod 176 | def update_workers(cls): 177 | for id in range(1, cls.count + 1): 178 | if id not in cls.workers: 179 | cls.workers[id] = False 180 | threading.Thread( 181 | target=cls.worker, 182 | args=(id,), 183 | daemon=True 184 | ).start() 185 | 186 | @classmethod 187 | def set_count(cls, count: int): 188 | if count <= 0: 189 | count = 1 190 | cls.count = count 191 | cls.update_workers() 192 | 193 | @classmethod 194 | def worker(cls, id: int): 195 | loaded_image, version, start_callback, error_callback, done_callback = (None, 2, None, None, None) 196 | while 1: 197 | cls.workers[id] = False 198 | try: 199 | loaded_image, version, start_callback, error_callback, done_callback = cls.queue.get(timeout=1) 200 | cls.workers[id] = True 201 | loaded_image: LoadedImage 202 | 203 | if start_callback: 204 | start_callback() 205 | 206 | url = ai.get_ai_image(loaded_image.base64, version) 207 | image, filepath = ai.download_image(url) 208 | 209 | if done_callback: 210 | done_callback(image, filepath) 211 | except queue.Empty: 212 | pass 213 | except Exception as e: 214 | if type(e) != KeyError: 215 | traceback.print_exc() 216 | if error_callback: 217 | try: 218 | error_callback(str(e)) 219 | except Exception: 220 | traceback.print_exc() 221 | finally: 222 | if id > cls.count: 223 | break 224 | del cls.workers[id] 225 | cls.update_workers() 226 | 227 | 228 | RequestQueue.update_workers() 229 | 230 | HGLOBAL = HANDLE 231 | SIZE_T = c_size_t 232 | GHND = 0x0042 233 | GMEM_SHARE = 0x2000 234 | 235 | GlobalAlloc = windll.kernel32.GlobalAlloc 236 | GlobalAlloc.restype = HGLOBAL 237 | GlobalAlloc.argtypes = [UINT, SIZE_T] 238 | 239 | GlobalLock = windll.kernel32.GlobalLock 240 | GlobalLock.restype = LPVOID 241 | GlobalLock.argtypes = [HGLOBAL] 242 | 243 | GlobalUnlock = windll.kernel32.GlobalUnlock 244 | GlobalUnlock.restype = BOOL 245 | GlobalUnlock.argtypes = [HGLOBAL] 246 | 247 | CF_DIB = 8 248 | 249 | OpenClipboard = windll.user32.OpenClipboard 250 | OpenClipboard.restype = BOOL 251 | OpenClipboard.argtypes = [HWND] 252 | 253 | EmptyClipboard = windll.user32.EmptyClipboard 254 | EmptyClipboard.restype = BOOL 255 | EmptyClipboard.argtypes = None 256 | 257 | SetClipboardData = windll.user32.SetClipboardData 258 | SetClipboardData.restype = HANDLE 259 | SetClipboardData.argtypes = [UINT, HANDLE] 260 | 261 | CloseClipboard = windll.user32.CloseClipboard 262 | CloseClipboard.restype = BOOL 263 | CloseClipboard.argtypes = None 264 | 265 | 266 | def image_to_clipboard(image: Image.Image): 267 | output = BytesIO() 268 | image.convert("RGB").save(output, "BMP") 269 | data = output.getvalue()[14:] 270 | output.close() 271 | 272 | hData = GlobalAlloc(GHND | GMEM_SHARE, len(data)) 273 | pData = GlobalLock(hData) 274 | memmove(pData, data, len(data)) 275 | GlobalUnlock(hData) 276 | 277 | OpenClipboard(None) 278 | EmptyClipboard() 279 | SetClipboardData(CF_DIB, pData) 280 | CloseClipboard() 281 | --------------------------------------------------------------------------------