├── .gitignore ├── Core ├── __init__.py ├── config.py ├── core.py ├── functions.py ├── image_utils.py ├── pathplus.py ├── superview.py ├── texts_utils.py └── video_utils.py ├── GUI ├── __init__.py ├── artemis_part.py ├── flat_widgets │ ├── __init__.py │ ├── flat_circular_progress.py │ ├── flat_icon_button.py │ ├── flat_line_edit.py │ ├── flat_progress_bar.py │ ├── flat_push_button.py │ └── flat_tab_widget.py ├── game_page.py ├── image_page.py ├── info_page.py ├── kirikiri_part.py ├── left_menu.py ├── main_content.py ├── main_ui.py ├── majiro_part.py ├── qt_core.py ├── setting_page.py ├── sr_engine_settings.py └── text_page.py ├── GamePageUIConnection.py ├── Icons ├── book-open.svg ├── clock.svg ├── columns.svg ├── edit.svg ├── icon.ico ├── icon_close.svg ├── icon_folder.svg ├── icon_folder_open.svg ├── icon_maximize.svg ├── icon_minimize.svg ├── icon_send.svg ├── info.svg ├── settings.svg └── slack.svg ├── ImagePageUIConnection.py ├── LICENSE ├── README.md ├── SettingPageUIConnection.py ├── VNEngines ├── Artemis │ ├── __init__.py │ ├── artemis.py │ └── pf8_struct.py ├── Kirikiri │ ├── __init__.py │ ├── amv_struct.py │ └── kirikiri.py ├── Majiro │ ├── __init__.py │ ├── majiro.py │ └── majiro_arc.py ├── __init__.py └── upscaler.py └── VisualNovelUpscaler.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled/optimized/DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution/packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test/coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # 自定义 132 | /Dependencies 133 | /old_modules 134 | /config* 135 | /test* 136 | /logs.txt 137 | /VNTranslator 138 | /env* 139 | /make_exe.py 140 | -------------------------------------------------------------------------------- /Core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .functions import * 4 | from .core import Core 5 | -------------------------------------------------------------------------------- /Core/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .functions import * 4 | 5 | 6 | class Config(object): 7 | """配置设置""" 8 | 9 | def __init__(self): 10 | self.bundle_dir = Path(sys.argv[0]).parent 11 | self.vnu_config = configparser.ConfigParser() 12 | self.vnu_config_file = self.bundle_dir/'config.ini' 13 | self.vnu_log_file = self.bundle_dir/'logs.txt' 14 | self.vnu_lic_file = self.bundle_dir/'LICENSE' 15 | 16 | def reset_config(self): 17 | # 初始化 18 | self.vnu_config = configparser.ConfigParser() 19 | with open(self.vnu_config_file, 'w', newline='', encoding='utf-8') as vcf: 20 | self.vnu_config.add_section('General') 21 | self.vnu_config.set('General', 'cpu_cores', str(int(cpu_count()/2))) 22 | self.vnu_config.set('General', 'gpu_id', '0') 23 | self.vnu_config.set('General', 'encoding_list', 'Shift_JIS,UTF-8,GBK,UTF-16') 24 | # 图片设置 25 | self.vnu_config.add_section('Image') 26 | self.vnu_config.set('Image', 'image_sr_engine', 'waifu2x_ncnn') 27 | self.vnu_config.set('Image', 'image_batch_size', '10') 28 | # 视频设置 29 | self.vnu_config.add_section('Video') 30 | self.vnu_config.set('Video', 'video_sr_engine', 'anime4k') 31 | self.vnu_config.set('Video', 'video_batch_size', '20') 32 | self.vnu_config.set('Video', 'video_quality', '8') 33 | # 超分引擎设置 34 | self.vnu_config.add_section('SREngine') 35 | self.vnu_config.set('SREngine', 'tta', '0') 36 | # waifu2x-ncnn-vulkan相关配置 37 | self.vnu_config.add_section('waifu2x_ncnn') 38 | self.vnu_config.set('waifu2x_ncnn', 'noise_level', '3') 39 | self.vnu_config.set('waifu2x_ncnn', 'tile_size', '0') 40 | self.vnu_config.set('waifu2x_ncnn', 'model_name', 'models-cunet') 41 | self.vnu_config.set('waifu2x_ncnn', 'load_proc_save', '1:2:2') 42 | # Real-CUGAN相关配置 43 | self.vnu_config.add_section('real_cugan') 44 | self.vnu_config.set('real_cugan', 'noise_level', '3') 45 | self.vnu_config.set('real_cugan', 'tile_size', '0') 46 | self.vnu_config.set('real_cugan', 'sync_gap_mode', '3') 47 | self.vnu_config.set('real_cugan', 'model_name', 'models-se') 48 | self.vnu_config.set('real_cugan', 'load_proc_save', '1:2:2') 49 | # Real-ESRGAN相关配置 50 | self.vnu_config.add_section('real_esrgan') 51 | self.vnu_config.set('real_esrgan', 'tile_size', '0') 52 | self.vnu_config.set('real_esrgan', 'model_name', 'realesrgan-x4plus-anime') 53 | self.vnu_config.set('real_esrgan', 'load_proc_save', '1:2:2') 54 | # srmd-ncnn-vulkan相关配置 55 | self.vnu_config.add_section('srmd_ncnn') 56 | self.vnu_config.set('srmd_ncnn', 'noise_level', '3') 57 | self.vnu_config.set('srmd_ncnn', 'tile_size', '0') 58 | self.vnu_config.set('srmd_ncnn', 'load_proc_save', '1:2:2') 59 | # realsr-ncnn-vulkan相关配置 60 | self.vnu_config.add_section('realsr_ncnn') 61 | self.vnu_config.set('realsr_ncnn', 'tile_size', '0') 62 | self.vnu_config.set('realsr_ncnn', 'model_name', 'models-DF2K_JPEG') 63 | self.vnu_config.set('realsr_ncnn', 'load_proc_save', '1:2:2') 64 | # anime4kcpp相关配置 65 | self.vnu_config.add_section('anime4k') 66 | self.vnu_config.set('anime4k', 'acnet', '1') 67 | self.vnu_config.set('anime4k', 'hdn_mode', '1') 68 | self.vnu_config.set('anime4k', 'hdn_level', '1') 69 | self.vnu_config.write(vcf) 70 | self.load_config() 71 | 72 | def load_config(self): 73 | # 依赖工具集文件路径 74 | self.toolkit_path = self.bundle_dir/'Dependencies' 75 | # https://github.com/FFmpeg/FFmpeg 76 | self.ffmpeg = self.toolkit_path/'ffmpeg'/'bin'/'ffmpeg.exe' 77 | self.ffprobe = self.toolkit_path/'ffmpeg'/'bin'/'ffprobe.exe' 78 | # https://github.com/UlyssesWu/FreeMote 79 | self.psb_de_exe = self.toolkit_path/'FreeMoteToolkit'/'PsbDecompile.exe' 80 | self.psb_en_exe = self.toolkit_path/'FreeMoteToolkit'/'PsBuild.exe' 81 | # https://github.com/vn-toolkit/tlg2png 82 | self.tlg2png_exe = self.toolkit_path/'tlg2png'/'tlg2png.exe' 83 | # https://github.com/krkrz/krkr2 84 | self.krkrtpc_exe = self.toolkit_path/'krkrtpc'/'krkrtpc.exe' 85 | # https://github.com/zhiyb/png2tlg 86 | self.png2tlg6_exe = self.toolkit_path/'png2tlg6'/'png2tlg6.exe' 87 | # https://github.com/xmoeproject/AlphaMovieDecoder 88 | self.amv_de_exe = self.toolkit_path/'AlphaMovieDecoder'/'AlphaMovieDecoderFake.exe' 89 | self.amv_de_folder = self.toolkit_path/'AlphaMovieDecoder'/'video' 90 | # https://github.com/zhiyb/AlphaMovieEncoder 91 | self.amv_en_exe = self.toolkit_path/'AlphaMovieEncoder'/'amenc.exe' 92 | # https://github.com/AtomCrafty/MajiroTools 93 | self.mjotool_exe = self.toolkit_path/'MajiroTools'/'maji.exe' 94 | # 通用参数 95 | self.vnu_config.read(self.vnu_config_file) 96 | self.cpu_cores = self.vnu_config.getint('General', 'cpu_cores') 97 | self.gpu_id = self.vnu_config.get('General', 'gpu_id') 98 | self.encoding_list = [encoding.strip() for encoding in self.vnu_config.get('General', 'encoding_list').split(',')] 99 | # 图片设置 100 | self.image_sr_engine = self.vnu_config.get('Image', 'image_sr_engine') 101 | self.image_batch_size = self.vnu_config.getint('Image', 'image_batch_size') 102 | # 视频设置 103 | self.video_quality = self.vnu_config.get('Video', 'video_quality') 104 | self.video_batch_size = self.vnu_config.getint('Video', 'video_batch_size') 105 | self.video_sr_engine = self.vnu_config.get('Video', 'video_sr_engine') 106 | # 超分引擎设置 107 | self.tta = self.vnu_config.get('SREngine', 'tta') 108 | # waifu2x-ncnn-vulkan相关配置 109 | # https://github.com/nihui/waifu2x-ncnn-vulkan 110 | self.waifu2x_ncnn_exe = self.toolkit_path/'waifu2x-ncnn-vulkan'/'waifu2x-ncnn-vulkan.exe' 111 | self.waifu2x_ncnn_noise_level = self.vnu_config.get('waifu2x_ncnn', 'noise_level') 112 | self.waifu2x_ncnn_tile_size = self.vnu_config.get('waifu2x_ncnn', 'tile_size') 113 | self.waifu2x_ncnn_model_name = self.vnu_config.get('waifu2x_ncnn', 'model_name') 114 | self.waifu2x_ncnn_model_path = self.waifu2x_ncnn_exe.parent/self.waifu2x_ncnn_model_name 115 | self.waifu2x_ncnn_load_proc_save = self.vnu_config.get('waifu2x_ncnn', 'load_proc_save') 116 | # Real-CUGAN相关配置 117 | # https://github.com/nihui/realcugan-ncnn-vulkan 118 | self.real_cugan_exe = self.toolkit_path/'realcugan-ncnn-vulkan'/'realcugan-ncnn-vulkan.exe' 119 | self.real_cugan_noise_level = self.vnu_config.get('real_cugan', 'noise_level') 120 | self.real_cugan_tile_size = self.vnu_config.get('real_cugan', 'tile_size') 121 | self.real_cugan_sync_gap_mode = self.vnu_config.get('real_cugan', 'sync_gap_mode') 122 | # self.real_cugan_model_path = self.vnu_config.get('real_cugan', 'model_name') 123 | self.real_cugan_model_name = self.vnu_config.get('real_cugan', 'model_name') 124 | self.real_cugan_model_path = self.real_cugan_exe.parent/self.real_cugan_model_name 125 | self.real_cugan_load_proc_save = self.vnu_config.get('real_cugan', 'load_proc_save') 126 | # Real-ESRGAN相关配置 127 | # https://github.com/xinntao/Real-ESRGAN 128 | self.real_esrgan_exe = self.toolkit_path/'realesrgan-ncnn-vulkan'/'realesrgan-ncnn-vulkan.exe' 129 | self.real_esrgan_model_path = self.real_esrgan_exe.parent/'models' 130 | self.real_esrgan_tile_size = self.vnu_config.get('real_esrgan', 'tile_size') 131 | self.real_esrgan_model_name = self.vnu_config.get('real_esrgan', 'model_name') 132 | self.real_esrgan_load_proc_save = self.vnu_config.get('real_esrgan', 'load_proc_save') 133 | # srmd-ncnn-vulkan相关配置 134 | # https://github.com/nihui/srmd-ncnn-vulkan 135 | self.srmd_ncnn_exe = self.toolkit_path/'srmd-ncnn-vulkan'/'srmd-ncnn-vulkan.exe' 136 | self.srmd_ncnn_model_path = self.srmd_ncnn_exe.parent/'models-srmd' 137 | self.srmd_ncnn_noise_level = self.vnu_config.get('srmd_ncnn', 'noise_level') 138 | self.srmd_ncnn_tile_size = self.vnu_config.get('srmd_ncnn', 'tile_size') 139 | self.srmd_ncnn_load_proc_save = self.vnu_config.get('srmd_ncnn', 'load_proc_save') 140 | # realsr-ncnn-vulkan相关配置 141 | # https://github.com/nihui/realsr-ncnn-vulkan 142 | self.realsr_ncnn_exe = self.toolkit_path/'realsr-ncnn-vulkan'/'realsr-ncnn-vulkan.exe' 143 | self.realsr_ncnn_tile_size = self.vnu_config.get('realsr_ncnn', 'tile_size') 144 | self.realsr_ncnn_model_name = self.vnu_config.get('realsr_ncnn', 'model_name') 145 | self.realsr_ncnn_model_path = self.realsr_ncnn_exe.parent/self.realsr_ncnn_model_name 146 | self.realsr_ncnn_load_proc_save = self.vnu_config.get('realsr_ncnn', 'load_proc_save') 147 | # anime4k相关配置 148 | # https://github.com/TianZerL/Anime4KCPP 149 | self.anime4k_exe = self.toolkit_path/'Anime4KCPP_CLI'/'Anime4KCPP_CLI.exe' 150 | self.anime4k_acnet = self.vnu_config.get('anime4k', 'acnet') 151 | self.anime4k_hdn_mode = self.vnu_config.get('anime4k', 'hdn_mode') 152 | self.anime4k_hdn_level = self.vnu_config.get('anime4k', 'hdn_level') 153 | -------------------------------------------------------------------------------- /Core/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from .functions import * 4 | from .config import Config 5 | from .texts_utils import TextsUtils 6 | from .image_utils import ImageUtils 7 | from .video_utils import VideoUtils 8 | 9 | 10 | class Core(Config, TextsUtils, ImageUtils, VideoUtils): 11 | """核心""" 12 | 13 | uuid_list = [] 14 | 15 | def __init__(self): 16 | Config.__init__(self) 17 | self.encoding = 'UTF-8' 18 | 19 | def emit_info(self, info_str): 20 | print(info_str) 21 | logging.info(info_str) 22 | 23 | def emit_progress(self, _percent, _left_time): 24 | print(_percent, _left_time, sep='\t') 25 | 26 | def pool_run(self, target, runs, *args) -> list: 27 | """ 28 | @brief 使用进程池多进程加速计算 29 | 30 | @param target 目标执行函数 31 | @param runs 执行可变参数迭代器 32 | @param args 其它固定参数,按执行函数参数顺序输入 33 | 34 | @return 将执行函数的返回值以列表返回 35 | """ 36 | pool = Pool(self.cpu_cores) 37 | processer_ls = [] 38 | for i in runs: 39 | processer = pool.apply_async(target, args=(i, *args)) 40 | processer_ls.append(processer) 41 | # results = [] 42 | # for processer in processer_ls: 43 | # results.append(processer.get()) 44 | pool.close() 45 | pool.join() 46 | # return results 47 | return [processer.get() for processer in processer_ls] 48 | 49 | def create_str(self, len_num=8) -> str: 50 | """ 51 | @brief 生成不重复的指定位数的字符串 52 | 53 | @param len_num 字符串长度 54 | 55 | @return 字符串 56 | """ 57 | while True: 58 | uuid_str = str(uuid.uuid4())[:len_num] 59 | if uuid_str not in self.uuid_list: 60 | self.uuid_list.append(uuid_str) 61 | break 62 | return uuid_str 63 | -------------------------------------------------------------------------------- /Core/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import csv 6 | import json 7 | import time 8 | import math 9 | import uuid 10 | import zlib 11 | import shutil 12 | import hashlib 13 | import logging 14 | import tempfile 15 | import traceback 16 | import subprocess 17 | import configparser 18 | from threading import Thread 19 | from functools import partial 20 | from io import StringIO as _io_StringIO # 避免和construct库冲突 21 | from multiprocessing import Pool, cpu_count, Process, freeze_support 22 | # 第三方库 23 | import png 24 | import construct 25 | import regex as re 26 | from wmi import WMI 27 | from PIL import Image 28 | Image.MAX_IMAGE_PIXELS = None # 超大分辨率图片支持 29 | import numpy as np 30 | from numba import jit 31 | # 自定义库 32 | from .pathplus import Path 33 | 34 | 35 | def get_gpu_list() -> list: 36 | """ 37 | @brief 获取显卡名称列表 38 | 39 | @return 返回显卡名称列表 40 | """ 41 | GPUs = WMI().Win32_VideoController() 42 | GPU_name_list = [i.name for i in GPUs] 43 | return GPU_name_list 44 | 45 | 46 | def get_gpu_id(GPU_name) -> str: 47 | """ 48 | @brief 获取显卡ID 49 | 50 | @param GPU_name 显卡名 51 | 52 | @return 返回显卡ID 53 | """ 54 | GPU_name_list = get_gpu_list() 55 | GPU_ID = str(GPU_name_list.index(GPU_name)) 56 | return GPU_ID 57 | 58 | 59 | def real_digit(str1) -> bool: 60 | """ 61 | @brief 判断字符串是否为数字 62 | 63 | @param str1 字符串 64 | 65 | @return 布尔值 66 | """ 67 | try: 68 | tmp = float(str1) 69 | return True 70 | except: 71 | return False 72 | 73 | 74 | def pattern_num2x(re_result, scale_ratio, test_mode=False, line=None) -> str: 75 | """ 76 | @brief 将正则匹配结果中的数字乘以放大倍数 77 | 78 | @param re_result re.match()捕获的正则匹配结果 79 | @param scale_ratio 放大倍数 80 | @param test_mode 测试模式 81 | @param line 原始行字符串,需要test_mode为True 82 | 83 | @return 放大数字后的行字符串 84 | """ 85 | re_result_ls = list(re_result.groups()) 86 | if test_mode: 87 | print(line) 88 | print(re_result_ls) 89 | for i in range(len(re_result_ls)): 90 | if real_digit(re_result_ls[i]): 91 | if test_mode: 92 | print(re_result_ls[i], sep=', ') 93 | re_result_ls[i] = str(int(float(re_result_ls[i])*scale_ratio)) 94 | re_result_ls = [i for i in re_result_ls if i != None] 95 | line = ''.join(re_result_ls) 96 | if test_mode: 97 | print(line) 98 | return line 99 | 100 | 101 | def seconds_format(time_length) -> str: 102 | """ 103 | @brief 将秒格式化输出为时分秒 104 | 105 | @param time_length 时长 106 | 107 | @return 输出字符串 108 | """ 109 | m, s = divmod(time_length, 60) 110 | h, m = divmod(m, 60) 111 | return "%02dh%02dm%02ds" % (h, m, s) 112 | 113 | 114 | def batch_group_list(inlist, batch_size=10) -> list: 115 | """ 116 | @brief 将列表以指定大小划分为多个列表 117 | 118 | @param inlist 输入列表 119 | @param batch_size 每个列表元素数量 120 | 121 | @return 划分列表的列表 122 | """ 123 | group_list = [] 124 | start_index = 0 125 | while True: 126 | end_index = start_index+batch_size 127 | group = inlist[start_index:end_index] 128 | if group == []: 129 | break 130 | group_list.append(group) 131 | start_index = end_index 132 | return group_list 133 | 134 | 135 | def pool_run(workers, target, runs, *args) -> list: 136 | """ 137 | @brief 使用进程池多进程加速计算 138 | 139 | @param workers 进程数 140 | @param target 目标执行函数 141 | @param runs 执行可变参数迭代器 142 | @param args 其它固定参数,按执行函数参数顺序输入 143 | 144 | @return 将执行函数的返回值以列表返回 145 | """ 146 | pool = Pool(workers) 147 | processer_ls = [] 148 | for i in runs: 149 | processer = pool.apply_async(target, args=(i, *args)) 150 | processer_ls.append(processer) 151 | pool.close() 152 | pool.join() 153 | return [processer.get() for processer in processer_ls] 154 | 155 | 156 | def show_folder(folder_path): 157 | """ 158 | @brief 显示文件夹 159 | 160 | @param folder_path 文件夹路径 161 | """ 162 | folder_path = Path(folder_path) 163 | try: 164 | os.startfile(folder_path) 165 | except: 166 | _p = subprocess.run(['start', folder_path], capture_output=True, shell=True) 167 | 168 | 169 | def sub_scale_num(match, scale_ratio): 170 | """ 171 | @brief 用于放大正则替换匹配到的数字pattern.sub(partial(sub_scale_num, scale_ratio), line) 172 | 173 | @param match The match 174 | @param scale_ratio 放大倍数 175 | 176 | @return 放大后的数字 177 | """ 178 | num_ = match.group() 179 | scaled_num_ = str(int(float(num_) * scale_ratio)) 180 | return scaled_num_ 181 | -------------------------------------------------------------------------------- /Core/pathplus.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import shutil 5 | import pathlib 6 | 7 | 8 | class Path(pathlib.Path): 9 | """ 10 | @brief 提供基于pathlib的定制化文件路径操作 11 | """ 12 | 13 | _flavour = type(pathlib.Path())._flavour 14 | 15 | def __new__(cls, *args, **kwargs): 16 | return pathlib.Path.__new__(cls, *args, **kwargs).resolve() 17 | 18 | def move_to(self, target_dir): 19 | """ 20 | @brief 移动到指定目录 21 | 22 | @param target_dir 目录路径 23 | 24 | @return 目标路径 25 | """ 26 | target_dir = self.__class__(target_dir) 27 | if target_dir.exists() and target_dir.is_file(): 28 | raise Exception('目标路径必须是文件夹') 29 | target_path = target_dir/self.name 30 | if target_path == self: 31 | pass 32 | else: 33 | if not target_dir.exists(): 34 | target_dir.mkdirs() 35 | if self.is_file(): 36 | if target_path.exists(): 37 | target_path.unlink() 38 | shutil.move(self, target_path) 39 | elif self.is_dir(): 40 | if target_path.exists(): 41 | shutil.rmtree(target_path) 42 | shutil.move(self, target_path) 43 | else: 44 | raise Exception(f'{self}必须是文件或文件夹') 45 | return target_path 46 | 47 | def copy_to(self, target_dir): 48 | """ 49 | @brief 复制到指定目录 50 | 51 | @param target_dir 目录路径 52 | 53 | @return 目标路径 54 | """ 55 | target_dir = self.__class__(target_dir) 56 | if target_dir.exists() and target_dir.is_file(): 57 | raise Exception(f'{target_dir}必须是文件夹') 58 | target_path = target_dir/self.name 59 | if target_path == self: 60 | pass 61 | else: 62 | if not target_dir.exists(): 63 | target_dir.mkdirs() 64 | if self.is_file(): 65 | if target_path.exists(): 66 | target_path.unlink() 67 | shutil.copyfile(self, target_path) 68 | elif self.is_dir(): 69 | if target_path.exists(): 70 | shutil.rmtree(target_path) 71 | shutil.copytree(self, target_path) 72 | else: 73 | raise Exception(f'{self}必须是文件或文件夹') 74 | return target_path 75 | 76 | def move_as(self, target_path): 77 | """ 78 | @brief 移动为指定路径 79 | 80 | @param target_path 目标路径 81 | 82 | @return 目标路径 83 | """ 84 | target_path = self.__class__(target_path) 85 | if target_path == self: 86 | pass 87 | else: 88 | if not target_path.parent.exists(): 89 | target_path.parent.mkdirs() 90 | if self.is_file(): 91 | if target_path.exists(): 92 | target_path.unlink() 93 | shutil.move(self, target_path) 94 | elif self.is_dir(): 95 | if target_path.exists(): 96 | shutil.rmtree(target_path) 97 | shutil.move(self, target_path) 98 | else: 99 | raise Exception(f'{self}必须是文件或文件夹') 100 | return target_path 101 | 102 | def copy_as(self, target_path): 103 | """ 104 | @brief 复制为指定路径 105 | 106 | @param target_path 目标路径 107 | 108 | @return 目标路径 109 | """ 110 | target_path = self.__class__(target_path) 111 | if target_path == self: 112 | pass 113 | else: 114 | if not target_path.parent.exists(): 115 | target_path.parent.mkdirs() 116 | if self.is_file(): 117 | if target_path.exists(): 118 | target_path.unlink() 119 | shutil.copyfile(self, target_path) 120 | elif self.is_dir(): 121 | if target_path.exists(): 122 | shutil.rmtree(target_path) 123 | shutil.copytree(self, target_path) 124 | else: 125 | raise Exception(f'{self}必须是文件或文件夹') 126 | return target_path 127 | 128 | @property 129 | def parent_names(self) -> list: 130 | """ 131 | @brief 获取父目录名的列表 132 | 133 | @return 父目录名列表 134 | """ 135 | parent_names = [i.name for i in self.parents] 136 | parent_names.remove('') 137 | return parent_names 138 | 139 | def file_list(self, extension=None, walk_mode=True, ignored_folders=None, parent_folder=None) -> list: 140 | """ 141 | @brief 获取文件夹中文件的路径对象 142 | 143 | @param extension 指定扩展名 144 | @param walk_mode 是否查找子文件夹 145 | @param ignored_folders 忽略文件夹列表,不遍历其子目录 146 | @param parent_folder 父级文件夹,包含子目录 147 | 148 | @return 文件路径对象列表 149 | """ 150 | if not self.is_dir(): 151 | raise Exception(f'{self}必须是文件夹') 152 | file_path_ls = [] 153 | for root, dirs, files in os.walk(self, topdown=True): 154 | root = self.__class__(root) 155 | if ignored_folders is not None: 156 | dirs[:] = [d for d in dirs if d not in ignored_folders] 157 | for file in files: 158 | file_path = root/file 159 | if extension is None: 160 | file_path_ls.append(file_path) 161 | else: 162 | if file_path.suffix.lower() == '.'+extension.lower(): 163 | file_path_ls.append(file_path) 164 | if walk_mode == False: 165 | break 166 | if parent_folder: 167 | file_path_ls = [file_path for file_path in file_path_ls if parent_folder in file_path.parent_names] 168 | return file_path_ls 169 | 170 | def folder_list(self): 171 | """ 172 | @brief 获取文件夹内的子文件夹的路径 173 | 174 | @return 子文件夹路径列表 175 | """ 176 | if not self.is_dir(): 177 | raise Exception(f'{self}必须是文件夹') 178 | dir_path_ls = [] 179 | for root, dirs, files in os.walk(self, topdown=True): 180 | root = self.__class__(root) 181 | for _dir in dirs: 182 | dir_path = root/_dir 183 | dir_path_ls.append(dir_path) 184 | return dir_path_ls 185 | 186 | def flat_folder_(self, del_folder=True) -> list: 187 | """ 188 | @brief 将文件夹中子文件夹中的图片移动到自身文件夹根目录 189 | 190 | @param del_folder 是否删除空文件夹 191 | 192 | @return 所有文件列表 193 | """ 194 | if not self.is_dir(): 195 | raise Exception(f'{self}必须是文件夹') 196 | flat_file_ls = [file.move_to(self) for file in self.file_list()] 197 | if del_folder: 198 | for i in self.iterdir(): 199 | if i.is_dir(): 200 | shutil.rmtree(i) 201 | return flat_file_ls 202 | 203 | def flat_folder(self, output_folder) -> list: 204 | """ 205 | @brief 将文件夹中子文件夹中的图片复制到指定文件夹根目录 206 | 207 | @param output_folder 输出目录 208 | 209 | @return 所有目标文件列表 210 | """ 211 | if not self.is_dir(): 212 | raise Exception(f'{self}必须是文件夹') 213 | output_folder = self.__class__(output_folder) 214 | flat_file_ls = [file.copy_to(output_folder) for file in self.file_list()] 215 | return flat_file_ls 216 | 217 | def reio_path(self, input_folder, output_folder, mk_dir=False): 218 | """ 219 | @brief 返回相对于输入文件夹目录结构在输出文件夹的路径 220 | 221 | @param input_folder 输入文件夹路径 222 | @param output_folder 输出文件夹路径 223 | @param mk_dir 如果目标路径所在的目录不存在,则创建改目录 224 | 225 | @return 目标路径 226 | """ 227 | input_folder = self.__class__(input_folder) 228 | output_folder = self.__class__(output_folder) 229 | target_path = output_folder/self.relative_to(input_folder) 230 | if mk_dir: 231 | if not target_path.parent.exists(): 232 | target_path.parent.mkdirs() 233 | return target_path 234 | 235 | @property 236 | def to_str(self) -> str: 237 | """ 238 | @brief 转化为字符串 239 | 240 | @return 字符串形式的路径 241 | """ 242 | return str(self) 243 | 244 | def sweep(self): 245 | """ 246 | @brief 清空文件夹 247 | """ 248 | if not self.is_dir(): 249 | raise Exception(f'{self}必须是文件夹') 250 | shutil.rmtree(self) 251 | self.mkdirs() 252 | 253 | def readbs(self, size=None) -> bytes: 254 | """ 255 | @brief 读取指定长度字节,不指定长度则全部读取 256 | 257 | @param size 长度 258 | 259 | @return 指定长度字节 260 | """ 261 | with open(self, 'rb') as _f: 262 | _bytes = _f.read(size) if size is not None else _f.read() 263 | return _bytes 264 | 265 | def mkdirs(self): 266 | """ 267 | @brief 创建多级目录,并避免并发时小概率因文件夹已存在而引发的错误 268 | """ 269 | self.mkdir(parents=True, exist_ok=True) 270 | -------------------------------------------------------------------------------- /Core/superview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import math 5 | 6 | # modified from https://intofpv.com/t-using-free-command-line-sorcery-to-fake-superview 7 | 8 | 9 | def derp_it(tx, target_width, src_width): 10 | x = (float(tx)/target_width - 0.5) * 2 # -1 -> 1 11 | sx = tx - (target_width - src_width)/2 12 | offset = math.pow(x, 2) * (-1 if x < 0 else 1) * ((target_width - src_width)/2) 13 | return sx - offset 14 | 15 | 16 | def go(): 17 | target_width = int(sys.argv[1]) 18 | height = int(sys.argv[2]) 19 | src_width = int(sys.argv[3]) 20 | 21 | xmap = open('xmap.pgm', 'w') 22 | xmap.write('P2 {0} {1} 65535\n'.format(target_width, height)) 23 | 24 | for y in range(height): 25 | for x in range(target_width): 26 | fudgeit = derp_it(x, target_width, src_width) 27 | xmap.write('{0} '.format(int(fudgeit))) 28 | xmap.write('\n') 29 | 30 | xmap.close() 31 | 32 | ymap = open('ymap.pgm', 'w') 33 | ymap.write('P2 {0} {1} 65535\n'.format(target_width, height)) 34 | 35 | for y in range(height): 36 | for x in range(target_width): 37 | ymap.write('{0} '.format(y)) 38 | ymap.write('\n') 39 | 40 | ymap.close() 41 | 42 | 43 | if __name__ == '__main__': 44 | go() 45 | -------------------------------------------------------------------------------- /Core/texts_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .functions import * 4 | 5 | 6 | class TextsUtils(object): 7 | """ 8 | @brief 专用于处理文本文件 9 | """ 10 | 11 | def get_encoding(self, text_file) -> str: 12 | """ 13 | @brief 获取文本编码,获取不到返回None,优先从自定义编码列表中识别 14 | 15 | @param text_file 文本文件路径 16 | 17 | @return 文本编码格式 18 | """ 19 | encoding = None 20 | with open(text_file, 'rb') as f: 21 | content_b = f.read() 22 | for _encoding in self.encoding_list: 23 | try: 24 | content_b.decode(encoding=_encoding) 25 | encoding = _encoding 26 | break 27 | except: 28 | pass 29 | if encoding is None: 30 | encoding = chardet.detect(content_b) 31 | return encoding 32 | 33 | def get_lines_encoding(self, text_file, split=True): 34 | ''' 35 | 返回文本内容和编码 36 | ''' 37 | try: 38 | with open(text_file, newline='', encoding=self.encoding) as f: 39 | lines = f.readlines() if split else f.read() 40 | current_encoding = self.encoding 41 | except: 42 | current_encoding = self.get_encoding(text_file) 43 | assert current_encoding != None, f'未能正确识别{text_file}的文本编码' 44 | with open(text_file, newline='', encoding=current_encoding) as f: 45 | lines = f.readlines() if split else f.read() 46 | return lines, current_encoding 47 | 48 | def csv2x(self, input_csv, output_csv, scale_ratio): 49 | ''' 50 | 将csv文件中的数字乘以放大倍数 51 | ''' 52 | result = [] 53 | lines, current_encoding = self.get_lines_encoding(input_csv, split=False) 54 | with _io_StringIO(lines) as _f: 55 | content = list(csv.reader(_f)) 56 | # try: 57 | # with open(input_csv, newline='', encoding=self.encoding) as f: 58 | # current_encoding = self.encoding 59 | # content = list(csv.reader(f)) 60 | # except: 61 | # current_encoding = self.get_encoding(input_csv) 62 | # with open(input_csv, newline='', encoding=current_encoding) as f: 63 | # content = list(csv.reader(f)) 64 | for content_ls in content: 65 | for i in range(len(content_ls)): 66 | if real_digit(content_ls[i]): 67 | content_ls[i] = str(int(float(content_ls[i]) * scale_ratio)) 68 | result.append(content_ls) 69 | with open(output_csv, 'w', newline='', encoding=current_encoding) as f: 70 | csvwtr = csv.writer(f) 71 | csvwtr.writerows(result) 72 | 73 | def line_pattern_num2x(self, re_result, test_mode=False, line=None) -> str: 74 | """ 75 | @brief 将正则匹配结果中的数字乘以放大倍数 76 | 77 | @param re_result re.match()捕获的正则匹配结果 78 | @param test_mode 测试模式 79 | @param line 原始行字符串,需要test_mode为True 80 | 81 | @return 放大数字后的行字符串 82 | """ 83 | return pattern_num2x(re_result, self.scale_ratio, test_mode=test_mode, line=line) 84 | 85 | def _sub_scale_num(self, match): 86 | """ 87 | @brief 用于放大正则替换匹配到的数字pattern.sub(self.sub_scale_num), line),放大倍数为self.scale_ratio 88 | 89 | @param match The match 90 | 91 | @return 放大后的数字 92 | """ 93 | num_ = match.group() 94 | scaled_num_ = str(int(float(num_) * self.scale_ratio)) 95 | return scaled_num_ 96 | -------------------------------------------------------------------------------- /Core/video_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .functions import * 4 | 5 | 6 | class VideoUtils(object): 7 | """ 8 | @brief 专用于处理视频文件 9 | """ 10 | 11 | def video_info(self, input_video) -> dict or bool: 12 | """ 13 | @brief 调用ffprobe获取视频信息 14 | 15 | @param input_video 输入视频 16 | 17 | @return 返回视频信息字典,未识别视频返回False 18 | """ 19 | input_video = Path(input_video) 20 | options = [self.ffprobe, 21 | '-show_format', 22 | '-show_streams', 23 | '-of', 'json', 24 | input_video 25 | ] 26 | get_video_info_p = subprocess.run(options, capture_output=True, shell=True) 27 | unsort_video_info = json.loads(get_video_info_p.stdout.decode('utf-8')) 28 | # 非常规编码视频返回空字典 29 | if not unsort_video_info or len(unsort_video_info['streams']) == 0: 30 | return False 31 | else: 32 | if len(unsort_video_info['streams']) == 1: 33 | video = 0 34 | audio = None 35 | elif unsort_video_info['streams'][0]['codec_type'] == 'video' and unsort_video_info['streams'][1]['codec_type'] == 'audio': 36 | video = 0 37 | audio = 1 38 | elif unsort_video_info['streams'][1]['codec_type'] == 'video' and unsort_video_info['streams'][0]['codec_type'] == 'audio': 39 | video = 1 40 | audio = 0 41 | video_info = {} 42 | video_info['vcodec'] = unsort_video_info['streams'][video]['codec_name'] 43 | video_info['width'] = unsort_video_info['streams'][video]['width'] 44 | video_info['height'] = unsort_video_info['streams'][video]['height'] 45 | try: 46 | video_info['frame_rate'] = '%.2f' % eval(unsort_video_info['streams'][video]['avg_frame_rate']) 47 | except: 48 | video_info['frame_rate'] = '%.2f' % eval(unsort_video_info['streams'][video]['r_frame_rate']) 49 | # video_info['bit_rate'] = unsort_video_info['streams'][video]['bit_rate'] 50 | video_info['video_duration'] = unsort_video_info['streams'][video]['duration'] 51 | if audio != None: 52 | video_info['acodec'] = unsort_video_info['streams'][audio]['codec_name'] 53 | video_info['audio_duration'] = unsort_video_info['streams'][audio]['duration'] 54 | return video_info 55 | 56 | def vcodec_trans(self, input_video, output_video, output_vcodec): 57 | """ 58 | @brief 视频转码、压制 59 | 60 | @param input_video 输入视频路径 61 | @param output_video 输出视频路径 62 | @param output_vcodec 输出视频编码 63 | """ 64 | input_video = Path(input_video) 65 | output_video = Path(output_video) 66 | options = [self.ffmpeg, '-y', 67 | '-i', input_video, 68 | '-c:v', output_vcodec, 69 | '-q:v', self.output_video_quality(output_vcodec), 70 | output_video 71 | ] 72 | format_trans_p = subprocess.run(options, capture_output=True, shell=True) 73 | return output_video 74 | 75 | def video2png(self, input_video, output_folder): 76 | """ 77 | @brief 将视频转换为png图片序列 78 | 79 | @param input_video 输入视频 80 | @param output_folder 输出文件夹 81 | 82 | @return 输出文件夹, 图片序列名称 83 | """ 84 | input_video = Path(input_video) 85 | output_folder = Path(output_folder) 86 | if not output_folder.exists(): 87 | output_folder.mkdir(parents=True) 88 | png_sequence = output_folder/(input_video.stem+'_%08d.png') 89 | options = [self.ffmpeg, '-y', 90 | '-i', input_video, 91 | '-qscale:v', '1', 92 | '-qmin', '1', 93 | '-qmax', '1', 94 | '-vsync', '0', 95 | '-threads', str(self.cpu_cores), 96 | png_sequence 97 | ] 98 | video2png_p = subprocess.run(options, capture_output=True, shell=True) 99 | return png_sequence 100 | 101 | def output_video_quality(self, output_vcodec) -> str: 102 | """ 103 | @brief 返回ffmpeg输出视频的质量参数 104 | 105 | @param output_vcodec 输出视频编码 106 | 107 | @return 视频质量参数 108 | """ 109 | special_vcodecs = ['theora'] 110 | if output_vcodec not in special_vcodecs: 111 | video_quality = str(10 - int(self.video_quality)) 112 | else: 113 | # 特殊编码视频的质量设定与常规视频不统一 114 | video_quality = self.video_quality 115 | return video_quality 116 | 117 | def png2video(self, png_sequence, origin_video, output_video, output_vcodec=None): 118 | """ 119 | @brief 将放大后的png图片序列转换回视频 120 | 121 | @param png_sequence png图片序列 122 | @param origin_video 原始视频(提供音频) 123 | @param output_video 输出视频路径 124 | @param output_vcodec 输出视频编码格式,默认为原视频编码格式 125 | 126 | @return 输出视频路径 127 | """ 128 | png_sequence = Path(png_sequence) 129 | origin_video = Path(origin_video) 130 | output_video = Path(output_video) 131 | origin_video_info = self.video_info(origin_video) 132 | if output_vcodec is None: 133 | output_vcodec = origin_video_info['vcodec'] 134 | if output_vcodec == 'wmv3': 135 | output_vcodec = 'wmv2' 136 | elif output_vcodec == 'wmv3': 137 | raise ValueError('输出视频编码不能为wmv3') 138 | options = [self.ffmpeg, '-y', 139 | '-r', origin_video_info['frame_rate'], 140 | '-i', png_sequence.to_str, 141 | '-i', origin_video, 142 | '-map', '0:v:0', 143 | '-map', '1:a:0?', 144 | '-c:a', 'copy', 145 | '-c:v', output_vcodec, 146 | '-r', origin_video_info['frame_rate'], 147 | '-q:v', self.output_video_quality(output_vcodec), 148 | '-threads', str(self.cpu_cores), 149 | output_video 150 | ] 151 | png2video_p = subprocess.run(options, capture_output=True, shell=True) 152 | return output_video 153 | 154 | def video_upscale(self, input_video, output_video, scale_ratio=2.0, output_vcodec=None): 155 | """ 156 | @brief 视频放大 157 | 158 | @param input_video 输出视频路径 159 | @param output_video 输出视频路径 160 | @param scale_ratio 视频放大倍数 161 | @param output_vcodec 输出视频编码 162 | 163 | @return 输出视频路径 164 | """ 165 | input_video = Path(input_video) 166 | output_video = Path(output_video) 167 | if not output_video.parent.exists(): 168 | output_video.parent.mkdir(parents=True) 169 | if output_vcodec is None: 170 | output_vcodec = self.video_info(input_video)['vcodec'] 171 | # ffmpeg不支持wmv3编码 172 | if output_vcodec == 'wmv3': 173 | output_vcodec = 'wmv2' 174 | elif output_vcodec == 'wmv3': 175 | raise ValueError('输出视频编码不能为wmv3') 176 | with tempfile.TemporaryDirectory() as video_tmp_folder1: 177 | video_tmp_folder1 = Path(video_tmp_folder1) 178 | self.emit_info(f'{input_video}拆帧中......') 179 | png_sequence = self.video2png(input_video, video_tmp_folder1) 180 | self.emit_info(f'{input_video}放大中......') 181 | self.image_upscale(video_tmp_folder1, video_tmp_folder1, scale_ratio, video_mode=True) 182 | tmp_video = video_tmp_folder1/output_video.name 183 | self.emit_info(f'{output_video}编码中......') 184 | tmp_video = self.png2video(png_sequence, input_video, tmp_video, output_vcodec) 185 | tmp_video.move_as(output_video) 186 | return output_video 187 | -------------------------------------------------------------------------------- /GUI/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .main_ui import MainUI 5 | from .flat_widgets import * 6 | -------------------------------------------------------------------------------- /GUI/artemis_part.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | 6 | 7 | class ArtemisPart(FTabWidget): 8 | def __init__(self): 9 | FTabWidget.__init__(self, height=40, position='top') 10 | self.icon_folder = Path(sys.argv[0]).parent/'Icons' 11 | self.initUI() 12 | self.set_ratio_state() 13 | self.set_resolution_state() 14 | self.select_all_part() 15 | self.set_game_resolution_encoding(1280, 720, 'UTF-8') 16 | # self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 17 | 18 | def initUI(self): 19 | 20 | self.setup_hd_parts() 21 | self.addTab(self.hd_parts_frame, '高清重制') 22 | 23 | self.setup_work_up() 24 | self.addTab(self.work_up_frame, '重制后处理') 25 | 26 | self.setup_zip() 27 | self.addTab(self.zip_frame, '存储优化') 28 | 29 | self.setup_connections() 30 | 31 | def setup_zip(self): 32 | self.zip_frame = QFrame() 33 | layout = QVBoxLayout(self.zip_frame) 34 | layout.addWidget(QLabel('正在开发中...')) 35 | 36 | def setup_hd_parts(self): 37 | self.hd_parts_frame = QFrame() 38 | layout = QHBoxLayout(self.hd_parts_frame) 39 | 40 | self.setup_choose_resolution() 41 | layout.addWidget(self.choose_resolution_Frame, 1) 42 | 43 | self.setup_select_run_parts() 44 | layout.addWidget(self.select_run_parts_frame, 1) 45 | 46 | def setup_choose_resolution(self): 47 | self.choose_resolution_Frame = QFrame() 48 | layout1 = QFormLayout(self.choose_resolution_Frame) 49 | self.choose_resolution_lb = QLabel('分辨率设定:') 50 | layout2 = QVBoxLayout() 51 | layout2.setContentsMargins(0, 5, 0, 0) 52 | layout1.addRow(self.choose_resolution_lb, layout2) 53 | 54 | formlayout1 = QFormLayout() 55 | self.before_resolution_lb = QLabel('原生分辨率:') 56 | resolution_hlayout = QHBoxLayout() 57 | resolution_hlayout.setContentsMargins(0, 0, 0, 0) 58 | resolution_hlayout.setSpacing(15) 59 | self.before_resolution = QLabel() 60 | self.main_encoding = QLabel() 61 | resolution_hlayout.addWidget(self.before_resolution) 62 | resolution_hlayout.addWidget(self.main_encoding) 63 | self.check_resolution_btn = FPushButton(text='检测分辨率', height=20, minimum_width=80, text_padding=0, text_align='center', border_radius=10) 64 | self.check_resolution_btn.show_shadow() 65 | resolution_hlayout.addWidget(self.check_resolution_btn) 66 | _spacer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 67 | resolution_hlayout.addItem(_spacer) 68 | formlayout1.addRow(self.before_resolution_lb, resolution_hlayout) 69 | 70 | self.s1080p_btn = QRadioButton('1080P') 71 | self.s2k_btn = QRadioButton('2K') 72 | self.s4k_btn = QRadioButton('4K') 73 | 74 | formlayout2 = QFormLayout() 75 | 76 | self.custiom_ratio_btn = QRadioButton('自定义放大倍率:') 77 | self.custiom_ratio_spinbox = QDoubleSpinBox() 78 | self.custiom_ratio_spinbox.setDecimals(3) 79 | self.custiom_ratio_spinbox.setSingleStep(0.5) 80 | formlayout2.addRow(self.custiom_ratio_btn, self.custiom_ratio_spinbox) 81 | 82 | self.custom_resolution_btn = QRadioButton('自定义分辨率:') 83 | custom_resolution_hlayout = QHBoxLayout() 84 | self.width_line_edit = FLineEdit() 85 | self.x_label = QLabel('x') 86 | self.x_label.setMaximumWidth(10) 87 | self.height_line_edit = FLineEdit() 88 | custom_resolution_hlayout.addWidget(self.width_line_edit) 89 | custom_resolution_hlayout.addWidget(self.x_label) 90 | custom_resolution_hlayout.addWidget(self.height_line_edit) 91 | formlayout2.addRow(self.custom_resolution_btn, custom_resolution_hlayout) 92 | 93 | layout2.addLayout(formlayout1) 94 | layout2.addWidget(self.s1080p_btn) 95 | layout2.addWidget(self.s2k_btn) 96 | layout2.addWidget(self.s4k_btn) 97 | layout2.addLayout(formlayout2) 98 | # 分组 99 | self.sr_group = QButtonGroup() 100 | self.sr_group.addButton(self.s1080p_btn) 101 | self.sr_group.addButton(self.s2k_btn) 102 | self.sr_group.addButton(self.s4k_btn) 103 | self.sr_group.addButton(self.custiom_ratio_btn) 104 | self.sr_group.addButton(self.custom_resolution_btn) 105 | 106 | def setup_select_run_parts(self): 107 | self.select_run_parts_frame = QFrame() 108 | layout1 = QFormLayout(self.select_run_parts_frame) 109 | 110 | self.game_part_lb = QLabel('处理部分选择:') 111 | layout2 = QVBoxLayout() 112 | layout2.setContentsMargins(0, 4, 0, 0) 113 | layout1.addRow(self.game_part_lb, layout2) 114 | 115 | self.text_part = QCheckBox('文本') 116 | self.image_part = QCheckBox('图片') 117 | self.animation_part = QCheckBox('动画') 118 | self.video_part = QCheckBox('视频') 119 | self.select_all_btn = FPushButton(text='全选', height=20, minimum_width=50, text_padding=0, text_align='center', border_radius=10) 120 | self.select_none_btn = FPushButton(text='全不选', height=20, minimum_width=50, text_padding=0, text_align='center', border_radius=10) 121 | hlayout = QHBoxLayout() 122 | hlayout.addWidget(self.select_all_btn) 123 | hlayout.addWidget(self.select_none_btn) 124 | 125 | layout2.addWidget(self.text_part) 126 | layout2.addWidget(self.image_part) 127 | layout2.addWidget(self.animation_part) 128 | layout2.addWidget(self.video_part) 129 | layout2.addLayout(hlayout) 130 | 131 | self.patch_mode_lb = QLabel('高清补丁输出:') 132 | self.keep_mode_btn = QCheckBox('保持目录结构') 133 | layout1.addRow(self.patch_mode_lb, self.keep_mode_btn) 134 | self.keep_mode_btn.setChecked(True) 135 | self.keep_mode_btn.setDisabled(True) 136 | 137 | def setup_work_up(self): 138 | self.work_up_frame = QFrame() 139 | self.work_up_layout = QFormLayout(self.work_up_frame) 140 | self.work_up_layout.setContentsMargins(15, 25, 15, 0) 141 | self.work_up_layout.setSpacing(15) 142 | 143 | self.setup_pfs_unpack() 144 | 145 | self.work_up_group = QButtonGroup() 146 | self.work_up_group.addButton(self.pfs_unpack_btn) 147 | 148 | def setup_pfs_unpack(self): 149 | self.pfs_unpack_btn = QRadioButton('批量解包:') 150 | self.pfs_unpack_btn.setChecked(True) 151 | layout = QHBoxLayout() 152 | self.work_up_layout.addRow(self.pfs_unpack_btn, layout) 153 | self.pfs_encoding_label = QLabel('编码格式:') 154 | self.pfs_encoding_line_edit = FLineEdit('UTF-8') 155 | layout.addWidget(self.pfs_encoding_label) 156 | layout.addWidget(self.pfs_encoding_line_edit) 157 | 158 | def check_pfs_unpack_btn(self): 159 | self.pfs_unpack_btn.setChecked(True) 160 | 161 | 162 | def setup_connections(self): 163 | self.s1080p_btn.toggled.connect(self.s1080p_btn_ratio) 164 | self.s2k_btn.toggled.connect(self.s2k_btn_ratio) 165 | self.s4k_btn.toggled.connect(self.s4k_btn_ratio) 166 | self.custiom_ratio_btn.toggled.connect(self.set_ratio_state) 167 | self.custiom_ratio_spinbox.textChanged.connect(self.auto_change_width_height) 168 | self.custom_resolution_btn.toggled.connect(self.set_resolution_state) 169 | self.width_line_edit.textEdited.connect(self.auto_change_height_ratio) 170 | self.height_line_edit.textEdited.connect(self.auto_change_width_ratio) 171 | self.select_all_btn.clicked.connect(self.select_all_part) 172 | self.select_none_btn.clicked.connect(self.select_none_part) 173 | self.pfs_encoding_line_edit.textChanged.connect(self.check_pfs_unpack_btn) 174 | 175 | def select_all_part(self): 176 | for check_box in self.select_run_parts_frame.findChildren(QCheckBox): 177 | if check_box is self.keep_mode_btn: 178 | continue 179 | check_box.setChecked(True) 180 | 181 | def select_none_part(self): 182 | for check_box in self.select_run_parts_frame.findChildren(QCheckBox): 183 | if check_box is self.keep_mode_btn: 184 | continue 185 | check_box.setChecked(False) 186 | 187 | def set_ratio_state(self): 188 | if self.custiom_ratio_btn.isChecked(): 189 | self.custiom_ratio_spinbox.setEnabled(True) 190 | else: 191 | self.custiom_ratio_spinbox.setDisabled(True) 192 | 193 | def set_resolution_state(self): 194 | if self.custom_resolution_btn.isChecked(): 195 | self.width_line_edit.setEnabled(True) 196 | self.height_line_edit.setEnabled(True) 197 | else: 198 | self.width_line_edit.setDisabled(True) 199 | self.height_line_edit.setDisabled(True) 200 | 201 | def auto_change_height_ratio(self): 202 | width, height = map(int, self.before_resolution.text().split('x')) 203 | try: 204 | ratio = int(self.width_line_edit.text())/width 205 | self.custiom_ratio_spinbox.setValue(ratio) 206 | self.height_line_edit.setText(str(int(height*ratio))) 207 | except: 208 | pass 209 | 210 | def auto_change_width_ratio(self): 211 | width, height = map(int, self.before_resolution.text().split('x')) 212 | try: 213 | pass 214 | ratio = int(self.height_line_edit.text())/height 215 | self.custiom_ratio_spinbox.setValue(ratio) 216 | self.width_line_edit.setText(str(int(width*ratio))) 217 | except: 218 | pass 219 | 220 | def auto_change_width_height(self): 221 | # 避免信号冲突 222 | if not self.custom_resolution_btn.isChecked(): 223 | width, height = map(int, self.before_resolution.text().split('x')) 224 | ratio = float(self.custiom_ratio_spinbox.value()) 225 | self.width_line_edit.setText(str(int(width*ratio))) 226 | self.height_line_edit.setText(str(int(height*ratio))) 227 | 228 | def judge_scaled_resolution_btn(self): 229 | width, height = map(int, self.before_resolution.text().split('x')) 230 | if width/height == 16/9: 231 | self.s1080p_btn.setEnabled(True) 232 | self.s2k_btn.setEnabled(True) 233 | self.s4k_btn.setEnabled(True) 234 | # self.s1080p_btn.setChecked(True) 235 | else: 236 | # 非16:9 237 | self.s1080p_btn.setDisabled(True) 238 | self.s2k_btn.setDisabled(True) 239 | self.s4k_btn.setDisabled(True) 240 | # 默认2倍放大 241 | self.custiom_ratio_btn.setChecked(True) 242 | self.custiom_ratio_spinbox.setValue(1) 243 | self.custiom_ratio_spinbox.setValue(2) 244 | 245 | def set_game_resolution_encoding(self, width, height, encoding): 246 | self.before_resolution.setText(f'{width}x{height}') 247 | self.main_encoding.setText(encoding) 248 | self.judge_scaled_resolution_btn() 249 | 250 | def s1080p_btn_ratio(self): 251 | width, height = map(int, self.before_resolution.text().split('x')) 252 | ratio = 1080/height 253 | self.custiom_ratio_spinbox.setValue(ratio) 254 | 255 | def s2k_btn_ratio(self): 256 | width, height = map(int, self.before_resolution.text().split('x')) 257 | ratio = 1440/height 258 | self.custiom_ratio_spinbox.setValue(ratio) 259 | 260 | def s4k_btn_ratio(self): 261 | width, height = map(int, self.before_resolution.text().split('x')) 262 | ratio = 2160/height 263 | self.custiom_ratio_spinbox.setValue(ratio) 264 | -------------------------------------------------------------------------------- /GUI/flat_widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 自定义控件 4 | from .flat_push_button import FPushButton 5 | from .flat_icon_button import FIconButton 6 | from .flat_line_edit import FLineEdit 7 | from .flat_progress_bar import FProgressBar 8 | from .flat_tab_widget import FTabWidget 9 | -------------------------------------------------------------------------------- /GUI/flat_widgets/flat_circular_progress.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..qt_core import * 4 | 5 | 6 | class FCircularProgress(QFrame): 7 | def __init__( 8 | self, 9 | value=0, 10 | progress_width=15, 11 | is_rounded=True, 12 | max_value=100, 13 | progress_color="#ff79c6", 14 | enable_text=True, 15 | font_family="Segoe UI", 16 | font_size=12, 17 | suffix="%", 18 | text_color="#ff79c6", 19 | enable_bg=True, 20 | bg_color="#44475a" 21 | ): 22 | QFrame.__init__(self) 23 | 24 | # CUSTOM PROPERTIES 25 | self.value = value 26 | self.progress_width = progress_width 27 | self.progress_rounded_cap = is_rounded 28 | self.max_value = max_value 29 | self.progress_color = progress_color 30 | # Text 31 | self.enable_text = enable_text 32 | self.font_family = font_family 33 | self.font_size = font_size 34 | self.suffix = suffix 35 | self.text_color = text_color 36 | # BG 37 | self.enable_bg = enable_bg 38 | self.bg_color = bg_color 39 | 40 | # ADD DROPSHADOW 41 | def add_shadow(self): 42 | self.shadow = QGraphicsDropShadowEffect(self) 43 | self.shadow.setBlurRadius(15) 44 | self.shadow.setXOffset(0) 45 | self.shadow.setYOffset(0) 46 | self.shadow.setColor(QColor(0, 0, 0, 200)) 47 | self.setGraphicsEffect(self.shadow) 48 | 49 | # SET VALUE 50 | def set_value(self, value): 51 | self.value = value 52 | self.repaint() # Render progress bar after change value 53 | 54 | # PAINT EVENT (DESIGN YOUR CIRCULAR PROGRESS HERE) 55 | 56 | def paintEvent(self, e): 57 | # SET PROGRESS PARAMETERS 58 | width = self.width() - self.progress_width - 30 59 | height = self.height() - self.progress_width - 30 60 | margin = self.progress_width/2 + 15 61 | value = self.value * 360/self.max_value 62 | 63 | # PAINTER 64 | paint = QPainter() 65 | paint.begin(self) 66 | paint.setRenderHint(QPainter.Antialiasing) # remove pixelated edges 67 | paint.setFont(QFont(self.font_family, self.font_size)) 68 | 69 | # CREATE RECTANGLE 70 | rect = QRect(0, 0, self.width(), self.height()) 71 | paint.setPen(Qt.NoPen) 72 | 73 | # PEN 74 | pen = QPen() 75 | pen.setWidth(self.progress_width) 76 | # Set Round Cap 77 | if self.progress_rounded_cap: 78 | pen.setCapStyle(Qt.RoundCap) 79 | 80 | # ENABLE BG 81 | if self.enable_bg: 82 | pen.setColor(QColor(self.bg_color)) 83 | paint.setPen(pen) 84 | paint.drawArc(margin, margin, width, height, 0, 360 * 16) 85 | 86 | # CREATE ARC/CIRCULAR PROGRESS 87 | pen.setColor(QColor(self.progress_color)) 88 | paint.setPen(pen) 89 | paint.drawArc(margin, margin, width, height, -90 * 16, -value * 16) 90 | 91 | # CREATE TEXT 92 | if self.enable_text: 93 | pen.setColor(QColor(self.text_color)) 94 | paint.setPen(pen) 95 | paint.drawText(rect, Qt.AlignCenter, f"{self.value}{self.suffix}") 96 | 97 | # END 98 | paint.end() 99 | -------------------------------------------------------------------------------- /GUI/flat_widgets/flat_icon_button.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..qt_core import * 4 | 5 | 6 | class FIconButton(QPushButton): 7 | """扁平化启动按钮""" 8 | 9 | def __init__(self, 10 | text=None, 11 | minimum_width=50, 12 | height=40, 13 | icon_path=None, 14 | icon_color="#c3ccdf", 15 | text_color="#c3ccdf", 16 | btn_color="#44475a", 17 | btn_hover="#4f5368", 18 | btn_pressed="#282a36", 19 | text_align="center", 20 | border_radius=15, 21 | ): 22 | QPushButton.__init__(self) 23 | if text is not None: 24 | self.setText(text) 25 | self.setCursor(Qt.PointingHandCursor) 26 | # 最小宽度 27 | self.minimum_width = minimum_width 28 | # 高度 29 | self.height = height 30 | # 图标路径 31 | self.icon_path = icon_path 32 | # 图标颜色 33 | self.icon_color = icon_color 34 | # 字体颜色 35 | self.text_color = text_color 36 | # 背景颜色 37 | self.btn_color = btn_color 38 | # 鼠标掠过颜色 39 | self.btn_hover = btn_hover 40 | # 鼠标按下颜色 41 | self.btn_pressed = btn_pressed 42 | # 字体对齐方式 43 | self.text_align = text_align 44 | # 边界圆角半径 45 | self.border_radius = border_radius 46 | # 设置样式 47 | self.set_style() 48 | # 设置图标 49 | if self.icon_path is not None: 50 | self.set_icon(self.icon_path) 51 | 52 | def set_style(self): 53 | style = f""" 54 | QPushButton {{ 55 | color: {self.text_color}; 56 | background-color: {self.btn_color}; 57 | text-align: {self.text_align}; 58 | border: none; 59 | border-radius: {self.border_radius}px; 60 | font: 'Segoe UI'; 61 | margin-right: 5; 62 | }} 63 | QPushButton:hover {{ 64 | background-color: {self.btn_hover}; 65 | }} 66 | QPushButton:pressed {{ 67 | background-color: {self.btn_pressed}; 68 | }} 69 | """ 70 | self.setStyleSheet(style) 71 | self.setMinimumWidth(self.minimum_width) 72 | self.setMaximumHeight(self.height) 73 | self.setMinimumHeight(self.height) 74 | 75 | def set_icon(self, icon_path): 76 | self.icon_pix = QPixmap(icon_path) 77 | # self.icon_pix.scaledToHeight(self.height) 78 | self.setIcon(self.icon_pix) 79 | -------------------------------------------------------------------------------- /GUI/flat_widgets/flat_line_edit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..qt_core import * 4 | 5 | 6 | class FLineEdit(QLineEdit): 7 | """扁平化输入框""" 8 | 9 | def __init__(self, 10 | text="", 11 | place_holder_text="", 12 | text_padding=0, 13 | height=None, 14 | radius=0, 15 | border_size=2, 16 | color="#FFF", 17 | selection_color="#FFF", 18 | bg_color="#333", 19 | bg_color_active="#222", 20 | context_color="#00ABE8" 21 | ): 22 | QLineEdit.__init__(self) 23 | self.setAcceptDrops(True) 24 | # 参数设置 25 | if text: 26 | self.setText(text) 27 | if place_holder_text: 28 | self.setPlaceholderText(place_holder_text) 29 | if height: 30 | self.setMinimumHeight(height) 31 | self.setMaximumHeight(height) 32 | self.text_padding = text_padding 33 | self.radius = radius 34 | self.border_size = border_size 35 | self.color = color 36 | self.selection_color = selection_color 37 | self.bg_color = bg_color 38 | self.bg_color_active = bg_color_active 39 | self.context_color = context_color 40 | self.set_style() 41 | 42 | def set_style(self): 43 | style = f''' 44 | QLineEdit {{ 45 | background-color: {self.bg_color}; 46 | border-radius: {self.radius}px; 47 | border: {self.border_size}px solid transparent; 48 | padding-left: {self.text_padding}px; 49 | padding-right: {self.text_padding}px; 50 | selection-color: {self.selection_color}; 51 | selection-background-color: {self.context_color}; 52 | color: {self.color}; 53 | }} 54 | QLineEdit:focus {{ 55 | border: {self.border_size}px solid {self.context_color}; 56 | background-color: {self.bg_color_active}; 57 | }} 58 | ''' 59 | self.setStyleSheet(style) 60 | 61 | def dragEnterEvent(self, event): 62 | event.accept() 63 | 64 | def dragMoveEvent(self, event): 65 | event.accept() 66 | 67 | def dropEvent(self, event): 68 | # 拖拽选择文件 69 | file_path = Path(event.mimeData().text().replace('file:///', '').strip()) 70 | # if file_path.is_dir(): 71 | self.setText(str(file_path)) 72 | event.accept() 73 | -------------------------------------------------------------------------------- /GUI/flat_widgets/flat_progress_bar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..qt_core import * 4 | 5 | 6 | class FProgressBar(QProgressBar): 7 | """扁平化进度条""" 8 | 9 | def __init__(self, 10 | width=None, 11 | height=None, 12 | border_radius=10): 13 | QProgressBar.__init__(self) 14 | 15 | self.width = width 16 | self.height = height 17 | self.border_radius = border_radius 18 | # 如果指定了宽高,则固定宽高 19 | if self.height: 20 | self.setMaximumHeight(self.height) 21 | self.setMaximumHeight(self.height) 22 | if self.width: 23 | self.setMaximumWidth(self.width) 24 | self.setMaximumWidth(self.width) 25 | 26 | self.change_style() 27 | 28 | def change_style(self): 29 | self.setStyleSheet(f""" 30 | QProgressBar {{ 31 | background-color: rgb(98, 114, 164); 32 | color: rgb(200, 200, 200); 33 | border-style: none; 34 | border-radius: {self.border_radius}px; 35 | text-align: center; 36 | }} 37 | QProgressBar::chunk {{ 38 | border-radius: {self.border_radius}px; 39 | background-color: qlineargradient(spread:pad, x1:0, y1:0.511364, x2:1, y2:0.523, stop:0 rgba(254, 121, 199, 255), stop:1 rgba(170, 85, 255, 255)); 40 | }} 41 | """) 42 | -------------------------------------------------------------------------------- /GUI/flat_widgets/flat_push_button.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..qt_core import * 4 | 5 | 6 | class FPushButton(QPushButton): 7 | """扁平化按钮""" 8 | 9 | def __init__(self, 10 | text='', 11 | height=40, 12 | minimum_width=50, 13 | icon_path=None, 14 | icon_color="#c3ccdf", 15 | text_color="#c3ccdf", 16 | btn_color="#44475a", 17 | btn_hover="#4f5368", 18 | btn_pressed="#282a36", 19 | text_padding=0, 20 | text_align="left", 21 | border_width=5, 22 | border_direction="border-right", 23 | border_radius=15, 24 | is_active=False 25 | ): 26 | QPushButton.__init__(self) 27 | self.setText(text) 28 | self.setMaximumHeight(height) 29 | self.setMinimumHeight(height) 30 | self.setMinimumWidth(minimum_width) 31 | self.setCursor(Qt.PointingHandCursor) 32 | # 最小宽度 33 | self.minimum_width = minimum_width 34 | # 图标路径 35 | self.icon_path = icon_path 36 | # 图标颜色 37 | self.icon_color = icon_color 38 | # 字体颜色 39 | self.text_color = text_color 40 | # 背景颜色 41 | self.btn_color = btn_color 42 | # 鼠标掠过颜色 43 | self.btn_hover = btn_hover 44 | # 鼠标按下颜色 45 | self.btn_pressed = btn_pressed 46 | # 左边距 47 | self.text_padding = text_padding 48 | # 字体对齐方式 49 | self.text_align = text_align 50 | # 激活边界标志宽度 51 | self.border_width = border_width 52 | # 激活边界标志方向 53 | self.border_direction = border_direction 54 | # 边界圆角半径 55 | self.border_radius = border_radius 56 | # 是否激活 57 | self.is_active = is_active 58 | self.set_style() 59 | 60 | def set_active(self, is_active): 61 | self.is_active = is_active 62 | self.set_style() 63 | 64 | def set_style(self): 65 | normal_style = f""" 66 | QPushButton {{ 67 | color: {self.text_color}; 68 | background-color: {self.btn_color}; 69 | padding-left: {self.text_padding}px; 70 | text-align: {self.text_align}; 71 | border: none; 72 | border-radius: {self.border_radius}px; 73 | font: 'Segoe UI'; 74 | }} 75 | QPushButton:hover {{ 76 | background-color: {self.btn_hover}; 77 | }} 78 | QPushButton:pressed {{ 79 | background-color: {self.btn_pressed}; 80 | }} 81 | """ 82 | active_style = f""" 83 | QPushButton {{ 84 | background-color: {self.btn_hover}; 85 | {self.border_direction}: {self.border_width}px solid {self.btn_pressed}; 86 | }} 87 | """ 88 | if self.is_active: 89 | style = normal_style + active_style 90 | else: 91 | style = normal_style 92 | self.setStyleSheet(style) 93 | 94 | def paintEvent(self, event): 95 | QPushButton.paintEvent(self, event) 96 | qp = QPainter() 97 | qp.begin(self) 98 | qp.setRenderHint(QPainter.Antialiasing) 99 | qp.setPen(Qt.NoPen) 100 | rect = QRect(0, 0, self.minimum_width, self.height()) 101 | if self.icon_path: 102 | self.draw_icon(qp, rect) 103 | qp.end() 104 | 105 | def draw_icon(self, qp, rect): 106 | # 绘制图标 107 | icon = QPixmap(self.icon_path) 108 | painter = QPainter(icon) 109 | painter.setCompositionMode(QPainter.CompositionMode_SourceIn) 110 | painter.fillRect(icon.rect(), self.icon_color) 111 | qp.drawPixmap( 112 | (rect.width() - icon.width())/2, 113 | (rect.height() - icon.height())/2, 114 | icon) 115 | painter.end() 116 | 117 | def show_shadow(self): 118 | # 显示阴影 119 | shadow = QGraphicsDropShadowEffect(self) 120 | shadow.setBlurRadius(self.border_radius) 121 | shadow.setXOffset(0) 122 | shadow.setYOffset(0) 123 | shadow.setColor(QColor(0, 0, 0, 200)) 124 | self.setGraphicsEffect(shadow) 125 | -------------------------------------------------------------------------------- /GUI/flat_widgets/flat_tab_widget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..qt_core import * 4 | 5 | 6 | class FTabWidget(QTabWidget): 7 | """扁平化标签页""" 8 | 9 | def __init__(self, 10 | width=105, 11 | height=35, 12 | margin_left=15, 13 | text_padding=10, 14 | hide_edge=True, 15 | position='top', 16 | shape='triangle', 17 | selection_color='#6272a4'): 18 | QTabWidget.__init__(self) 19 | 20 | # 标签宽 21 | self.width = width 22 | # 标签高 23 | self.height = height 24 | # 标签左余白 25 | self.margin_left = margin_left 26 | # 文字边距 27 | self.text_padding = text_padding 28 | # 是否隐藏边框 29 | self.hide_edge = hide_edge 30 | # 标签位置 31 | self.position = position 32 | # 标签形状 33 | self.shape = shape 34 | # 选中背景颜色 35 | self.selection_color = selection_color 36 | 37 | self.set_style() 38 | 39 | def set_style(self): 40 | un_selected_height = int(self.height/1.2) 41 | height_diff = self.height - un_selected_height 42 | style = f''' 43 | QTabBar::tab {{ 44 | width: {self.width}px; 45 | height: {self.height}px; 46 | margin-left: {self.margin_left}px; 47 | padding-left: {self.text_padding}px; 48 | padding-right: {self.text_padding}px; 49 | }} 50 | QTabBar::tab:selected {{ 51 | background: {self.selection_color}; 52 | }} 53 | QTabBar::tab:!selected {{ 54 | margin-{self.position}: {height_diff}px; 55 | height: {un_selected_height}px; 56 | }} 57 | ''' 58 | # 应用样式 59 | self.setStyleSheet(style) 60 | # 去除边框 61 | if self.hide_edge: 62 | self.setDocumentMode(True) 63 | # 标签显示位置 64 | match self.position: 65 | case 'top': 66 | self.setTabPosition(QTabWidget.North) 67 | case 'bottom': 68 | self.setTabPosition(QTabWidget.South) 69 | case 'left': 70 | self.setTabPosition(QTabWidget.West) 71 | case 'right': 72 | self.setTabPosition(QTabWidget.East) 73 | # 改变标签形状 74 | match self.shape: 75 | case 'triangle': 76 | self.setTabShape(QTabWidget.Triangular) 77 | case 'round': 78 | self.setTabShape(QTabWidget.Rounded) 79 | 80 | -------------------------------------------------------------------------------- /GUI/game_page.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | from .kirikiri_part import KirikiriPart 6 | from .artemis_part import ArtemisPart 7 | from .majiro_part import MajiroPart 8 | 9 | 10 | class GamePage(QFrame): 11 | def __init__(self): 12 | QFrame.__init__(self) 13 | self.icon_folder = Path(sys.argv[0]).parent/'Icons' 14 | self.initUI() 15 | self.switch_kirikiri() 16 | 17 | def initUI(self): 18 | self.setup_layouts() 19 | self.setup_connections() 20 | 21 | def setup_layouts(self): 22 | layout = QVBoxLayout(self) 23 | layout.setContentsMargins(10, 0, 10, 0) 24 | # layout.setSpacing(0) 25 | 26 | self.setup_top_bar() 27 | layout.addWidget(self.top_bar) 28 | 29 | self.setup_input_folder() 30 | layout.addWidget(self.input_folder_frame) 31 | 32 | self.setup_output_folder() 33 | layout.addWidget(self.output_folder_frame) 34 | 35 | self.setup_game_engine_area() 36 | layout.addWidget(self.game_engine_area) 37 | 38 | self.setup_info_area() 39 | layout.addWidget(self.info_area_frame) 40 | 41 | self.setup_run_part() 42 | layout.addWidget(self.run_part_frame) 43 | 44 | def setup_connections(self): 45 | self.kirikiri_btn.clicked.connect(self.switch_kirikiri) 46 | self.artemis_btn.clicked.connect(self.switch_artemis) 47 | self.majiro_btn.clicked.connect(self.switch_majiro) 48 | self.select_input_folder_btn.clicked.connect(self.choose_input_folder) 49 | self.select_output_folder_btn.clicked.connect(self.choose_output_folder) 50 | self.select_input_folder_line_edit.textChanged.connect(self.auto_fill_output_folder) 51 | 52 | def setup_top_bar(self): 53 | self.top_bar = QFrame() 54 | self.top_bar.setMaximumHeight(35) 55 | self.top_bar.setMinimumHeight(35) 56 | top_bar_layout = QHBoxLayout(self.top_bar) 57 | top_bar_layout.setContentsMargins(10, 0, 10, 0) 58 | top_bar_layout.setSpacing(20) 59 | self.kirikiri_btn = FPushButton(text='KiriKiri 2/Z', height=self.top_bar.height(), btn_pressed='yellow', text_padding=0, text_align='center', border_direction='border', border_radius=15, border_width=3) 60 | self.artemis_btn = FPushButton(text='Artemis', height=self.top_bar.height(), btn_pressed='yellow', text_padding=0, text_align='center', border_direction='border', border_radius=15, border_width=3) 61 | self.majiro_btn = FPushButton(text='Majiro', height=self.top_bar.height(), btn_pressed='yellow', text_padding=0, text_align='center', border_direction='border', border_radius=15, border_width=3) 62 | top_bar_layout.addWidget(self.kirikiri_btn) 63 | top_bar_layout.addWidget(self.artemis_btn) 64 | top_bar_layout.addWidget(self.majiro_btn) 65 | 66 | def setup_input_folder(self): 67 | self.input_folder_frame = QFrame() 68 | hlayout = QHBoxLayout(self.input_folder_frame) 69 | hlayout.setContentsMargins(10, 20, 10, 0) 70 | self.select_input_folder_lb = QLabel('输入路径') 71 | self.select_input_folder_lb.setStyleSheet("font: 700 12pt 'Segoe UI'") 72 | hlayout.addWidget(self.select_input_folder_lb) 73 | self.select_input_folder_line_edit = FLineEdit(place_holder_text='选择或拖拽需要处理的文件夹', height=30, radius=12, text_padding=10) 74 | hlayout.addWidget(self.select_input_folder_line_edit) 75 | self.select_input_folder_btn = FPushButton(height=30, icon_path=self.icon_folder/'icon_folder_open.svg') 76 | hlayout.addWidget(self.select_input_folder_btn) 77 | 78 | def choose_input_folder(self): 79 | path_text = QFileDialog.getExistingDirectory() 80 | if path_text: 81 | # 转换为操作系统支持的路径格式 82 | format_path_text = Path(path_text).to_str 83 | self.select_input_folder_line_edit.setText(format_path_text) 84 | 85 | def setup_output_folder(self): 86 | self.output_folder_frame = QFrame() 87 | hlayout = QHBoxLayout(self.output_folder_frame) 88 | hlayout.setContentsMargins(10, 10, 10, 20) 89 | self.select_output_folder_lb = QLabel('输出目录') 90 | self.select_output_folder_lb.setStyleSheet("font: 700 12pt 'Segoe UI'") 91 | hlayout.addWidget(self.select_output_folder_lb) 92 | self.select_output_folder_line_edit = FLineEdit(height=30, radius=12) 93 | self.select_output_folder_line_edit = FLineEdit(place_holder_text='指定输出文件夹', height=30, radius=12, text_padding=10) 94 | hlayout.addWidget(self.select_output_folder_line_edit) 95 | self.select_output_folder_btn = FPushButton(height=30, icon_path=self.icon_folder/'icon_folder.svg') 96 | hlayout.addWidget(self.select_output_folder_btn) 97 | 98 | def auto_fill_output_folder(self): 99 | output_folder_path = Path(self.select_input_folder_line_edit.text().strip()).parent/'VNU_OUTPUT' 100 | # while output_folder_path.exists(): 101 | # output_folder_path = output_folder_path.with_name(output_folder_path.name+'_Output') 102 | self.select_output_folder_line_edit.setText(str(output_folder_path)) 103 | 104 | def choose_output_folder(self): 105 | path_text = QFileDialog.getExistingDirectory() 106 | if path_text: 107 | # 转换为操作系统支持的路径格式 108 | format_path_text = Path(path_text).to_str 109 | self.select_output_folder_line_edit.setText(format_path_text) 110 | 111 | def setup_game_engine_area(self): 112 | self.game_engine_area = QStackedWidget() 113 | # self.game_engine_area.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 114 | 115 | self.kirikiri = KirikiriPart() 116 | self.kirikiri.setObjectName('Kirikiri') 117 | self.game_engine_area.addWidget(self.kirikiri) 118 | 119 | self.game_engine_area.setMaximumHeight(245) 120 | 121 | self.artemis = ArtemisPart() 122 | self.artemis.setObjectName('Artemis') 123 | self.game_engine_area.addWidget(self.artemis) 124 | 125 | self.majiro = MajiroPart() 126 | self.majiro.setObjectName('Majiro') 127 | self.game_engine_area.addWidget(self.majiro) 128 | 129 | def reset_engine_selection(self): 130 | for btn in self.top_bar.findChildren(QPushButton): 131 | try: 132 | btn.set_active(False) 133 | except: 134 | pass 135 | 136 | def setup_info_area(self): 137 | self.info_area_frame = QFrame() 138 | # self.info_area_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 139 | layout = QVBoxLayout(self.info_area_frame) 140 | layout.setContentsMargins(10, 0, 10, 0) 141 | self.info_text_edit = QTextEdit() 142 | # self.info_text_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 143 | self.info_text_edit.setReadOnly(True) 144 | self.info_text_edit.setStyleSheet('background-color:#333') 145 | layout.addWidget(self.info_text_edit) 146 | 147 | def setup_run_part(self): 148 | self.run_part_frame = QFrame() 149 | layout = QHBoxLayout(self.run_part_frame) 150 | layout.setContentsMargins(10, 0, 10, 20) 151 | 152 | self.status_progress_bar = FProgressBar(height=30, border_radius=12.5) 153 | layout.addWidget(self.status_progress_bar) 154 | 155 | self.run_btn = FIconButton(text="开始处理", minimum_width=150, height=30, icon_path=self.icon_folder/'icon_send.svg') 156 | layout.addWidget(self.run_btn) 157 | 158 | def set_running_state(self, state): 159 | if state == 0: 160 | self.run_btn.setEnabled(True) 161 | self.run_btn.setText('开始处理') 162 | self.run_btn.set_icon(self.icon_folder/'icon_send.svg') 163 | self.status_progress_bar.setRange(0, 100) 164 | self.status_progress_bar.setValue(0) 165 | elif state == 1: 166 | self.run_btn.setDisabled(True) 167 | self.run_btn.setText('正在处理') 168 | self.run_btn.set_icon(self.icon_folder/'clock.svg') 169 | self.status_progress_bar.setRange(0, 0) 170 | elif state == 2: 171 | self.run_btn.setDisabled(True) 172 | self.run_btn.setText('正在处理') 173 | self.run_btn.set_icon(self.icon_folder/'clock.svg') 174 | self.status_progress_bar.setRange(0, 100) 175 | elif state == 3: 176 | self.run_btn.setEnabled(True) 177 | self.run_btn.setText('开始处理') 178 | self.run_btn.set_icon(self.icon_folder/'icon_send.svg') 179 | self.status_progress_bar.setRange(0, 100) 180 | self.status_progress_bar.setValue(100) 181 | 182 | def switch_kirikiri(self): 183 | if not self.kirikiri_btn.is_active: 184 | self.reset_engine_selection() 185 | self.kirikiri_btn.set_active(True) 186 | self.game_engine_area.setCurrentWidget(self.kirikiri) 187 | 188 | def switch_artemis(self): 189 | if not self.artemis_btn.is_active: 190 | self.reset_engine_selection() 191 | self.artemis_btn.set_active(True) 192 | self.game_engine_area.setCurrentWidget(self.artemis) 193 | 194 | def switch_majiro(self): 195 | if not self.majiro_btn.is_active: 196 | self.reset_engine_selection() 197 | self.majiro_btn.set_active(True) 198 | self.game_engine_area.setCurrentWidget(self.majiro) 199 | -------------------------------------------------------------------------------- /GUI/image_page.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | 6 | 7 | class ImagePage(QFrame): 8 | 9 | def __init__(self): 10 | QFrame.__init__(self) 11 | self.icon_folder = Path(sys.argv[0]).parent/'Icons' 12 | self.initUI() 13 | # self.show_image(self.icon_folder/'sample.png') 14 | 15 | def initUI(self): 16 | self.setup_layouts() 17 | self.setup_connections() 18 | # add_shadow(self.list_widget) 19 | # add_shadow(self.image_show_label) 20 | # add_shadow(self.setting_frame) 21 | 22 | def setup_connections(self): 23 | self.input_line_edit.editingFinished.connect(self.get_image_list) 24 | self.input_line_edit.editingFinished.connect(self.auto_fill_output_folder) 25 | self.filter_line_edit.editingFinished.connect(self.get_image_list) 26 | self.list_widget.currentItemChanged.connect(self.switch_show_image) 27 | self.input_btn.clicked.connect(self.choose_input_folder) 28 | self.output_btn.clicked.connect(self.choose_output_folder) 29 | self.ignr_btn.toggled.connect(self.get_image_list) 30 | 31 | def setup_layouts(self): 32 | self.layout = QVBoxLayout(self) 33 | self.layout.setSpacing(10) 34 | 35 | self.setup_show_image_area() 36 | 37 | self.hlayout = QHBoxLayout() 38 | self.layout.addLayout(self.hlayout) 39 | 40 | self.setup_image_list_view() 41 | 42 | self.setup_settings() 43 | 44 | self.setup_in_out_folders() 45 | 46 | self.setup_run_part() 47 | 48 | def setup_settings(self): 49 | self.setting_frame = QFrame() 50 | self.setting_frame.setMaximumHeight(145) 51 | self.setting_frame.setMinimumHeight(145) 52 | self.setting_frame.setStyleSheet('background-color:#456') 53 | self.hlayout.addWidget(self.setting_frame) 54 | self.set_formlayout = QFormLayout(self.setting_frame) 55 | 56 | self.custiom_ratio_lb = QLabel('图片放大倍率:') 57 | self.custiom_ratio_spinbox = QDoubleSpinBox() 58 | self.custiom_ratio_spinbox.setDecimals(3) 59 | self.custiom_ratio_spinbox.setSingleStep(0.5) 60 | self.custiom_ratio_spinbox.setValue(2) 61 | self.set_formlayout.addRow(self.custiom_ratio_lb, self.custiom_ratio_spinbox) 62 | 63 | self.output_extention_lab = QLabel('输出图片格式:') 64 | self.output_extention_line_edit = FLineEdit() 65 | self.output_extention_line_edit.setText('png') 66 | self.set_formlayout.addRow(self.output_extention_lab, self.output_extention_line_edit) 67 | 68 | self.filter_lab = QLabel('图片格式筛选:') 69 | self.filter_line_edit = FLineEdit(place_holder_text='使用英文逗号分隔图片格式') 70 | self.filter_line_edit.setText('png,jpg,jpeg,bmp,webp,tif,tiff') 71 | self.set_formlayout.addRow(self.filter_lab, self.filter_line_edit) 72 | 73 | self.ignr_lb = QLabel('处理子文件夹:') 74 | self.ignr_btn = QCheckBox() 75 | self.ignr_btn.setChecked(True) 76 | self.set_formlayout.addRow(self.ignr_lb, self.ignr_btn) 77 | 78 | self.suffix_lab = QLabel('输出图片后缀:') 79 | self.suffix_line_edit = FLineEdit() 80 | self.set_formlayout.addRow(self.suffix_lab, self.suffix_line_edit) 81 | 82 | def setup_image_list_view(self): 83 | self.list_widget = QListWidget() 84 | self.list_widget.setMaximumHeight(145) 85 | self.list_widget.setMinimumHeight(145) 86 | self.list_widget.setStyleSheet('background-color:#456') 87 | self.hlayout.addWidget(self.list_widget) 88 | 89 | def setup_show_image_area(self): 90 | self.image_show_label = QLabel() 91 | self.image_show_label.setStyleSheet('background-color:#456') 92 | # self.image_show_label.setMinimumWidth(540) 93 | self.image_show_label.setMinimumHeight(360) 94 | self.image_show_label.setAlignment(Qt.AlignCenter) 95 | self.layout.addWidget(self.image_show_label) 96 | 97 | def get_image_list(self): 98 | self.list_widget.clear() 99 | input_path = Path(self.input_line_edit.text().strip()) 100 | if input_path.exists(): 101 | if input_path.is_dir(): 102 | extension_list = [('.' + extension.strip().lower()) for extension in self.filter_line_edit.text().split(',')] 103 | walk_mode = True if self.ignr_btn.isChecked() else False 104 | image_list = [file_path for file_path in input_path.file_list(walk_mode=walk_mode) if file_path.suffix.lower() in extension_list] 105 | for image_file in image_list: 106 | self.list_widget.addItem(image_file.to_str) 107 | elif input_path.is_file(): 108 | self.list_widget.addItem(input_path.to_str) 109 | self.list_widget.setCurrentRow(0) 110 | 111 | def switch_show_image(self): 112 | try: 113 | if self.list_widget.currentItem() is not None: 114 | image_path = Path(self.list_widget.currentItem().text().strip()) 115 | self.show_image(image_path) 116 | except: 117 | warn_msg = QMessageBox() 118 | reply = warn_msg.warning(self.ui, '提示', '无法读取该图片!', QMessageBox.Yes) 119 | 120 | def show_image(self, image_path): 121 | image_show_pix = QPixmap(image_path).scaledToHeight(self.image_show_label.height(), Qt.SmoothTransformation) 122 | self.image_show_label.setPixmap(image_show_pix) 123 | 124 | def setup_in_out_folders(self): 125 | layout1 = QHBoxLayout() 126 | self.layout.addLayout(layout1) 127 | 128 | self.input_btn = FIconButton('选择输入路径', minimum_width=100, height=30) 129 | self.input_line_edit = FLineEdit(place_holder_text='将图片文件或文件夹拖拽至此处', height=30, radius=12, text_padding=10) 130 | layout1.addWidget(self.input_line_edit) 131 | layout1.addWidget(self.input_btn) 132 | self.input_line_edit.dropEvent = self.drop_get_list 133 | 134 | layout2 = QHBoxLayout() 135 | self.layout.addLayout(layout2) 136 | self.output_btn = FIconButton('选择输出目录', minimum_width=100, height=30) 137 | self.output_line_edit = FLineEdit(place_holder_text='输出图片至此处', height=30, radius=12, text_padding=10) 138 | layout2.addWidget(self.output_line_edit) 139 | layout2.addWidget(self.output_btn) 140 | 141 | def choose_input_folder(self): 142 | path_text = QFileDialog.getExistingDirectory() 143 | if path_text: 144 | # 转换为操作系统支持的路径格式 145 | format_path_text = Path(path_text).to_str 146 | self.input_line_edit.setText(format_path_text) 147 | self.get_image_list() 148 | self.auto_fill_output_folder() 149 | 150 | def choose_output_folder(self): 151 | path_text = QFileDialog.getExistingDirectory() 152 | if path_text: 153 | # 转换为操作系统支持的路径格式 154 | format_path_text = Path(path_text).to_str 155 | self.output_line_edit.setText(format_path_text) 156 | 157 | def drop_get_list(self, event): 158 | file_path = Path(event.mimeData().text().replace('file:///', '').strip()) 159 | self.input_line_edit.setText(str(file_path)) 160 | self.get_image_list() 161 | self.auto_fill_output_folder() 162 | event.accept() 163 | 164 | def auto_fill_output_folder(self): 165 | output_folder_path = Path(self.input_line_edit.text().strip()).parent/'VNU_OUTPUT' 166 | self.output_line_edit.setText(str(output_folder_path)) 167 | 168 | def setup_run_part(self): 169 | run_part_layout = QHBoxLayout() 170 | self.layout.addLayout(run_part_layout) 171 | 172 | self.status_progress_bar = FProgressBar(height=30, border_radius=12.5) 173 | run_part_layout.addWidget(self.status_progress_bar) 174 | 175 | self.run_btn = FIconButton(text="开始处理", minimum_width=150, height=30, icon_path=self.icon_folder/'icon_send.svg') 176 | run_part_layout.addWidget(self.run_btn) 177 | 178 | def set_running_state(self, state): 179 | if state == 0: 180 | self.run_btn.setEnabled(True) 181 | self.run_btn.setText('开始处理') 182 | self.run_btn.set_icon(self.icon_folder/'icon_send.svg') 183 | self.status_progress_bar.setRange(0, 100) 184 | self.status_progress_bar.setValue(0) 185 | elif state == 1: 186 | self.run_btn.setDisabled(True) 187 | self.run_btn.setText('正在统计') 188 | self.run_btn.set_icon(self.icon_folder/'clock.svg') 189 | self.status_progress_bar.setValue(0) 190 | elif state == 2: 191 | self.run_btn.setEnabled(True) 192 | self.run_btn.setText('开始处理') 193 | self.run_btn.set_icon(self.icon_folder/'icon_send.svg') 194 | self.status_progress_bar.setValue(100) 195 | -------------------------------------------------------------------------------- /GUI/info_page.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | 6 | 7 | class InfoPage(QFrame): 8 | 9 | def __init__(self): 10 | 11 | QFrame.__init__(self) 12 | self.setup_layouts() 13 | self.setup_connections() 14 | 15 | def setup_connections(self): 16 | self.check_update_btn.clicked.connect(self.open_releases_page) 17 | self.submit_issue_btn.clicked.connect(self.open_issues_page) 18 | 19 | def setup_layouts(self): 20 | self.layout = QVBoxLayout(self) 21 | self.layout.setContentsMargins(20, 20, 20, 20) 22 | self.tab_view = FTabWidget(height=80, position='bottom') 23 | self.layout.addWidget(self.tab_view) 24 | self.setup_help_info() 25 | self.setup_licenses() 26 | self.setup_update_bug() 27 | 28 | def setup_help_info(self): 29 | self.help_info_frame = QFrame() 30 | self.tab_view.addTab(self.help_info_frame, '教程&&帮助') 31 | 32 | def setup_licenses(self): 33 | license_msg = QTextEdit() 34 | license_msg.setReadOnly(True) 35 | self.tab_view.addTab(license_msg, 'License') 36 | 37 | with open(Path(sys.argv[0]).parent/'LICENSE', 'r', newline='', encoding='utf-8') as f: 38 | license_text = f.read() 39 | license_msg.setPlainText(license_text) 40 | # license_msg.append(license_text) 41 | # license_msg.moveCursor(QTextCursor.Start) 42 | 43 | def setup_update_bug(self): 44 | self.update_bug_frame = QFrame() 45 | self.tab_view.addTab(self.update_bug_frame, '更新&&反馈') 46 | 47 | layout = QVBoxLayout(self.update_bug_frame) 48 | self.check_update_btn = FIconButton('检查更新') 49 | layout.addWidget(self.check_update_btn) 50 | self.submit_issue_btn = FIconButton('提交反馈') 51 | layout.addWidget(self.submit_issue_btn) 52 | 53 | def open_releases_page(self): 54 | QDesktopServices.openUrl(QUrl('https://github.com/hokejyo/VisualNovelUpscaler/releases')) 55 | 56 | def open_issues_page(self): 57 | QDesktopServices.openUrl(QUrl('https://github.com/hokejyo/VisualNovelUpscaler/issues')) 58 | -------------------------------------------------------------------------------- /GUI/left_menu.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | 6 | 7 | class LeftMenu(QFrame): 8 | def __init__(self): 9 | QFrame.__init__(self) 10 | self.icon_folder = Path(sys.argv[0]).parent/'Icons' 11 | # self.setStyleSheet("background-color: #44475a; border-radius: 15px;") 12 | self.setStyleSheet("background-color: #44475a") 13 | self.setup_layouts() 14 | self.setup_connections() 15 | 16 | def setup_connections(self): 17 | self.menu_button.clicked.connect(self.fold_menu) 18 | # self.page1_button.clicked.connect(lambda: self.fold_menu() if self.page1_button.is_active else None) 19 | 20 | def setup_layouts(self): 21 | # 设置最大、最小宽度 22 | self.setMaximumWidth(50) 23 | self.setMinimumWidth(50) 24 | # 左侧菜单布局 25 | self.left_menu_layout = QVBoxLayout(self) 26 | self.left_menu_layout.setContentsMargins(0, 0, 0, 0) 27 | self.left_menu_layout.setSpacing(0) 28 | # 顶部区域 29 | self.setup_left_menu_top() 30 | # 中部空间 31 | self.left_menu_spacer = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding) 32 | # 底部区域 33 | self.setup_left_menu_bottom() 34 | # 底部文字 35 | self.setup_left_menu_label() 36 | # 添加组件进布局 37 | self.left_menu_layout.addWidget(self.left_menu_top_frame) 38 | self.left_menu_layout.addItem(self.left_menu_spacer) 39 | self.left_menu_layout.addWidget(self.left_menu_bottom_frame) 40 | self.left_menu_layout.addWidget(self.version_label) 41 | 42 | def setup_left_menu_top(self): 43 | # 左侧菜单顶部区域 44 | self.left_menu_top_frame = QFrame() 45 | self.left_menu_top_frame.setMinimumHeight(40) 46 | self.left_menu_top_layout = QVBoxLayout(self.left_menu_top_frame) 47 | self.left_menu_top_layout.setContentsMargins(0, 0, 0, 0) 48 | self.left_menu_top_layout.setSpacing(0) 49 | # 按钮 50 | self.menu_button = FPushButton(text="折叠菜单", text_padding=60, icon_path=self.icon_folder/'columns.svg') 51 | self.image_button = FPushButton(text="图像增强", text_padding=60, icon_path=self.icon_folder/'slack.svg') 52 | self.game_button = FPushButton(text="视觉小说", text_padding=60, icon_path=self.icon_folder/'book-open.svg') 53 | self.text_button = FPushButton(text="文本处理", text_padding=60, icon_path=self.icon_folder/'edit.svg') 54 | # self.page3_button = FPushButton(text="批量处理", text_padding=60, icon_path=self.icon_folder/'trello.svg') 55 | self.left_menu_top_layout.addWidget(self.menu_button) 56 | self.left_menu_top_layout.addWidget(self.image_button) 57 | self.left_menu_top_layout.addWidget(self.game_button) 58 | self.left_menu_top_layout.addWidget(self.text_button) 59 | # self.left_menu_top_layout.addWidget(self.page3_button) 60 | 61 | def setup_left_menu_bottom(self): 62 | # 菜单底部布局 63 | self.left_menu_bottom_frame = QFrame() 64 | self.left_menu_bottom_frame.setMinimumHeight(40) 65 | self.left_menu_bottom_layout = QVBoxLayout(self.left_menu_bottom_frame) 66 | self.left_menu_bottom_layout.setContentsMargins(0, 0, 0, 0) 67 | self.left_menu_bottom_layout.setSpacing(0) 68 | self.info_btn = FPushButton(text='关于', text_padding=60, icon_path=self.icon_folder/'info.svg') 69 | self.setting_btn = FPushButton(text='设置', text_padding=60, icon_path=self.icon_folder/'settings.svg') 70 | self.left_menu_bottom_layout.addWidget(self.info_btn) 71 | self.left_menu_bottom_layout.addWidget(self.setting_btn) 72 | 73 | def setup_left_menu_label(self): 74 | # 版本号 75 | self.version_label = QLabel('version') 76 | # 设置对齐和字体高度、颜色 77 | self.version_label.setAlignment(Qt.AlignCenter) 78 | self.version_label.setMinimumHeight(30) 79 | self.version_label.setMaximumHeight(30) 80 | self.version_label.setStyleSheet("color: #c3ccdf") 81 | 82 | def fold_menu(self): 83 | # 折叠菜单 84 | menu_width = self.width() 85 | width = 50 86 | if menu_width == 50: 87 | width = 240 88 | self.animation = QPropertyAnimation(self, b"minimumWidth") 89 | self.animation.setStartValue(menu_width) 90 | self.animation.setEndValue(width) 91 | self.animation.setDuration(200) 92 | self.animation.start() 93 | 94 | def reset_selection(self): 95 | # 重置为非活动状态 96 | for btn in self.findChildren(QPushButton): 97 | try: 98 | btn.set_active(False) 99 | except: 100 | pass 101 | -------------------------------------------------------------------------------- /GUI/main_content.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | 6 | 7 | class MainContent(QFrame): 8 | def __init__(self): 9 | QFrame.__init__(self) 10 | self.setObjectName('maincontent') 11 | self.icon_folder = Path(sys.argv[0]).parent/'Icons' 12 | self.log_file = Path(sys.argv[0]).parent/'logs.txt' 13 | self.setStyleSheet("background-color: #282a36;color: #6272a4;") 14 | # self.setStyleSheet("background-color: #282a36;color: #6272a4;QFrame {border-radius: 15px};") 15 | # self.setStyleSheet("QFrame {border-radius: 15px;}") 16 | self.setup_layouts() 17 | self.setup_connections() 18 | 19 | def setup_connections(self): 20 | self.show_logs_btn.clicked.connect(self.show_logs) 21 | 22 | def setup_layouts(self): 23 | self.content_layout = QVBoxLayout(self) 24 | self.content_layout.setContentsMargins(0, 0, 0, 0) 25 | self.content_layout.setSpacing(0) 26 | # 顶部标题栏 27 | self.setup_top_bar() 28 | # 内容页 29 | self.setup_pages() 30 | # 底部状态栏 31 | self.setup_bottom_bar() 32 | # 添加组件进布局 33 | self.content_layout.addWidget(self.top_bar) 34 | self.content_layout.addWidget(self.pages) 35 | self.content_layout.addWidget(self.bottom_bar) 36 | 37 | def setup_top_bar(self): 38 | # 顶部标题栏 39 | self.top_bar = QFrame() 40 | # 设置高度 41 | self.top_bar.setMinimumHeight(40) 42 | self.top_bar.setMaximumHeight(40) 43 | self.top_bar_layout = QHBoxLayout(self.top_bar) 44 | # 左边距为10 45 | self.top_bar_layout.setContentsMargins(20, 0, 0, 0) 46 | self.top_label_left = QLabel("Visual Novel Upscaler") 47 | self.top_label_left.setStyleSheet("font: 700 10pt 'Segoe UI'") 48 | # 空间 49 | self.top_spacer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 50 | # 窗口功能按键 51 | self.minimize_btn = FPushButton(height=self.top_bar.height(), 52 | minimum_width=self.top_bar.height(), 53 | btn_color='#282a36', 54 | icon_path=self.icon_folder/'icon_minimize.svg') 55 | self.maximize_btn = FPushButton(height=self.top_bar.height(), 56 | minimum_width=self.top_bar.height(), 57 | btn_color='#282a36', 58 | icon_path=self.icon_folder/'icon_maximize.svg') 59 | self.close_btn = FPushButton(height=self.top_bar.height(), 60 | minimum_width=self.top_bar.height(), 61 | btn_color='#282a36', 62 | icon_path=self.icon_folder/'icon_close.svg') 63 | # 添加控件 64 | self.top_bar_layout.addWidget(self.top_label_left) 65 | self.top_bar_layout.addItem(self.top_spacer) 66 | self.top_bar_layout.addWidget(self.minimize_btn) 67 | self.top_bar_layout.addWidget(self.maximize_btn) 68 | self.top_bar_layout.addWidget(self.close_btn) 69 | 70 | def setup_pages(self): 71 | # 内容页 72 | self.pages = QStackedWidget() 73 | self.pages.setStyleSheet("font-size: 10pt; color: #f8f8f2;") 74 | 75 | def setup_bottom_bar(self): 76 | # 底部状态栏 77 | self.bottom_bar = QFrame() 78 | self.bottom_bar.setMinimumHeight(30) 79 | self.bottom_bar.setMaximumHeight(30) 80 | self.bottom_bar.setStyleSheet(f"background-color: '#282a36';") 81 | self.bottom_bar_layout = QHBoxLayout(self.bottom_bar) 82 | self.bottom_bar_layout.setContentsMargins(10, 0, 2.5, 0) 83 | # self.bottom_label_left = QLabel("准备就绪!") 84 | self.show_logs_btn = FIconButton('查看日志', minimum_width=75, height=25, border_radius=10) 85 | self.bottom_spacer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 86 | # 窗口尺寸控制 87 | self.setup_resize_botton() 88 | # 添加组件 89 | # self.bottom_bar_layout.addWidget(self.bottom_label_left) 90 | self.bottom_bar_layout.addWidget(self.show_logs_btn) 91 | self.bottom_bar_layout.addItem(self.bottom_spacer) 92 | self.bottom_bar_layout.addWidget(self.frame_grip) 93 | 94 | def setup_resize_botton(self): 95 | self.frame_grip = QFrame() 96 | self.frame_grip.setObjectName(u"frame_grip") 97 | self.frame_grip.setMinimumSize(QSize(30, 30)) 98 | self.frame_grip.setMaximumSize(QSize(30, 30)) 99 | self.frame_grip.setStyleSheet(u"padding: 5px;") 100 | self.frame_grip.setFrameShape(QFrame.StyledPanel) 101 | self.frame_grip.setFrameShadow(QFrame.Raised) 102 | self.sizegrip = QSizeGrip(self.frame_grip) 103 | self.sizegrip.setStyleSheet(f"QSizeGrip {{ width: 10px; height: 10px; margin: 5px;background-color: #6272a4; border-radius: 10px;}} QSizeGrip:hover {{ background-color: yellow}}") 104 | self.sizegrip.setToolTip('窗口缩放') 105 | 106 | def show_logs(self): 107 | os.startfile(self.log_file) 108 | -------------------------------------------------------------------------------- /GUI/main_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | from .left_menu import LeftMenu 6 | from .main_content import MainContent 7 | from .image_page import ImagePage 8 | from .game_page import GamePage 9 | from .text_page import TextPage 10 | from .info_page import InfoPage 11 | from .setting_page import SettingPage 12 | 13 | 14 | class MainUI(QMainWindow): 15 | 16 | def __init__(self): 17 | QMainWindow.__init__(self) 18 | self.icon_folder = Path(sys.argv[0]).parent/'Icons' 19 | self.initUI() 20 | # 设置标题栏能用鼠标拖动 21 | self.maincontent.top_bar.mouseMoveEvent = self.moveWindow 22 | 23 | def moveWindow(self, event): 24 | if self.is_maxed: 25 | x_left = event.globalPosition().toPoint().x()-self.leftmenu.width() 26 | x_right = self.width()-event.globalPosition().toPoint().x() 27 | x_left_precent = x_left/self.maincontent.top_bar.width() 28 | y_top = event.globalPosition().toPoint().y() 29 | self.maximize_restore() 30 | if x_left_precent < 0.25: 31 | self.move(event.globalPosition().toPoint()-QPoint(self.leftmenu.width()+x_left, y_top)) 32 | elif x_left_precent > 0.75: 33 | self.move(event.globalPosition().toPoint()-QPoint(self.width()-x_right, y_top)) 34 | else: 35 | # 移动到等比例位置 36 | self.move(event.globalPosition().toPoint()-QPoint(self.leftmenu.width()+self.maincontent.top_bar.width()*x_left_precent, y_top)) 37 | if event.buttons() == Qt.LeftButton: 38 | self.move(self.pos() + event.globalPosition().toPoint() - self.dragPos) 39 | self.dragPos = event.globalPosition().toPoint() 40 | event.accept() 41 | 42 | def initUI(self): 43 | # 设置标题 44 | self.setWindowTitle('Visual Novel Upscaler') 45 | self.setObjectName('mainwin') 46 | # 设置图标 47 | self.setWindowIcon(QIcon(str(self.icon_folder/'icon.ico'))) 48 | # 移除标题栏 49 | self.setWindowFlags(Qt.FramelessWindowHint) 50 | self.setAttribute(Qt.WA_TranslucentBackground) 51 | # 改变尺寸 52 | self.resize(1080, 720) 53 | # 设置最小尺寸 54 | self.setMinimumSize(1080, 720) 55 | self.setup_layouts() 56 | self.setup_pages() 57 | self.setup_connections() 58 | self.move_to_center() 59 | self.is_maxed = False 60 | # 设置圆角 61 | self.setStyleSheet("QFrame {border-radius: 15px};") 62 | # 设置透明度 63 | self.setWindowOpacity(1) 64 | # 显示默认页面 65 | self.show_image_page() 66 | 67 | def setup_layouts(self): 68 | self.central_frame = QFrame() 69 | self.setCentralWidget(self.central_frame) 70 | # 主窗口布局 71 | main_layout = QHBoxLayout(self.central_frame) 72 | # 设置页边距都为0 73 | main_layout.setContentsMargins(0, 0, 0, 0) 74 | # 设置部件间距为0 75 | main_layout.setSpacing(0) 76 | # 添加左侧菜单 77 | self.leftmenu = LeftMenu() 78 | main_layout.addWidget(self.leftmenu) 79 | # 添加内容页面 80 | self.maincontent = MainContent() 81 | main_layout.addWidget(self.maincontent) 82 | 83 | def set_version(self, version_text): 84 | self.leftmenu.version_label.setText(version_text) 85 | 86 | def mousePressEvent(self, event): 87 | self.dragPos = event.globalPosition().toPoint() 88 | 89 | def move_to_center(self): 90 | # 获取屏幕坐标系 91 | screen_center = QGuiApplication.screens()[0].geometry().center() 92 | self.move(screen_center-self.frameGeometry().center()) 93 | 94 | def setup_connections(self): 95 | self.leftmenu.image_button.clicked.connect(self.show_image_page) 96 | self.leftmenu.game_button.clicked.connect(self.show_game_page) 97 | self.leftmenu.text_button.clicked.connect(self.show_text_page) 98 | self.leftmenu.info_btn.clicked.connect(self.show_info_page) 99 | self.leftmenu.setting_btn.clicked.connect(self.show_setting_page) 100 | self.maincontent.minimize_btn.clicked.connect(self.showMinimized) 101 | self.maincontent.maximize_btn.clicked.connect(self.maximize_restore) 102 | # 退出询问 103 | self.maincontent.close_btn.clicked.connect(self.quit_question) 104 | 105 | def setup_pages(self): 106 | # 内容页 107 | self.imagepage = ImagePage() 108 | self.maincontent.pages.addWidget(self.imagepage) 109 | 110 | self.gamepage = GamePage() 111 | self.maincontent.pages.addWidget(self.gamepage) 112 | 113 | self.textpage = TextPage() 114 | self.maincontent.pages.addWidget(self.textpage) 115 | 116 | self.infopage = InfoPage() 117 | self.maincontent.pages.addWidget(self.infopage) 118 | 119 | self.settingpage = SettingPage() 120 | self.maincontent.pages.addWidget(self.settingpage) 121 | 122 | def show_image_page(self): 123 | self.maincontent.pages.setCurrentWidget(self.imagepage) 124 | if self.leftmenu.image_button.is_active: 125 | self.leftmenu.fold_menu() 126 | else: 127 | self.leftmenu.reset_selection() 128 | self.leftmenu.image_button.set_active(True) 129 | 130 | def show_game_page(self): 131 | self.maincontent.pages.setCurrentWidget(self.gamepage) 132 | if self.leftmenu.game_button.is_active: 133 | self.leftmenu.fold_menu() 134 | else: 135 | self.leftmenu.reset_selection() 136 | self.leftmenu.game_button.set_active(True) 137 | 138 | def show_text_page(self): 139 | self.maincontent.pages.setCurrentWidget(self.textpage) 140 | if self.leftmenu.text_button.is_active: 141 | self.leftmenu.fold_menu() 142 | else: 143 | self.leftmenu.reset_selection() 144 | self.leftmenu.text_button.set_active(True) 145 | 146 | def show_info_page(self): 147 | self.maincontent.pages.setCurrentWidget(self.infopage) 148 | if self.leftmenu.info_btn.is_active: 149 | self.leftmenu.fold_menu() 150 | else: 151 | self.leftmenu.reset_selection() 152 | self.leftmenu.info_btn.set_active(True) 153 | 154 | def show_setting_page(self): 155 | self.maincontent.pages.setCurrentWidget(self.settingpage) 156 | if self.leftmenu.setting_btn.is_active: 157 | self.leftmenu.fold_menu() 158 | else: 159 | self.leftmenu.reset_selection() 160 | self.leftmenu.setting_btn.set_active(True) 161 | 162 | def maximize_restore(self): 163 | if not self.is_maxed: 164 | # 放大时取消圆角,填补空隙 165 | self.setStyleSheet("QFrame {border-radius: 0px;}") 166 | self.maincontent.sizegrip.hide() 167 | self.showMaximized() 168 | self.is_maxed = True 169 | else: 170 | self.setStyleSheet("QFrame {border-radius: 15px;}") 171 | self.maincontent.sizegrip.show() 172 | self.showNormal() 173 | self.is_maxed = False 174 | 175 | def quit_question(self): 176 | # 退出时询问 177 | quit_message = QMessageBox() 178 | reply = quit_message.question(self, '退出程序', '确认退出?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) 179 | self.close() if reply == QMessageBox.Yes else None 180 | -------------------------------------------------------------------------------- /GUI/majiro_part.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | 6 | 7 | class MajiroPart(FTabWidget): 8 | def __init__(self): 9 | FTabWidget.__init__(self, height=40, position='top') 10 | self.icon_folder = Path(sys.argv[0]).parent/'Icons' 11 | self.initUI() 12 | self.set_ratio_state() 13 | self.set_resolution_state() 14 | self.select_all_part() 15 | self.set_game_resolution_encoding(1280, 720, 'Shift_JIS') 16 | # self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 17 | 18 | def initUI(self): 19 | 20 | self.setup_hd_parts() 21 | self.addTab(self.hd_parts_frame, '高清重制') 22 | 23 | self.setup_work_up() 24 | self.addTab(self.work_up_frame, '重制后处理') 25 | 26 | self.setup_zip() 27 | self.addTab(self.zip_frame, '存储优化') 28 | 29 | self.setup_connections() 30 | 31 | def setup_zip(self): 32 | self.zip_frame = QFrame() 33 | layout = QVBoxLayout(self.zip_frame) 34 | layout.addWidget(QLabel('正在开发中...')) 35 | 36 | def setup_hd_parts(self): 37 | self.hd_parts_frame = QFrame() 38 | layout = QHBoxLayout(self.hd_parts_frame) 39 | 40 | self.setup_choose_resolution() 41 | layout.addWidget(self.choose_resolution_Frame, 1) 42 | 43 | self.setup_select_run_parts() 44 | layout.addWidget(self.select_run_parts_frame, 1) 45 | 46 | def setup_choose_resolution(self): 47 | self.choose_resolution_Frame = QFrame() 48 | layout1 = QFormLayout(self.choose_resolution_Frame) 49 | self.choose_resolution_lb = QLabel('分辨率设定:') 50 | layout2 = QVBoxLayout() 51 | layout2.setContentsMargins(0, 5, 0, 0) 52 | layout1.addRow(self.choose_resolution_lb, layout2) 53 | 54 | formlayout1 = QFormLayout() 55 | self.before_resolution_lb = QLabel('原生分辨率:') 56 | resolution_hlayout = QHBoxLayout() 57 | resolution_hlayout.setContentsMargins(0, 0, 0, 0) 58 | resolution_hlayout.setSpacing(15) 59 | self.before_resolution = QLabel() 60 | self.main_encoding = QLabel() 61 | resolution_hlayout.addWidget(self.before_resolution) 62 | resolution_hlayout.addWidget(self.main_encoding) 63 | self.check_resolution_btn = FPushButton(text='检测分辨率', height=20, minimum_width=80, text_padding=0, text_align='center', border_radius=10) 64 | self.check_resolution_btn.show_shadow() 65 | resolution_hlayout.addWidget(self.check_resolution_btn) 66 | _spacer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 67 | resolution_hlayout.addItem(_spacer) 68 | formlayout1.addRow(self.before_resolution_lb, resolution_hlayout) 69 | 70 | self.s1080p_btn = QRadioButton('1080P') 71 | self.s2k_btn = QRadioButton('2K') 72 | self.s4k_btn = QRadioButton('4K') 73 | 74 | formlayout2 = QFormLayout() 75 | 76 | self.custiom_ratio_btn = QRadioButton('自定义放大倍率:') 77 | self.custiom_ratio_spinbox = QDoubleSpinBox() 78 | self.custiom_ratio_spinbox.setDecimals(3) 79 | self.custiom_ratio_spinbox.setSingleStep(0.5) 80 | formlayout2.addRow(self.custiom_ratio_btn, self.custiom_ratio_spinbox) 81 | 82 | self.custom_resolution_btn = QRadioButton('自定义分辨率:') 83 | custom_resolution_hlayout = QHBoxLayout() 84 | self.width_line_edit = FLineEdit() 85 | self.x_label = QLabel('x') 86 | self.x_label.setMaximumWidth(10) 87 | self.height_line_edit = FLineEdit() 88 | custom_resolution_hlayout.addWidget(self.width_line_edit) 89 | custom_resolution_hlayout.addWidget(self.x_label) 90 | custom_resolution_hlayout.addWidget(self.height_line_edit) 91 | formlayout2.addRow(self.custom_resolution_btn, custom_resolution_hlayout) 92 | 93 | layout2.addLayout(formlayout1) 94 | layout2.addWidget(self.s1080p_btn) 95 | layout2.addWidget(self.s2k_btn) 96 | layout2.addWidget(self.s4k_btn) 97 | layout2.addLayout(formlayout2) 98 | # 分组 99 | self.sr_group = QButtonGroup() 100 | self.sr_group.addButton(self.s1080p_btn) 101 | self.sr_group.addButton(self.s2k_btn) 102 | self.sr_group.addButton(self.s4k_btn) 103 | self.sr_group.addButton(self.custiom_ratio_btn) 104 | self.sr_group.addButton(self.custom_resolution_btn) 105 | 106 | def setup_select_run_parts(self): 107 | self.select_run_parts_frame = QFrame() 108 | layout1 = QFormLayout(self.select_run_parts_frame) 109 | 110 | self.game_part_lb = QLabel('处理部分选择:') 111 | layout2 = QVBoxLayout() 112 | layout2.setContentsMargins(0, 4, 0, 0) 113 | layout1.addRow(self.game_part_lb, layout2) 114 | 115 | self.text_part = QCheckBox('文本') 116 | self.image_part = QCheckBox('图片') 117 | self.video_part = QCheckBox('视频') 118 | self.select_all_btn = FPushButton(text='全选', height=20, minimum_width=50, text_padding=0, text_align='center', border_radius=10) 119 | self.select_none_btn = FPushButton(text='全不选', height=20, minimum_width=50, text_padding=0, text_align='center', border_radius=10) 120 | hlayout = QHBoxLayout() 121 | hlayout.addWidget(self.select_all_btn) 122 | hlayout.addWidget(self.select_none_btn) 123 | 124 | layout2.addWidget(self.text_part) 125 | layout2.addWidget(self.image_part) 126 | layout2.addWidget(self.video_part) 127 | layout2.addLayout(hlayout) 128 | 129 | self.patch_mode_lb = QLabel('高清补丁输出:') 130 | self.keep_mode_btn = QCheckBox('保持目录结构') 131 | layout1.addRow(self.patch_mode_lb, self.keep_mode_btn) 132 | self.keep_mode_btn.setChecked(True) 133 | self.keep_mode_btn.setDisabled(True) 134 | 135 | def setup_work_up(self): 136 | self.work_up_frame = QFrame() 137 | self.work_up_layout = QFormLayout(self.work_up_frame) 138 | self.work_up_layout.setContentsMargins(15, 25, 15, 0) 139 | self.work_up_layout.setSpacing(15) 140 | 141 | self.setup_mjo_converter() 142 | 143 | self.work_up_group = QButtonGroup() 144 | self.work_up_group.addButton(self.mjo_convert_btn) 145 | 146 | def setup_mjo_converter(self): 147 | self.mjo_convert_btn = QRadioButton('MJO格式转换:') 148 | hlayout1 = QHBoxLayout() 149 | self.work_up_layout.addRow(self.mjo_convert_btn, hlayout1) 150 | 151 | self.mjo_in_label = QLabel('输入格式:') 152 | self.mjo_in = QComboBox() 153 | self.mjo_in.addItems(['mjo', 'mjil&mjres']) 154 | self.mjo_out_label = QLabel('输出格式:') 155 | self.mjo_out = QComboBox() 156 | self.mjo_out.addItems(['mjil&mjres']) 157 | hlayout1.addWidget(self.mjo_in_label) 158 | hlayout1.addWidget(self.mjo_in) 159 | hlayout1.addWidget(self.mjo_out_label) 160 | hlayout1.addWidget(self.mjo_out) 161 | sapcer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 162 | hlayout1.addItem(sapcer) 163 | 164 | def change_mjo_out(self): 165 | match self.mjo_in.currentText(): 166 | case 'mjo': 167 | self.mjo_out.clear() 168 | self.mjo_out.addItems(['mjil&mjres']) 169 | case 'mjil&mjres': 170 | self.mjo_out.clear() 171 | self.mjo_out.addItems(['mjo']) 172 | self.mjo_convert_btn.setChecked(True) 173 | 174 | def setup_connections(self): 175 | self.s1080p_btn.toggled.connect(self.s1080p_btn_ratio) 176 | self.s2k_btn.toggled.connect(self.s2k_btn_ratio) 177 | self.s4k_btn.toggled.connect(self.s4k_btn_ratio) 178 | self.custiom_ratio_btn.toggled.connect(self.set_ratio_state) 179 | self.custiom_ratio_spinbox.textChanged.connect(self.auto_change_width_height) 180 | self.custom_resolution_btn.toggled.connect(self.set_resolution_state) 181 | self.width_line_edit.textEdited.connect(self.auto_change_height_ratio) 182 | self.height_line_edit.textEdited.connect(self.auto_change_width_ratio) 183 | self.select_all_btn.clicked.connect(self.select_all_part) 184 | self.select_none_btn.clicked.connect(self.select_none_part) 185 | self.mjo_in.currentTextChanged.connect(self.change_mjo_out) 186 | 187 | def select_all_part(self): 188 | for check_box in self.select_run_parts_frame.findChildren(QCheckBox): 189 | if check_box is self.keep_mode_btn: 190 | continue 191 | check_box.setChecked(True) 192 | 193 | def select_none_part(self): 194 | for check_box in self.select_run_parts_frame.findChildren(QCheckBox): 195 | if check_box is self.keep_mode_btn: 196 | continue 197 | check_box.setChecked(False) 198 | 199 | def set_ratio_state(self): 200 | if self.custiom_ratio_btn.isChecked(): 201 | self.custiom_ratio_spinbox.setEnabled(True) 202 | else: 203 | self.custiom_ratio_spinbox.setDisabled(True) 204 | 205 | def set_resolution_state(self): 206 | if self.custom_resolution_btn.isChecked(): 207 | self.width_line_edit.setEnabled(True) 208 | self.height_line_edit.setEnabled(True) 209 | else: 210 | self.width_line_edit.setDisabled(True) 211 | self.height_line_edit.setDisabled(True) 212 | 213 | def auto_change_height_ratio(self): 214 | width, height = map(int, self.before_resolution.text().split('x')) 215 | try: 216 | ratio = int(self.width_line_edit.text())/width 217 | self.custiom_ratio_spinbox.setValue(ratio) 218 | self.height_line_edit.setText(str(int(height*ratio))) 219 | except: 220 | pass 221 | 222 | def auto_change_width_ratio(self): 223 | width, height = map(int, self.before_resolution.text().split('x')) 224 | try: 225 | pass 226 | ratio = int(self.height_line_edit.text())/height 227 | self.custiom_ratio_spinbox.setValue(ratio) 228 | self.width_line_edit.setText(str(int(width*ratio))) 229 | except: 230 | pass 231 | 232 | def auto_change_width_height(self): 233 | # 避免信号冲突 234 | if not self.custom_resolution_btn.isChecked(): 235 | width, height = map(int, self.before_resolution.text().split('x')) 236 | ratio = float(self.custiom_ratio_spinbox.value()) 237 | self.width_line_edit.setText(str(int(width*ratio))) 238 | self.height_line_edit.setText(str(int(height*ratio))) 239 | 240 | def judge_scaled_resolution_btn(self): 241 | width, height = map(int, self.before_resolution.text().split('x')) 242 | if width/height == 16/9: 243 | self.s1080p_btn.setEnabled(True) 244 | self.s2k_btn.setEnabled(True) 245 | self.s4k_btn.setEnabled(True) 246 | # self.s1080p_btn.setChecked(True) 247 | else: 248 | # 非16:9 249 | self.s1080p_btn.setDisabled(True) 250 | self.s2k_btn.setDisabled(True) 251 | self.s4k_btn.setDisabled(True) 252 | # 默认2倍放大 253 | self.custiom_ratio_btn.setChecked(True) 254 | self.custiom_ratio_spinbox.setValue(1) 255 | self.custiom_ratio_spinbox.setValue(2) 256 | 257 | def set_game_resolution_encoding(self, width, height, encoding): 258 | self.before_resolution.setText(f'{width}x{height}') 259 | self.main_encoding.setText(encoding) 260 | self.judge_scaled_resolution_btn() 261 | 262 | def s1080p_btn_ratio(self): 263 | width, height = map(int, self.before_resolution.text().split('x')) 264 | ratio = 1080/height 265 | self.custiom_ratio_spinbox.setValue(ratio) 266 | 267 | def s2k_btn_ratio(self): 268 | width, height = map(int, self.before_resolution.text().split('x')) 269 | ratio = 1440/height 270 | self.custiom_ratio_spinbox.setValue(ratio) 271 | 272 | def s4k_btn_ratio(self): 273 | width, height = map(int, self.before_resolution.text().split('x')) 274 | ratio = 2160/height 275 | self.custiom_ratio_spinbox.setValue(ratio) 276 | -------------------------------------------------------------------------------- /GUI/qt_core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from Core import * 4 | from PySide6.QtCore import * 5 | from PySide6.QtGui import * 6 | from PySide6.QtWidgets import * 7 | 8 | def add_shadow(target_widget): 9 | """ 10 | @brief 给控件添加阴影 11 | 12 | @param target_widget 控件 13 | """ 14 | shadow = QGraphicsDropShadowEffect(target_widget) 15 | shadow.setBlurRadius(15) 16 | shadow.setXOffset(0) 17 | shadow.setYOffset(0) 18 | shadow.setColor(QColor(0, 0, 0, 200)) 19 | target_widget.setGraphicsEffect(shadow) 20 | -------------------------------------------------------------------------------- /GUI/setting_page.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | from .sr_engine_settings import * 6 | 7 | 8 | class SettingPage(QFrame): 9 | def __init__(self): 10 | QFrame.__init__(self) 11 | # self.theme = {'MainContent_background': '#282a36', 'MainContent_font': '#6272a4', 'MainContent_bar': '#21232d', } 12 | self.sr_engine_list = ['waifu2x_ncnn', 13 | 'real_cugan', 14 | 'real_esrgan', 15 | 'srmd_ncnn', 16 | 'realsr_ncnn', 17 | 'anime4k'] 18 | self.initUI() 19 | 20 | def initUI(self): 21 | # self.setStyleSheet(f"background-color: {self.theme['MainContent_background']};color: {self.theme['MainContent_font']};font: 700 12pt 'Segoe UI';") 22 | self.setup_layouts() 23 | self.setup_connections() 24 | 25 | def setup_layouts(self): 26 | layout = QVBoxLayout(self) 27 | layout.setContentsMargins(20, 0, 20, 20) 28 | self.setup_general_settings() 29 | layout.addWidget(self.general_setting_frame) 30 | self.setup_image_settings() 31 | layout.addWidget(self.image_setting_frame) 32 | self.setup_video_settings() 33 | layout.addWidget(self.video_setting_frame) 34 | self.setup_sr_engine_settings() 35 | layout.addWidget(self.image_setting_frame) 36 | 37 | spacer_mid1 = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding) 38 | spacer_mid2 = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding) 39 | spacer_mid3 = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding) 40 | layout.addItem(spacer_mid1) 41 | layout.addItem(spacer_mid2) 42 | layout.addItem(spacer_mid3) 43 | 44 | self.setup_bottom_bar() 45 | layout.addWidget(self.bottom_bar) 46 | 47 | def setup_connections(self): 48 | self.sr_engine_combobox.currentTextChanged.connect(self.switch_image_sr_engine_settings) 49 | self.image_sr_engine_combobox.currentTextChanged.connect(self.image_engine_auto_switch) 50 | self.video_sr_engine_combobox.currentTextChanged.connect(self.video_engine_auto_switch) 51 | 52 | def setup_general_settings(self): 53 | self.general_setting_frame = QFrame() 54 | layout = QFormLayout(self.general_setting_frame) 55 | layout.setSpacing(5) 56 | self.general_setting_lb = QLabel('通用设置') 57 | layout.addRow(self.general_setting_lb) 58 | self.cpu_lb = QLabel('CPU并发核数:') 59 | self.cpu_spinbox = QSpinBox() 60 | self.cpu_spinbox.setMinimum(1) 61 | layout.addRow(self.cpu_lb, self.cpu_spinbox) 62 | self.gpu_lb = QLabel('GPU型号:') 63 | self.gpu_combobox = QComboBox() 64 | self.gpu_combobox.addItems(get_gpu_list()) 65 | layout.addRow(self.gpu_lb, self.gpu_combobox) 66 | self.text_encoding_lb = QLabel('文本编码列表:') 67 | self.text_encoding_line_edit = FLineEdit(place_holder_text='使用英文逗号分隔,优先级从左到右') 68 | layout.addRow(self.text_encoding_lb, self.text_encoding_line_edit) 69 | 70 | def setup_bottom_bar(self): 71 | width = 60 72 | height = 30 73 | self.bottom_bar = QFrame() 74 | self.bottom_bar.setMinimumHeight(height) 75 | self.bottom_bar.setMaximumHeight(height) 76 | layout = QHBoxLayout(self.bottom_bar) 77 | layout.setContentsMargins(20, 0, 20, 0) 78 | layout.setSpacing(15) 79 | bottom_spacer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 80 | layout.addItem(bottom_spacer) 81 | self.save_btn = FPushButton(text='保存', height=height, minimum_width=width, text_padding=0, text_align='center', border_radius=15) 82 | self.cancle_btn = FPushButton(text='取消', height=height, minimum_width=width, text_padding=0, text_align='center', border_radius=15) 83 | self.reset_btn = FPushButton(text='重置', height=height, minimum_width=width, text_padding=0, text_align='center', border_radius=15) 84 | layout.addWidget(self.save_btn) 85 | layout.addWidget(self.cancle_btn) 86 | layout.addWidget(self.reset_btn) 87 | 88 | def setup_sr_engine_settings(self): 89 | self.image_setting_frame = QFrame() 90 | layout = QFormLayout(self.image_setting_frame) 91 | layout.setSpacing(5) 92 | 93 | self.sr_engine_setting_lb = QLabel('超分引擎设置') 94 | layout.addRow(self.sr_engine_setting_lb) 95 | self.sr_engine_lb = QLabel('超分辨率引擎:') 96 | self.sr_engine_combobox = QComboBox() 97 | self.sr_engine_combobox.addItems(self.sr_engine_list) 98 | layout.addRow(self.sr_engine_lb, self.sr_engine_combobox) 99 | 100 | self.tta_lb = QLabel('TTA模式:') 101 | self.tta_checkbox = QCheckBox() 102 | layout.addRow(self.tta_lb, self.tta_checkbox) 103 | 104 | self.image_setting_stacks = QStackedWidget() 105 | layout.addRow(self.image_setting_stacks) 106 | self.real_cugan_settings = RealCUGNSettings() 107 | self.image_setting_stacks.addWidget(self.real_cugan_settings) 108 | self.waifu2x_ncnn_settings = Waifu2xNCNNSettings() 109 | self.image_setting_stacks.addWidget(self.waifu2x_ncnn_settings) 110 | self.real_esrgan_settings = RealESRGANSettings() 111 | self.image_setting_stacks.addWidget(self.real_esrgan_settings) 112 | self.srmd_ncnn_settings = SRMDNCNNSettings() 113 | self.image_setting_stacks.addWidget(self.srmd_ncnn_settings) 114 | self.realsr_ncnn_settings = RealSRNCNNSettings() 115 | self.image_setting_stacks.addWidget(self.realsr_ncnn_settings) 116 | self.anime4k_settings = Anime4KSettings() 117 | self.image_setting_stacks.addWidget(self.anime4k_settings) 118 | 119 | def switch_image_sr_engine_settings(self): 120 | sr_engine = self.sr_engine_combobox.currentText() 121 | match sr_engine: 122 | case 'waifu2x_ncnn': 123 | self.image_setting_stacks.setCurrentWidget(self.waifu2x_ncnn_settings) 124 | case 'real_cugan': 125 | self.image_setting_stacks.setCurrentWidget(self.real_cugan_settings) 126 | case 'real_esrgan': 127 | self.image_setting_stacks.setCurrentWidget(self.real_esrgan_settings) 128 | case 'srmd_ncnn': 129 | self.image_setting_stacks.setCurrentWidget(self.srmd_ncnn_settings) 130 | case 'realsr_ncnn': 131 | self.image_setting_stacks.setCurrentWidget(self.realsr_ncnn_settings) 132 | case 'anime4k': 133 | self.image_setting_stacks.setCurrentWidget(self.anime4k_settings) 134 | 135 | def image_engine_auto_switch(self): 136 | sr_engine = self.image_sr_engine_combobox.currentText() 137 | self.sr_engine_combobox.setCurrentText(sr_engine) 138 | 139 | def setup_image_settings(self): 140 | self.image_setting_frame = QFrame() 141 | layout = QFormLayout(self.image_setting_frame) 142 | layout.setSpacing(5) 143 | self.image_setting_lb = QLabel('图片设置') 144 | layout.addRow(self.image_setting_lb) 145 | self.image_sr_engine_lb = QLabel('图片超分引擎:') 146 | self.image_sr_engine_combobox = QComboBox() 147 | image_sr_engine_list = [sr_engine for sr_engine in self.sr_engine_list] 148 | # image_sr_engine_list.remove('anime4k') 149 | self.image_sr_engine_combobox.addItems(image_sr_engine_list) 150 | layout.addRow(self.image_sr_engine_lb, self.image_sr_engine_combobox) 151 | self.image_batch_size_lb = QLabel('图片批次大小:') 152 | self.image_batch_size_spinbox = QSpinBox() 153 | self.image_batch_size_spinbox.setRange(0, 999) 154 | layout.addRow(self.image_batch_size_lb, self.image_batch_size_spinbox) 155 | 156 | def video_engine_auto_switch(self): 157 | sr_engine = self.video_sr_engine_combobox.currentText() 158 | self.sr_engine_combobox.setCurrentText(sr_engine) 159 | 160 | def setup_video_settings(self): 161 | self.video_setting_frame = QFrame() 162 | layout = QFormLayout(self.video_setting_frame) 163 | layout.setSpacing(5) 164 | self.video_setting_lb = QLabel('视频设置') 165 | layout.addRow(self.video_setting_lb) 166 | self.video_sr_engine_lb = QLabel('视频超分引擎:') 167 | self.video_sr_engine_combobox = QComboBox() 168 | self.video_sr_engine_combobox.addItems(self.sr_engine_list) 169 | layout.addRow(self.video_sr_engine_lb, self.video_sr_engine_combobox) 170 | self.video_batch_size_lb = QLabel('视频批次大小:') 171 | self.video_batch_size_spinbox = QSpinBox() 172 | self.video_batch_size_spinbox.setRange(0, 999) 173 | layout.addRow(self.video_batch_size_lb, self.video_batch_size_spinbox) 174 | self.video_quality_lb = QLabel('输出视频质量:') 175 | self.video_quality_spinbox = QSpinBox() 176 | self.video_quality_spinbox.setRange(0, 10) 177 | layout.addRow(self.video_quality_lb, self.video_quality_spinbox) 178 | -------------------------------------------------------------------------------- /GUI/sr_engine_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | 6 | 7 | class Waifu2xNCNNSettings(QFrame): 8 | 9 | def __init__(self): 10 | QFrame.__init__(self) 11 | layout = QFormLayout(self) 12 | layout.setContentsMargins(0, 0, 0, 0) 13 | layout.setSpacing(5) 14 | 15 | self.noise_level_lb = QLabel('降噪等级:') 16 | self.noise_level_spinbox = QSpinBox() 17 | self.noise_level_spinbox.setRange(-1, 3) 18 | layout.addRow(self.noise_level_lb, self.noise_level_spinbox) 19 | 20 | self.tile_size_lb = QLabel('分割尺寸:') 21 | self.tile_size_line_edit = FLineEdit() 22 | layout.addRow(self.tile_size_lb, self.tile_size_line_edit) 23 | 24 | self.modle_name_lb = QLabel('超分模型选择:') 25 | self.modle_name_combobox = QComboBox() 26 | layout.addRow(self.modle_name_lb, self.modle_name_combobox) 27 | self.modle_name_combobox.addItems(['models-cunet', 'models-upconv_7_anime_style_art_rgb', 'models-upconv_7_photo']) 28 | 29 | self.load_proc_save_lb = QLabel('显卡线程分配:') 30 | self.load_proc_save_line_edit = FLineEdit() 31 | self.load_proc_save_line_edit = FLineEdit(place_holder_text='解码:放大:编码') 32 | layout.addRow(self.load_proc_save_lb, self.load_proc_save_line_edit) 33 | 34 | 35 | class RealCUGNSettings(QFrame): 36 | 37 | def __init__(self): 38 | QFrame.__init__(self) 39 | layout = QFormLayout(self) 40 | layout.setContentsMargins(0, 0, 0, 0) 41 | layout.setSpacing(5) 42 | 43 | self.noise_level_lb = QLabel('降噪等级:') 44 | self.noise_level_spinbox = QSpinBox() 45 | self.noise_level_spinbox.setRange(-1, 3) 46 | layout.addRow(self.noise_level_lb, self.noise_level_spinbox) 47 | 48 | self.tile_size_lb = QLabel('分割尺寸:') 49 | self.tile_size_line_edit = FLineEdit() 50 | layout.addRow(self.tile_size_lb, self.tile_size_line_edit) 51 | 52 | self.sync_gap_mode_lb = QLabel('同步间隙等级:') 53 | self.sync_gap_mode_spinbox = QSpinBox() 54 | self.sync_gap_mode_spinbox.setRange(0, 3) 55 | layout.addRow(self.sync_gap_mode_lb, self.sync_gap_mode_spinbox) 56 | 57 | self.modle_name_lb = QLabel('超分模型选择:') 58 | self.modle_name_combobox = QComboBox() 59 | layout.addRow(self.modle_name_lb, self.modle_name_combobox) 60 | self.modle_name_combobox.addItems(['models-se', 'models-nose']) 61 | 62 | self.load_proc_save_lb = QLabel('显卡线程分配:') 63 | self.load_proc_save_line_edit = FLineEdit() 64 | self.load_proc_save_line_edit = FLineEdit(place_holder_text='解码:放大:编码') 65 | layout.addRow(self.load_proc_save_lb, self.load_proc_save_line_edit) 66 | 67 | 68 | class RealESRGANSettings(QFrame): 69 | 70 | def __init__(self): 71 | QFrame.__init__(self) 72 | layout = QFormLayout(self) 73 | layout.setContentsMargins(0, 0, 0, 0) 74 | layout.setSpacing(5) 75 | 76 | self.tile_size_lb = QLabel('分割尺寸:') 77 | self.tile_size_line_edit = FLineEdit() 78 | layout.addRow(self.tile_size_lb, self.tile_size_line_edit) 79 | 80 | self.modle_name_lb = QLabel('超分模型选择:') 81 | self.modle_name_combobox = QComboBox() 82 | layout.addRow(self.modle_name_lb, self.modle_name_combobox) 83 | self.modle_name_combobox.addItems(['realesrgan-x4plus', 'realesrgan-x4plus-anime', 'realesr-animevideov3-x2', 'realesr-animevideov3-x3', 'realesr-animevideov3-x4']) 84 | 85 | self.load_proc_save_lb = QLabel('显卡线程分配:') 86 | self.load_proc_save_line_edit = FLineEdit() 87 | self.load_proc_save_line_edit = FLineEdit(place_holder_text='解码:放大:编码') 88 | layout.addRow(self.load_proc_save_lb, self.load_proc_save_line_edit) 89 | 90 | 91 | class SRMDNCNNSettings(QFrame): 92 | 93 | def __init__(self): 94 | QFrame.__init__(self) 95 | layout = QFormLayout(self) 96 | layout.setContentsMargins(0, 0, 0, 0) 97 | layout.setSpacing(5) 98 | 99 | self.noise_level_lb = QLabel('降噪等级:') 100 | self.noise_level_spinbox = QSpinBox() 101 | self.noise_level_spinbox.setRange(-1, 10) 102 | layout.addRow(self.noise_level_lb, self.noise_level_spinbox) 103 | 104 | self.tile_size_lb = QLabel('分割尺寸:') 105 | self.tile_size_line_edit = FLineEdit() 106 | layout.addRow(self.tile_size_lb, self.tile_size_line_edit) 107 | 108 | self.load_proc_save_lb = QLabel('显卡线程分配:') 109 | self.load_proc_save_line_edit = FLineEdit() 110 | self.load_proc_save_line_edit = FLineEdit(place_holder_text='解码:放大:编码') 111 | layout.addRow(self.load_proc_save_lb, self.load_proc_save_line_edit) 112 | 113 | 114 | class RealSRNCNNSettings(QFrame): 115 | 116 | def __init__(self): 117 | QFrame.__init__(self) 118 | layout = QFormLayout(self) 119 | layout.setContentsMargins(0, 0, 0, 0) 120 | layout.setSpacing(5) 121 | 122 | self.tile_size_lb = QLabel('分割尺寸:') 123 | self.tile_size_line_edit = FLineEdit() 124 | layout.addRow(self.tile_size_lb, self.tile_size_line_edit) 125 | 126 | self.modle_name_lb = QLabel('超分模型选择:') 127 | self.modle_name_combobox = QComboBox() 128 | layout.addRow(self.modle_name_lb, self.modle_name_combobox) 129 | self.modle_name_combobox.addItems(['models-DF2K', 'models-DF2K_JPEG']) 130 | 131 | self.load_proc_save_lb = QLabel('显卡线程分配:') 132 | self.load_proc_save_line_edit = FLineEdit() 133 | self.load_proc_save_line_edit = FLineEdit(place_holder_text='解码:放大:编码') 134 | layout.addRow(self.load_proc_save_lb, self.load_proc_save_line_edit) 135 | 136 | 137 | class Anime4KSettings(QFrame): 138 | 139 | def __init__(self): 140 | QFrame.__init__(self) 141 | layout = QFormLayout(self) 142 | layout.setContentsMargins(0, 0, 0, 0) 143 | layout.setSpacing(5) 144 | 145 | self.acnet_lb = QLabel('ACNet模式:') 146 | self.acnet_checkbox = QCheckBox() 147 | layout.addRow(self.acnet_lb, self.acnet_checkbox) 148 | 149 | self.hdn_mode_lb = QLabel('HDN模式:') 150 | self.hdn_mode_checkbox = QCheckBox() 151 | layout.addRow(self.hdn_mode_lb, self.hdn_mode_checkbox) 152 | 153 | self.hdn_level_lb = QLabel('降噪去噪等级:') 154 | self.hdn_level_spinbox = QSpinBox() 155 | self.hdn_level_spinbox.setRange(1, 3) 156 | layout.addRow(self.hdn_level_lb, self.hdn_level_spinbox) 157 | -------------------------------------------------------------------------------- /GUI/text_page.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .qt_core import * 4 | from .flat_widgets import * 5 | 6 | 7 | class TextPage(QFrame): 8 | 9 | def __init__(self): 10 | QFrame.__init__(self) 11 | self.icon_folder = Path(sys.argv[0]).parent/'Icons' 12 | self.initUI() 13 | self.set_file_root_path('./') 14 | 15 | def initUI(self): 16 | self.setup_layouts() 17 | self.setup_connections() 18 | 19 | def setup_connections(self): 20 | self.text_tree_view.selectionModel().currentChanged.connect(self.show_text) 21 | # self.text_tree_view.clicked.connect(self.show_text) 22 | self.text_tree_view.doubleClicked.connect(self.changed_root_dir) 23 | 24 | def setup_layouts(self): 25 | self.main_layout = QVBoxLayout(self) 26 | 27 | self.setup_edit() 28 | 29 | self.setup_text_area() 30 | 31 | def setup_edit(self): 32 | layout = QHBoxLayout() 33 | self.main_layout.addLayout(layout) 34 | 35 | self.lb1 = QLabel('正则表达式:') 36 | kwds_ = '|'.join(['width', 'height', 'left', 'top']) 37 | self.lnedt = FLineEdit(rf'(?<=(\W|^)({kwds_})\W+((int|float|double)\W+)?)(\d+)(?=\W|$)') 38 | layout.addWidget(self.lb1) 39 | layout.addWidget(self.lnedt) 40 | 41 | def setup_text_area(self): 42 | self.text_layout = QSplitter(Qt.Horizontal) 43 | self.main_layout.addWidget(self.text_layout) 44 | 45 | self.setup_text_tree_view() 46 | 47 | self.org_text_edit = QTextEdit() 48 | self.org_text_edit.setStyleSheet('background-color:#456') 49 | 50 | self.edited_text_edit = QTextEdit() 51 | self.edited_text_edit.setStyleSheet('background-color:#456') 52 | 53 | self.text_layout.addWidget(self.org_text_edit) 54 | self.text_layout.addWidget(self.edited_text_edit) 55 | 56 | def setup_text_tree_view(self): 57 | self.text_tree_view = QTreeView() 58 | self.text_layout.addWidget(self.text_tree_view) 59 | 60 | self.file_model = QFileSystemModel() 61 | self.text_tree_view.setModel(self.file_model) 62 | for i in range(1, self.file_model.columnCount()): 63 | self.text_tree_view.hideColumn(i) 64 | self.text_tree_view.setSortingEnabled(False) 65 | # self.text_tree_view.setStyleSheet('background-color:#456') 66 | 67 | def set_file_root_path(self, root_path): 68 | self.file_model.setRootPath(root_path) 69 | self.text_tree_view.setRootIndex(self.file_model.index(root_path)) 70 | 71 | def show_text(self, current): 72 | file_path = Path(self.text_tree_view.model().filePath(current)) 73 | if file_path.exists() and file_path.is_file(): 74 | try: 75 | with open(file_path, 'r') as f: 76 | content = f.read() 77 | self.org_text_edit.setPlainText(content) 78 | except: 79 | self.org_text_edit.setPlainText('无法打开此文件') 80 | 81 | def changed_root_dir(self, current): 82 | file_path = Path(self.text_tree_view.model().filePath(current)) 83 | print(file_path) 84 | if file_path.exists() and file_path.is_dir(): 85 | self.set_file_root_path(str(file_path)) 86 | -------------------------------------------------------------------------------- /GamePageUIConnection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from Core import * 4 | from GUI import * 5 | from VNEngines import * 6 | 7 | 8 | class GamePageUIConnection(object): 9 | 10 | def __init__(self): 11 | self.ui_game_page_connections() 12 | 13 | def ui_game_page_connections(self): 14 | self.ui.gamepage.kirikiri.check_resolution_btn.clicked.connect(self.kirikiri_check_resolution) 15 | self.ui.gamepage.artemis.check_resolution_btn.clicked.connect(self.artemis_check_resolution) 16 | self.ui.gamepage.run_btn.clicked.connect(self.game_page_run) 17 | 18 | def game_page_run(self): 19 | self.input_path = Path(self.ui.gamepage.select_input_folder_line_edit.text().strip()) 20 | self.output_folder = Path(self.ui.gamepage.select_output_folder_line_edit.text().strip()) 21 | # 子线程中运行 22 | self.game_page_runner = GamePageRunner(self) 23 | # pyside6信号需要通过实例绑定 24 | self.game_page_runner.start_sig.connect(self.start_game_page_runner_and_lock) 25 | self.game_page_runner.info_sig.connect(self.append_game_info_text_edit) 26 | self.game_page_runner.progress_sig.connect(self.update_game_progress_bar) 27 | self.game_page_runner.finish_sig.connect(self.finish_game_page_runner_and_unlock) 28 | self.game_page_runner.crash_sig.connect(self.crash_game_page_runner_and_unlock) 29 | self.game_page_runner.tip_sig.connect(self.show_game_page_tip) 30 | self.game_page_runner.start() 31 | 32 | def show_game_page_tip(self, _str): 33 | warn_msg = QMessageBox() 34 | reply = warn_msg.warning(self.ui, '提示', _str, QMessageBox.Yes) 35 | 36 | def start_game_page_runner_and_lock(self): 37 | # 开始时锁定,防止重复操作 38 | self.ui.gamepage.set_running_state(1) 39 | # 清空历史信息 40 | self.ui.gamepage.info_text_edit.clear() 41 | self.emit_info(format('开始处理', '=^76')) 42 | self.ui.gamepage.info_text_edit.append(format('开始处理', '=^76')) 43 | 44 | def append_game_info_text_edit(self, info_str): 45 | self.ui.gamepage.info_text_edit.append(info_str) 46 | 47 | def update_game_progress_bar(self, _percent, _left_time): 48 | self.ui.gamepage.set_running_state(2) 49 | self.ui.gamepage.status_progress_bar.setValue(_percent) 50 | left_time_str = seconds_format(_left_time) 51 | self.ui.gamepage.run_btn.setText(left_time_str) 52 | if _percent == 100: 53 | self.ui.gamepage.set_running_state(1) 54 | 55 | def finish_game_page_runner_and_unlock(self, info_str=''): 56 | self.ui.gamepage.set_running_state(3) 57 | self.ui.gamepage.info_text_edit.append(info_str) 58 | self.emit_info(format('结束处理', '=^76')) 59 | self.ui.gamepage.info_text_edit.append(format('结束处理', '=^76')) 60 | finish_info_msg = QMessageBox() 61 | reply = finish_info_msg.information(self.ui, '处理完成', f'{info_str}\n是否打开输出文件夹?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) 62 | show_folder(self.output_folder) if reply == QMessageBox.Yes else None 63 | 64 | def crash_game_page_runner_and_unlock(self, info_str): 65 | self.ui.gamepage.set_running_state(0) 66 | self.emit_info(format('中断处理', '=^76')) 67 | self.ui.gamepage.info_text_edit.append(format('中断处理', '=^76')) 68 | raise Exception(info_str) 69 | 70 | def kirikiri_check_resolution(self): 71 | input_folder = Path(self.ui.gamepage.select_input_folder_line_edit.text().strip()) 72 | try: 73 | _kirikiri = Kirikiri() 74 | scwidth, scheight, encoding = _kirikiri.get_resolution_encoding(input_folder) 75 | self.ui.gamepage.kirikiri.set_game_resolution_encoding(scwidth, scheight, encoding) 76 | except: 77 | warn_msg = QMessageBox() 78 | reply = warn_msg.warning(self.ui, '提示', '未能找到游戏分辨率和主要编码格式!', QMessageBox.Yes) 79 | 80 | def artemis_check_resolution(self): 81 | input_folder = Path(self.ui.gamepage.select_input_folder_line_edit.text().strip()) 82 | try: 83 | _artemis = Artemis() 84 | scwidth, scheight, encoding = _artemis.get_resolution_encoding(input_folder) 85 | self.ui.gamepage.artemis.set_game_resolution_encoding(scwidth, scheight, encoding) 86 | except: 87 | warn_msg = QMessageBox() 88 | reply = warn_msg.warning(self.ui, '提示', '未能找到游戏分辨率和主要编码格式!', QMessageBox.Yes) 89 | 90 | 91 | class GamePageRunner(QThread): 92 | """用于处理高清重制等耗时任务""" 93 | 94 | # 开始信号 95 | start_sig = Signal() 96 | # 信息文本框 97 | info_sig = Signal(str) 98 | # 进度(0-100) 99 | progress_sig = Signal(int, int) 100 | # 结束弹窗信息 101 | finish_sig = Signal(str) 102 | # 崩溃错误信息 103 | crash_sig = Signal(str) 104 | # 检查路径提示等 105 | tip_sig = Signal(str) 106 | 107 | def __init__(self, vnu): 108 | QThread.__init__(self) 109 | self.vnu = vnu 110 | 111 | def path_pass(self, only_folder=True, check_output=True, warn_kwd=None) -> bool: 112 | """ 113 | @brief 路径检查,发送提示 114 | 115 | @param only_folder 输入路径只能是文件夹 116 | @param check_output 检查输出文件夹 117 | @param warn_kwd 禁止字样 118 | 119 | @return Bool 120 | """ 121 | warn_message = None 122 | if not self.vnu.input_path.exists(): 123 | warn_message = '输入路径不存在!' 124 | if self.vnu.input_path == Path('./'): 125 | warn_message = '输入路径不能与工作目录相同!' 126 | if only_folder: 127 | if not self.vnu.input_path.is_dir(): 128 | warn_message = '输入路径需要是文件夹!' 129 | if check_output: 130 | if self.vnu.output_folder == Path('./'): 131 | warn_message = '输出路径不能与工作目录相同!' 132 | if self.vnu.input_path == self.vnu.output_folder: 133 | warn_message = '输入路径和输出路径不能相同!' 134 | if warn_kwd is not None: 135 | parent_names = [self.vnu.input_path.name] 136 | if self.vnu.input_path.is_dir(): 137 | parent_names += self.vnu.input_path.parent_names 138 | for parent_name in parent_names: 139 | if warn_kwd in parent_name: 140 | warn_message = f'输入路径及其上级目录不能含有{warn_kwd}字样!' 141 | break 142 | if warn_message is not None: 143 | self.tip_sig.emit(warn_message) 144 | return False 145 | else: 146 | # 通过时触发开始信号 147 | self.start_sig.emit() 148 | if check_output: 149 | if not self.vnu.output_folder.exists(): 150 | self.vnu.output_folder.mkdir(parents=True) 151 | return True 152 | 153 | def run(self): 154 | try: 155 | # Kirikiri 156 | if self.vnu.ui.gamepage.game_engine_area.currentWidget() is self.vnu.ui.gamepage.kirikiri: 157 | self.kirikiri_run() 158 | # Artemis 159 | elif self.vnu.ui.gamepage.game_engine_area.currentWidget() is self.vnu.ui.gamepage.artemis: 160 | self.artemis_run() 161 | # Majiro 162 | elif self.vnu.ui.gamepage.game_engine_area.currentWidget() is self.vnu.ui.gamepage.majiro: 163 | self.majiro_run() 164 | except Exception as e: 165 | self.crash_sig.emit(traceback.format_exc()) 166 | 167 | def kirikiri_run(self): 168 | kirikiri = Kirikiri(self) 169 | # 给ui起个别名 170 | ugk = self.vnu.ui.gamepage.kirikiri 171 | if ugk.currentWidget() is ugk.hd_parts_frame: 172 | if self.path_pass(warn_kwd='patch'): 173 | # 设置放大部分 174 | run_dict = {} 175 | run_dict['script'] = ugk.text_part.isChecked() 176 | run_dict['image'] = ugk.image_part.isChecked() 177 | run_dict['animation'] = ugk.animation_part.isChecked() 178 | run_dict['video'] = ugk.video_part.isChecked() 179 | # 开始放大 180 | kirikiri.upscale( 181 | game_data=self.vnu.input_path, 182 | patch_folder=self.vnu.output_folder, 183 | scale_ratio=ugk.custiom_ratio_spinbox.value(), 184 | run_dict=run_dict, 185 | encoding=ugk.main_encoding.text(), 186 | resolution=[int(i) for i in ugk.before_resolution.text().split('x')], 187 | # 高级选项 188 | upscale_fg=True if ugk.upscale_fg_btn.isChecked() else False 189 | ) 190 | self.finish_sig.emit('高清重制完成!') 191 | elif ugk.currentWidget() is ugk.work_up_frame: 192 | # 对话框头像坐标调整 193 | if ugk.stand_crt_btn.isChecked(): 194 | if self.path_pass(): 195 | face_zoom = ugk.crt_ratio.value() 196 | xpos_move = ugk.crt_movex.value() 197 | kirikiri.stand_correction(self.vnu.input_path, self.vnu.output_folder, face_zoom, xpos_move) 198 | self.finish_sig.emit('对话框头像坐标调整完成!') 199 | # scn格式转换 200 | elif ugk.scn_cvt_btn.isChecked(): 201 | if self.path_pass(only_folder=False): 202 | input_format = ugk.scn_in.currentText() 203 | output_format = ugk.scn_out.currentText() 204 | if input_format == 'scn': 205 | kirikiri.scn_de_batch(self.vnu.input_path, self.vnu.output_folder) 206 | elif input_format == 'json': 207 | kirikiri.scn_en_batch(self.vnu.input_path, self.vnu.output_folder) 208 | self.finish_sig.emit('scn转换完成!') 209 | # tlg图片格式转换 210 | elif ugk.tlg_convert_btn.isChecked(): 211 | if self.path_pass(only_folder=False): 212 | input_format = ugk.tlg_in.currentText() 213 | output_format = ugk.tlg_out.currentText() 214 | if input_format == 'tlg': 215 | if output_format == 'png': 216 | kirikiri.tlg2png_batch(self.vnu.input_path, self.vnu.output_folder) 217 | else: 218 | tlg5_mode = False if output_format == 'tlg6' else True 219 | kirikiri.tlg2tlg_batch(self.vnu.input_path, self.vnu.output_folder, tlg5_mode) 220 | elif input_format == 'png': 221 | tlg5_mode = False if output_format == 'tlg6' else True 222 | kirikiri.png2tlg_batch(self.vnu.input_path, self.vnu.output_folder, tlg5_mode) 223 | self.finish_sig.emit('tlg图片转换完成!') 224 | # pimg格式转换 225 | elif ugk.pimg_cvt_btn.isChecked(): 226 | if self.path_pass(only_folder=False): 227 | input_format = ugk.pimg_in.currentText() 228 | output_format = ugk.pimg_out.currentText() 229 | if input_format == 'pimg': 230 | kirikiri.pimg_de_batch(self.vnu.input_path, self.vnu.output_folder) 231 | elif input_format == 'json&png': 232 | kirikiri.pimg_en_batch(self.vnu.input_path, self.vnu.output_folder) 233 | self.finish_sig.emit('pimg转换完成!') 234 | # amv动画格式转换 235 | elif ugk.amv_cvt_btn.isChecked(): 236 | if self.path_pass(only_folder=False): 237 | input_format = ugk.amv_in.currentText() 238 | output_format = ugk.amv_out.currentText() 239 | if input_format == 'amv' and output_format == 'json&png': 240 | kirikiri.amv2png_batch(self.vnu.input_path, self.vnu.output_folder) 241 | elif input_format == 'json&png' and output_format == 'amv': 242 | kirikiri.png2amv_batch(self.vnu.input_path, self.vnu.output_folder) 243 | self.finish_sig.emit('amv转换完成!') 244 | elif ugk.flat_patch_btn.isChecked(): 245 | if self.path_pass(warn_kwd='patch'): 246 | kirikiri.flat_kirikiri_patch_folder(self.vnu.input_path, self.vnu.output_folder) 247 | self.finish_sig.emit('补丁文件平铺完成!') 248 | 249 | def artemis_run(self): 250 | artemis = Artemis(self) 251 | # 给ui起个别名 252 | uga = self.vnu.ui.gamepage.artemis 253 | if uga.currentWidget() is uga.hd_parts_frame: 254 | if self.path_pass(): 255 | # 设置放大部分 256 | run_dict = {} 257 | run_dict['script'] = uga.text_part.isChecked() 258 | run_dict['image'] = uga.image_part.isChecked() 259 | run_dict['animation'] = uga.animation_part.isChecked() 260 | run_dict['video'] = uga.video_part.isChecked() 261 | artemis.upscale( 262 | game_data=self.vnu.input_path, 263 | patch_folder=self.vnu.output_folder, 264 | scale_ratio=uga.custiom_ratio_spinbox.value(), 265 | run_dict=run_dict, 266 | encoding=uga.main_encoding.text(), 267 | resolution=[int(i) for i in uga.before_resolution.text().split('x')] 268 | ) 269 | self.finish_sig.emit('高清重制完成!') 270 | elif uga.currentWidget() is uga.work_up_frame: 271 | if uga.pfs_unpack_btn.isChecked(): 272 | if self.path_pass(only_folder=False): 273 | pfs_encoding = uga.pfs_encoding_line_edit.text().strip() 274 | artemis.batch_extract_pfs(self.vnu.input_path, self.vnu.output_folder, pfs_encoding) 275 | self.finish_sig.emit(f'拆包完成!请把游戏目录中类似script、movie等文件夹及*.ini文件也复制到:\n{self.vnu.output_folder}中') 276 | 277 | def majiro_run(self): 278 | majiro = Majiro(self) 279 | # 给ui起个别名 280 | ugm = self.vnu.ui.gamepage.majiro 281 | if ugm.currentWidget() is ugm.hd_parts_frame: 282 | if self.path_pass(): 283 | # 设置放大部分 284 | run_dict = {} 285 | run_dict['script'] = ugm.text_part.isChecked() 286 | run_dict['image'] = ugm.image_part.isChecked() 287 | run_dict['video'] = ugm.video_part.isChecked() 288 | majiro.upscale( 289 | game_data=self.vnu.input_path, 290 | patch_folder=self.vnu.output_folder, 291 | scale_ratio=ugm.custiom_ratio_spinbox.value(), 292 | run_dict=run_dict, 293 | encoding=ugm.main_encoding.text(), 294 | resolution=[int(i) for i in ugm.before_resolution.text().split('x')] 295 | ) 296 | self.finish_sig.emit('高清重制完成!') 297 | elif ugm.currentWidget() is ugm.work_up_frame: 298 | if ugm.mjo_convert_btn.isChecked(): 299 | if self.path_pass(only_folder=False): 300 | input_format = ugm.mjo_in.currentText() 301 | output_format = ugm.mjo_out.currentText() 302 | if input_format == 'mjo' and output_format == 'mjil&mjres': 303 | majiro.mjo_de_batch(self.vnu.input_path, self.vnu.output_folder) 304 | elif input_format == 'mjil&mjres' and output_format == 'mjo': 305 | majiro.mjo_en_batch(self.vnu.input_path, self.vnu.output_folder) 306 | self.finish_sig.emit('mjo转换完成!') 307 | -------------------------------------------------------------------------------- /Icons/book-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Icons/clock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Icons/columns.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hokejyo/VisualNovelUpscaler/d755913eb72f739ad4faea70e689cf933ba54c7f/Icons/icon.ico -------------------------------------------------------------------------------- /Icons/icon_close.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Icons/icon_folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Icons/icon_folder_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Icons/icon_maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Icons/icon_minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Icons/icon_send.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 36 | 37 | 57 | 59 | 60 | 62 | image/svg+xml 63 | 65 | 66 | 67 | 68 | 69 | 73 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Icons/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Icons/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Icons/slack.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ImagePageUIConnection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from Core import * 4 | from GUI import * 5 | 6 | 7 | class ImagePageUIConnection(object): 8 | 9 | def __init__(self): 10 | self.ui.imagepage.run_btn.clicked.connect(self.image_page_run) 11 | 12 | def image_page_run(self): 13 | self.input_path = Path(self.ui.imagepage.input_line_edit.text().strip()) 14 | self.output_folder = Path(self.ui.imagepage.output_line_edit.text().strip()) 15 | if self.check_image_page_in_out_folder(self.input_path, self.output_folder): 16 | self.image_page_runner = ImagePageRunner(self) 17 | # 信号绑定 18 | self.image_page_runner.start_sig.connect(self.start_image_page_runner_and_lock) 19 | self.image_page_runner.progress_sig.connect(self.update_image_progress_bar) 20 | self.image_page_runner.finish_sig.connect(self.finish_image_page_runner_and_unlock) 21 | self.image_page_runner.crash_sig.connect(self.crash_image_page_runner_and_unlock) 22 | self.image_page_runner.start() 23 | 24 | def check_image_page_in_out_folder(self, input_path, output_folder) -> bool: 25 | input_path = Path(input_path) 26 | output_folder = Path(output_folder) 27 | warn_message = None 28 | if not input_path.exists(): 29 | warn_message = '输入路径不存在' 30 | if input_path == Path('./'): 31 | warn_message = '输入路径不能与工作目录相同' 32 | if output_folder == Path('./'): 33 | warn_message = '输出路径不能与工作目录相同' 34 | if warn_message is not None: 35 | warn_msg = QMessageBox() 36 | reply = warn_msg.warning(self.ui, '提示', warn_message + '!', QMessageBox.Yes) 37 | return False 38 | else: 39 | if not output_folder.exists(): 40 | output_folder.mkdir(parents=True) 41 | return True 42 | 43 | def start_image_page_runner_and_lock(self): 44 | # 开始时锁定,防止重复操作 45 | self.ui.imagepage.set_running_state(1) 46 | # 清空历史信息 47 | self.emit_info(format('开始处理', '=^76')) 48 | 49 | def update_image_progress_bar(self, _percent, _left_time): 50 | self.ui.imagepage.status_progress_bar.setValue(_percent) 51 | left_time_str = seconds_format(_left_time) 52 | self.ui.imagepage.run_btn.setText(left_time_str) 53 | if _percent == 100: 54 | self.ui.imagepage.set_running_state(1) 55 | 56 | def finish_image_page_runner_and_unlock(self, info_str=''): 57 | self.ui.imagepage.set_running_state(2) 58 | self.emit_info(format('结束处理', '=^76')) 59 | finish_info_msg = QMessageBox() 60 | reply = finish_info_msg.information(self.ui, '处理完成', f'{info_str}\n是否打开输出文件夹?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) 61 | show_folder(self.output_folder) if reply == QMessageBox.Yes else None 62 | # os.system(f'start {self.output_folder}') if reply == QMessageBox.Yes else None 63 | 64 | def crash_image_page_runner_and_unlock(self, info_str): 65 | self.ui.imagepage.set_running_state(0) 66 | self.emit_info(format('中断处理', '=^76')) 67 | raise Exception(info_str) 68 | 69 | 70 | class ImagePageRunner(QThread): 71 | 72 | # 开始信号 73 | start_sig = Signal() 74 | # 进度(0-100) 75 | progress_sig = Signal(int, int) 76 | # 结束弹窗信息 77 | finish_sig = Signal(str) 78 | # 崩溃错误信息 79 | crash_sig = Signal(str) 80 | 81 | def __init__(self, vnu): 82 | QThread.__init__(self) 83 | self.vnu = vnu 84 | 85 | def run(self): 86 | self.start_sig.emit() 87 | try: 88 | image_upscaler = ImageUpscaler(self) 89 | input_path = self.vnu.input_path 90 | output_folder = self.vnu.output_folder 91 | scale_ratio = float(self.vnu.ui.imagepage.custiom_ratio_spinbox.value()) 92 | output_extention = self.vnu.ui.imagepage.output_extention_line_edit.text().strip().lower() 93 | filters = [extension.strip().lower() for extension in self.vnu.ui.imagepage.filter_line_edit.text().split(',')] 94 | walk_mode = True if self.vnu.ui.imagepage.ignr_btn.isChecked() else False 95 | stem_sfx = self.vnu.ui.imagepage.suffix_line_edit.text().strip() 96 | rename_sfx = None if stem_sfx == '' else stem_sfx 97 | image_upscaler.image_upscale(input_path=input_path, 98 | output_folder=output_folder, 99 | scale_ratio=scale_ratio, 100 | output_extention=output_extention, 101 | filters=filters, 102 | walk_mode=walk_mode, 103 | rename_sfx=rename_sfx 104 | ) 105 | self.finish_sig.emit('图片放大完成!') 106 | except Exception as e: 107 | self.crash_sig.emit(traceback.format_exc()) 108 | 109 | 110 | class ImageUpscaler(Core): 111 | 112 | def __init__(self, image_ui_runner): 113 | Core.__init__(self) 114 | self.load_config() 115 | self.__class__.image_ui_runner = image_ui_runner 116 | 117 | def emit_progress(self, _percent, _left_time): 118 | print(_percent, _left_time, sep='\t') 119 | self.image_ui_runner.progress_sig.emit(_percent, _left_time) 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visual Novel Upscaler 2 | 一键将视觉小说/Galgame以高分辨率重制,现已支持Kirikiri2/Z及Artemis引擎 3 | ## 使用方法 4 | 1.下载并解压到纯英文目录 5 | 2.拆包游戏,放到一个文件夹中 6 | 3.打开程序,指定输入目录和输出目录运行 7 | ## Credits 8 | [waifu2x-ncnn-vulkan](https://github.com/nihui/waifu2x-ncnn-vulkan) 9 | [Real-CUGAN](https://github.com/nihui/realcugan-ncnn-vulkan) 10 | [Real-ESRGAN](https://github.com/xinntao/Real-ESRGAN) 11 | [SRMD ncnn Vulkan](https://github.com/nihui/srmd-ncnn-vulkan) 12 | [RealSR ncnn Vulkan](https://github.com/nihui/realsr-ncnn-vulkan) 13 | [Anime4KCPP](https://github.com/TianZerL/Anime4KCPP) 14 | [PySide6](https://wiki.qt.io/Qt_for_Python) 15 | [FFmpeg](https://github.com/FFmpeg/FFmpeg) 16 | [FreeMote](https://github.com/UlyssesWu/FreeMote) 17 | [tlg2png](https://github.com/vn-tools/tlg2png) 18 | [krkr2](https://github.com/krkrz/krkr2) 19 | [png2tlg](https://github.com/zhiyb/png2tlg) 20 | [AlphaMovieDecoder](https://github.com/xmoeproject/AlphaMovieDecoder) 21 | [AlphaMovieEncoder](https://github.com/zhiyb/AlphaMovieEncoder) 22 | [MajiroTools](https://github.com/AtomCrafty/MajiroTools) 23 | [pypng](https://github.com/drj11/pypng) 24 | -------------------------------------------------------------------------------- /SettingPageUIConnection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from Core import * 4 | from GUI import * 5 | 6 | 7 | class SettingPageUIConnection(object): 8 | 9 | def __init__(self): 10 | self.ui_setting_connections() 11 | self.ui_config_load() 12 | self.ui.settingpage.sr_engine_combobox.setCurrentIndex(0) 13 | 14 | def ui_setting_connections(self): 15 | # 配置页面应用 16 | self.ui.settingpage.save_btn.clicked.connect(self.ui_config_save) 17 | # 配置页面取消 18 | self.ui.settingpage.cancle_btn.clicked.connect(self.ui_config_load) 19 | # 配置页面重置 20 | self.ui.settingpage.reset_btn.clicked.connect(self.ui_config_reset) 21 | 22 | def ui_config_load(self): 23 | # 载入配置文件 24 | try: 25 | self.load_config() 26 | except: 27 | config_reset_message = QMessageBox() 28 | reply = config_reset_message.question(self.ui, '配置文件未正确配置', '是否重置配置文件?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) 29 | sys.exit() if reply == QMessageBox.No else self.reset_config() 30 | # cpu核数 31 | self.ui.settingpage.cpu_spinbox.setValue(self.cpu_cores) 32 | # 显卡型号 33 | self.ui.settingpage.gpu_combobox.setCurrentIndex(int(self.gpu_id)) 34 | # 文本编码列表 35 | self.ui.settingpage.text_encoding_line_edit.setText(','.join(self.encoding_list)) 36 | # 图片超分引擎 37 | self.ui.settingpage.image_sr_engine_combobox.setCurrentText(self.image_sr_engine) 38 | # 图片单次批量 39 | self.ui.settingpage.image_batch_size_spinbox.setValue(self.image_batch_size) 40 | # 视频超分引擎 41 | self.ui.settingpage.video_sr_engine_combobox.setCurrentText(self.video_sr_engine) 42 | # 视频单次批量 43 | self.ui.settingpage.video_batch_size_spinbox.setValue(self.video_batch_size) 44 | # 输出视频质量 45 | self.ui.settingpage.video_quality_spinbox.setValue(int(self.video_quality)) 46 | # TTA模式 47 | tta_bool = False if self.tta == '0' else True 48 | self.ui.settingpage.tta_checkbox.setChecked(tta_bool) 49 | # waifu2x_ncnn 50 | self.ui.settingpage.waifu2x_ncnn_settings.noise_level_spinbox.setValue(int(self.waifu2x_ncnn_noise_level)) 51 | self.ui.settingpage.waifu2x_ncnn_settings.tile_size_line_edit.setText(self.waifu2x_ncnn_tile_size) 52 | self.ui.settingpage.waifu2x_ncnn_settings.modle_name_combobox.setCurrentText(self.waifu2x_ncnn_model_name) 53 | self.ui.settingpage.waifu2x_ncnn_settings.load_proc_save_line_edit.setText(self.waifu2x_ncnn_load_proc_save) 54 | # real_cugan 55 | self.ui.settingpage.real_cugan_settings.noise_level_spinbox.setValue(int(self.real_cugan_noise_level)) 56 | self.ui.settingpage.real_cugan_settings.tile_size_line_edit.setText(self.real_cugan_tile_size) 57 | self.ui.settingpage.real_cugan_settings.sync_gap_mode_spinbox.setValue(int(self.real_cugan_sync_gap_mode)) 58 | self.ui.settingpage.real_cugan_settings.modle_name_combobox.setCurrentText(self.real_cugan_model_name) 59 | self.ui.settingpage.real_cugan_settings.load_proc_save_line_edit.setText(self.real_cugan_load_proc_save) 60 | # real_esrgan 61 | self.ui.settingpage.real_esrgan_settings.tile_size_line_edit.setText(self.real_esrgan_tile_size) 62 | self.ui.settingpage.real_esrgan_settings.modle_name_combobox.setCurrentText(self.real_esrgan_model_name) 63 | self.ui.settingpage.real_esrgan_settings.load_proc_save_line_edit.setText(self.real_esrgan_load_proc_save) 64 | # srmd_ncnn 65 | self.ui.settingpage.srmd_ncnn_settings.noise_level_spinbox.setValue(int(self.srmd_ncnn_noise_level)) 66 | self.ui.settingpage.srmd_ncnn_settings.tile_size_line_edit.setText(self.srmd_ncnn_tile_size) 67 | self.ui.settingpage.srmd_ncnn_settings.load_proc_save_line_edit.setText(self.srmd_ncnn_load_proc_save) 68 | # realsr_ncnn 69 | self.ui.settingpage.realsr_ncnn_settings.tile_size_line_edit.setText(self.realsr_ncnn_tile_size) 70 | self.ui.settingpage.realsr_ncnn_settings.modle_name_combobox.setCurrentText(self.realsr_ncnn_model_name) 71 | self.ui.settingpage.realsr_ncnn_settings.load_proc_save_line_edit.setText(self.realsr_ncnn_load_proc_save) 72 | # anime4kcpp 73 | acnet_bool = False if self.anime4k_acnet == '0' else True 74 | self.ui.settingpage.anime4k_settings.acnet_checkbox.setChecked(acnet_bool) 75 | hdn_mode_bool = False if self.anime4k_hdn_mode == '0' else True 76 | self.ui.settingpage.anime4k_settings.hdn_mode_checkbox.setChecked(hdn_mode_bool) 77 | self.ui.settingpage.anime4k_settings.hdn_level_spinbox.setValue(int(self.anime4k_hdn_level)) 78 | 79 | def ui_config_save(self): 80 | with open(self.vnu_config_file, 'w', newline='', encoding='utf-8') as vcf: 81 | # 通用设置 82 | self.vnu_config.set('General', 'cpu_cores', str(self.ui.settingpage.cpu_spinbox.value())) 83 | self.vnu_config.set('General', 'gpu_id', get_gpu_id(self.ui.settingpage.gpu_combobox.currentText())) 84 | self.vnu_config.set('General', 'encoding_list', self.ui.settingpage.text_encoding_line_edit.text().strip()) 85 | # 图片设置 86 | self.vnu_config.set('Image', 'image_sr_engine', self.ui.settingpage.image_sr_engine_combobox.currentText()) 87 | self.vnu_config.set('Image', 'image_batch_size', str(self.ui.settingpage.image_batch_size_spinbox.value())) 88 | # 视频设置 89 | self.vnu_config.set('Video', 'video_sr_engine', self.ui.settingpage.video_sr_engine_combobox.currentText()) 90 | self.vnu_config.set('Video', 'video_batch_size', str(self.ui.settingpage.video_batch_size_spinbox.value())) 91 | self.vnu_config.set('Video', 'video_quality', str(self.ui.settingpage.video_quality_spinbox.value())) 92 | # 超分引擎设置 93 | tta = '1' if self.ui.settingpage.tta_checkbox.isChecked() else '0' 94 | self.vnu_config.set('SREngine', 'tta', tta) 95 | # waifu2x_ncnn 96 | self.vnu_config.set('waifu2x_ncnn', 'noise_level', str(self.ui.settingpage.waifu2x_ncnn_settings.noise_level_spinbox.value())) 97 | self.vnu_config.set('waifu2x_ncnn', 'tile_size', self.ui.settingpage.waifu2x_ncnn_settings.tile_size_line_edit.text()) 98 | self.vnu_config.set('waifu2x_ncnn', 'model_name', self.ui.settingpage.waifu2x_ncnn_settings.modle_name_combobox.currentText()) 99 | self.vnu_config.set('waifu2x_ncnn', 'load_proc_save', self.ui.settingpage.waifu2x_ncnn_settings.load_proc_save_line_edit.text()) 100 | # Real-CUGAN 101 | self.vnu_config.set('real_cugan', 'noise_level', str(self.ui.settingpage.real_cugan_settings.noise_level_spinbox.value())) 102 | self.vnu_config.set('real_cugan', 'tile_size', self.ui.settingpage.real_cugan_settings.tile_size_line_edit.text()) 103 | self.vnu_config.set('real_cugan', 'sync_gap_mode', str(self.ui.settingpage.real_cugan_settings.sync_gap_mode_spinbox.value())) 104 | self.vnu_config.set('real_cugan', 'model_name', self.ui.settingpage.real_cugan_settings.modle_name_combobox.currentText()) 105 | self.vnu_config.set('real_cugan', 'load_proc_save', self.ui.settingpage.real_cugan_settings.load_proc_save_line_edit.text()) 106 | # real_esrgan 107 | self.vnu_config.set('real_esrgan', 'tile_size', self.ui.settingpage.real_esrgan_settings.tile_size_line_edit.text()) 108 | self.vnu_config.set('real_esrgan', 'model_name', self.ui.settingpage.real_esrgan_settings.modle_name_combobox.currentText()) 109 | self.vnu_config.set('real_esrgan', 'load_proc_save', self.ui.settingpage.real_esrgan_settings.load_proc_save_line_edit.text()) 110 | # srmd_ncnn 111 | self.vnu_config.set('srmd_ncnn', 'noise_level', str(self.ui.settingpage.srmd_ncnn_settings.noise_level_spinbox.value())) 112 | self.vnu_config.set('srmd_ncnn', 'tile_size', self.ui.settingpage.srmd_ncnn_settings.tile_size_line_edit.text()) 113 | self.vnu_config.set('srmd_ncnn', 'load_proc_save', self.ui.settingpage.srmd_ncnn_settings.load_proc_save_line_edit.text()) 114 | # realsr_ncnn 115 | self.vnu_config.set('realsr_ncnn', 'tile_size', self.ui.settingpage.realsr_ncnn_settings.tile_size_line_edit.text()) 116 | self.vnu_config.set('realsr_ncnn', 'model_name', self.ui.settingpage.realsr_ncnn_settings.modle_name_combobox.currentText()) 117 | self.vnu_config.set('realsr_ncnn', 'load_proc_save', self.ui.settingpage.realsr_ncnn_settings.load_proc_save_line_edit.text()) 118 | # anime4kcpp 119 | acnet = '1' if self.ui.settingpage.anime4k_settings.acnet_checkbox.isChecked() else '0' 120 | self.vnu_config.set('anime4k', 'acnet', acnet) 121 | hdn_mode = '1' if self.ui.settingpage.anime4k_settings.hdn_mode_checkbox.isChecked() else '0' 122 | self.vnu_config.set('anime4k', 'hdn_mode', hdn_mode) 123 | self.vnu_config.set('anime4k', 'hdn_level', str(self.ui.settingpage.anime4k_settings.hdn_level_spinbox.value())) 124 | self.vnu_config.write(vcf) 125 | self.ui_config_load() 126 | 127 | def ui_config_reset(self): 128 | config_reset_message = QMessageBox() 129 | reply = config_reset_message.question(self.ui, '重置配置文件', '是否重置配置文件?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) 130 | if reply == QMessageBox.Yes: 131 | self.reset_config() 132 | self.ui_config_load() 133 | self.ui.settingpage.sr_engine_combobox.setCurrentIndex(0) 134 | -------------------------------------------------------------------------------- /VNEngines/Artemis/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /VNEngines/Artemis/artemis.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from ..upscaler import * 4 | from .pf8_struct import PF8Struct 5 | 6 | 7 | class Artemis(Upscaler): 8 | """Artemis Engine""" 9 | 10 | def __init__(self, game_ui_runner=None): 11 | Upscaler.__init__(self, game_ui_runner) 12 | self.encoding = 'UTF-8' 13 | 14 | def get_resolution_encoding(self, input_folder): 15 | ''' 16 | 获取文本编码和分辨率 17 | ''' 18 | input_folder = Path(input_folder) 19 | for ini_file in input_folder.file_list('ini'): 20 | if ini_file.name == 'system.ini': 21 | encoding = self.get_encoding(ini_file) 22 | with open(ini_file, newline='', encoding=encoding) as f: 23 | lines = f.readlines() 24 | pattern = re.compile(r'(WIDTH|HEIGHT|CHARSET)\W+([A-Za-z0-9]+-?[A-Za-z0-9]+).*') 25 | for line in lines: 26 | if line.startswith('WIDTH'): 27 | scwidth = int(re.match(pattern, line).group(2)) 28 | if line.startswith('HEIGHT'): 29 | scheight = int(re.match(pattern, line).group(2)) 30 | if line.startswith('CHARSET'): 31 | encoding = re.match(pattern, line).group(2) 32 | if line.startswith('[ANDROID]'): 33 | break 34 | break 35 | return scwidth, scheight, encoding 36 | 37 | """ 38 | ================================================== 39 | Artemis引擎脚本文件:ini, tbl, lua, iet, ipt, ast 40 | ================================================== 41 | """ 42 | 43 | def _script2x(self): 44 | self.emit_info('开始处理游戏脚本......') 45 | self.sysini2x() 46 | self.tbl2x() 47 | self.ipt2x() 48 | self.ast2x() 49 | self.lua2x() 50 | 51 | def sysini2x(self): 52 | ''' 53 | 游戏分辨率,存档位置修改 54 | ''' 55 | sys_ini_file_ls = [ini_file for ini_file in self.game_data.file_list('ini') if ini_file.name == 'system.ini'] 56 | for ini_file in sys_ini_file_ls: 57 | pattern1 = re.compile(r'(WIDTH|HEIGHT)(\W+)(\d+)(.*)') 58 | result = [] 59 | lines, current_encoding = self.get_lines_encoding(ini_file) 60 | for line in lines: 61 | re_result = re.match(pattern1, line) 62 | if re_result: 63 | line = self.line_pattern_num2x(re_result) 64 | result.append(line) 65 | with open(self.a2p(ini_file), 'w', newline='', encoding=current_encoding) as f: 66 | _save_change = True 67 | pattern2 = re.compile(r'^(;?)(SAVEPATH.*)') 68 | for line in result: 69 | re_result = re.match(pattern2, line) 70 | if re_result: 71 | if re_result.group(1): 72 | if _save_change: 73 | line = 'SAVEPATH = savedataHD\r\n' 74 | _save_change = False 75 | else: 76 | line = ';' + line 77 | f.write(line) 78 | 79 | # def tbl2x(self): 80 | # for tbl_file in self.game_data.file_list('tbl'): 81 | # if tbl_file.name.startswith('list_windows_'): 82 | # self.windows_xx_tbl2x(tbl_file) 83 | # if tbl_file.name == 'list_windows.tbl': 84 | # self.windwos_tbl2x(tbl_file) 85 | 86 | def tbl2x(self): 87 | tbl_file_ls = self.game_data.file_list('tbl') 88 | self.pool_run(self.windwos_tbl2x, tbl_file_ls) 89 | 90 | def _tbl_file2x(self,tbl_file): 91 | ls1 = ['game_scale', 'game_wasmbar', 'fontsize', 'line_size', 'line_window', 'line_back', 'line_scroll', 'line_name01', 'line_name02'] 92 | pattern_rule1 = '(' + keyn1 + r'\W+?\{' + ')' + '(.*?)' + r'(\}.*)' 93 | 94 | def windwos_tbl2x(self, tbl_file): 95 | ''' 96 | 主要是ui修正,游戏窗口,立绘定位 97 | ''' 98 | result = [] 99 | lines, current_encoding = self.get_lines_encoding(tbl_file) 100 | for line in lines: 101 | ls1 = ['game_scale', 'game_wasmbar', 'fontsize', 'line_size', 'line_window', 'line_back', 'line_scroll', 'line_name01', 'line_name02'] 102 | # ls1 = ['game_scale', 'game_wasmbar', 'title_anime', 'fontsize', 'line_size', 'line_window', 'line_back', 'line_scroll', 'line_name01', 'line_name02'] 103 | for keyn1 in ls1: 104 | if line.startswith(keyn1): 105 | pattern_rule1 = '(' + keyn1 + r'\W+?\{' + ')' + '(.*?)' + r'(\}.*)' 106 | pattern1 = re.compile(pattern_rule1) 107 | re_result1 = re.match(pattern1, line) 108 | if re_result1: 109 | line_ls = list(re_result1.groups()) 110 | tmp_ls = line_ls[1].split(',') 111 | for i in range(len(tmp_ls)): 112 | if real_digit(tmp_ls[i]): 113 | tmp_ls[i] = str(int(int(tmp_ls[i]) * self.scale_ratio)) 114 | line_ls[1] = ','.join(tmp_ls) 115 | line = ''.join([i for i in line_ls if i != None]) 116 | ls2 = ['x', 'y', 'w', 'h', 'r', 'cx', 'cy', 'cw', 'ch', 'fx', 'fy', 'fw', 'fh', 'left', 'top', 'size', 'width', 'height', 'spacetop', 'spacemiddle', 'spacebottom', 'kerning', 'rubysize'] 117 | for keyn2 in ls2: 118 | pattern2 = re.compile(rf'(.*\W+{keyn2}\W+)(\d+)(.*)') 119 | re_result2 = re.match(pattern2, line) 120 | if re_result2: 121 | line = self.line_pattern_num2x(re_result2) 122 | ls3 = ['clip', 'clip_a', 'clip_c', 'clip_d'] 123 | for keyn3 in ls3: 124 | pattern3 = re.compile(rf'(.*\W+{keyn3}\W+?")(.*?)(".*)') 125 | re_result3 = re.match(pattern3, line) 126 | if re_result3: 127 | line_ls = list(re_result3.groups()) 128 | tmp_ls = line_ls[1].split(',') 129 | for i in range(len(tmp_ls)): 130 | if real_digit(tmp_ls[i]): 131 | tmp_ls[i] = str(int(int(tmp_ls[i]) * self.scale_ratio)) 132 | line_ls[1] = ','.join(tmp_ls) 133 | line = ''.join([i for i in line_ls if i != None]) 134 | ls4 = ['game_width', 'game_height'] 135 | for keyn4 in ls4: 136 | pattern4 = re.compile(rf'^({keyn4}\W+)(\d+)(.*)') 137 | re_result4 = re.match(pattern4, line) 138 | if re_result4: 139 | line = self.line_pattern_num2x(re_result4) 140 | result.append(line) 141 | with open(self.a2p(tbl_file), 'w', newline='', encoding=current_encoding) as f: 142 | for line in result: 143 | f.write(line) 144 | 145 | def ipt2x(self): 146 | ''' 147 | 粒子效果显示修正,部分游戏对话框修正 148 | ''' 149 | ipt_file_ls = self.game_data.file_list('ipt') 150 | for ipt_file in ipt_file_ls: 151 | result = [] 152 | lines, current_encoding = self.get_lines_encoding(ipt_file) 153 | for line in lines: 154 | keyn_ls = ['x', 'y', 'w', 'h', 'ax', 'ay'] 155 | for keyn in keyn_ls: 156 | pattern = re.compile(rf'(.*\W+{keyn}\W+)(\d+)(.*)') 157 | re_result = re.match(pattern, line) 158 | if re_result: 159 | line = self.line_pattern_num2x(re_result) 160 | pattern2 = re.compile(r'(.*\W+")(\d+.*?)(".*)') 161 | re_result2 = re.match(pattern2, line) 162 | if re_result2: 163 | line2ls = list(re_result2.groups()) 164 | num_str_ls = line2ls[1].split(',') 165 | for i, num_str in enumerate(num_str_ls): 166 | if real_digit(num_str): 167 | num_str_ls[i] = str(int(int(num_str) * self.scale_ratio)) 168 | line2ls[1] = ','.join(num_str_ls) 169 | line = ''.join(line2ls) 170 | result.append(line) 171 | with open(self.a2p(ipt_file), 'w', newline='', encoding=current_encoding) as f: 172 | for line in result: 173 | f.write(line) 174 | 175 | def ast2x(self): 176 | ast_file_ls = self.game_data.file_list('ast') 177 | self.pool_run(self.ast_file_2x, ast_file_ls) 178 | 179 | def ast_file_2x(self, ast_file): 180 | ''' 181 | 人物位置修正,剧本文件 182 | ''' 183 | # for ast_file in self.game_data.file_list('ast'): 184 | result = [] 185 | lines, current_encoding = self.get_lines_encoding(ast_file) 186 | for line in lines: 187 | keyn_ls = ['mx', 'my', 'ax', 'ay', 'bx', 'by', 'x', 'y', 'x2', 'y2'] 188 | for keyn in keyn_ls: 189 | pattern = re.compile(rf'(.*\W+{keyn}\W+?)(-?\d+)(.*)') 190 | re_result = re.match(pattern, line) 191 | if re_result: 192 | line = self.line_pattern_num2x(re_result) 193 | result.append(line) 194 | with open(self.a2p(ast_file), 'w', newline='', encoding=current_encoding) as f: 195 | for line in result: 196 | f.write(line) 197 | 198 | def lua2x(self): 199 | lua_file_ls = self.game_data.file_list('lua') 200 | self.pool_run(self.lua_file_2x, lua_file_ls) 201 | 202 | def lua_file_2x(self, lua_file): 203 | ''' 204 | 部分游戏音量值位置修正 205 | ''' 206 | # for lua_file in self.game_data.file_list('lua'): 207 | changed_sign = 0 208 | lines, current_encoding = self.get_lines_encoding(lua_file) 209 | result = [] 210 | keyn_ls = ['width', 'height', 'left', 'top', 'x', 'y'] 211 | for line in lines: 212 | for keyn in keyn_ls: 213 | pattern = re.compile(rf'(.*\W+{keyn}\W+)(\d+)(.*)') 214 | re_result = re.match(pattern, line) 215 | if re_result: 216 | changed_sign = 1 217 | line = self.line_pattern_num2x(re_result) 218 | result.append(line) 219 | if changed_sign == 1: 220 | with open(self.a2p(lua_file), 'w', newline='', encoding=current_encoding) as f: 221 | for line in result: 222 | f.write(line) 223 | 224 | """ 225 | ================================================== 226 | Artemis引擎图片文件:png(可能包含立绘坐标) 227 | ================================================== 228 | """ 229 | 230 | def _image2x(self): 231 | self.emit_info('开始处理游戏图片......') 232 | self.png2x() 233 | 234 | def png2x(self): 235 | png_file_ls = self.game_data.file_list('png') 236 | if png_file_ls: 237 | self.emit_info('正在放大png图片......') 238 | self.image_upscale(self.game_data, self.patch_folder, self.scale_ratio, 'png') 239 | self.emit_info('正在将立绘坐标信息写入到png图片') 240 | png_text_dict = self.get_all_png_text() 241 | for png_file, png_text in png_text_dict.items(): 242 | self.write_png_text_(png_file, png_text) 243 | 244 | def get_all_png_text(self) -> dict: 245 | ''' 246 | 将含有文本信息的png图片中的坐标放大并保存 247 | ''' 248 | png_text_dict = {} 249 | text_png_path_ls = self.game_data.file_list('png') 250 | for png_file in text_png_path_ls: 251 | # 放大后的png图片在临时文件夹中的路径 252 | scaled_png_path = self.a2p(png_file) 253 | # 获取原始图片中的png坐标信息 254 | png_text = self.read_png_text(png_file) 255 | if png_text is not None: 256 | scaled_png_text = self.png_text_2x(png_text, self.scale_ratio) 257 | png_text_dict[scaled_png_path] = scaled_png_text 258 | return png_text_dict 259 | 260 | """ 261 | ================================================== 262 | Artemis引擎动画文件:ogv 263 | ================================================== 264 | """ 265 | 266 | def _animation2x(self): 267 | self.emit_info('开始处理游戏动画......') 268 | self.ogv2x() 269 | 270 | def ogv2x(self): 271 | ogv_file_ls = self.game_data.file_list('ogv') 272 | if ogv_file_ls: 273 | for ogv_file in ogv_file_ls: 274 | target_ogv = self.a2p(ogv_file) 275 | output_video = self.video_upscale(ogv_file, target_ogv, self.scale_ratio) 276 | self.emit_info(f'{output_video} saved!') 277 | 278 | """ 279 | ================================================== 280 | Artemis引擎视频文件:wmv2、dat(wmv3) 281 | ================================================== 282 | """ 283 | 284 | def _video2x(self): 285 | self.emit_info('开始处理游戏视频......') 286 | video_extension_ls = ['wmv', 'dat', 'mp4', 'avi', 'mpg', 'mkv'] 287 | for video_extension in video_extension_ls: 288 | video_file_ls = [video_file for video_file in self.game_data.file_list(video_extension) if self.video_info(video_file)] 289 | if video_file_ls: 290 | self.emit_info(f'{video_extension}视频放大中......') 291 | for video_file in video_file_ls: 292 | with tempfile.TemporaryDirectory() as tmp_folder: 293 | self.tmp_folder = Path(tmp_folder) 294 | self.emit_info(f'正在处理:{video_file}') 295 | tmp_video = video_file.copy_as(self.a2t(video_file)) 296 | if video_extension == 'dat': 297 | tmp_video = tmp_video.move_as(tmp_video.with_suffix('.wmv')) 298 | output_video = self.video_upscale(tmp_video, tmp_video, self.scale_ratio) 299 | if video_extension == 'dat': 300 | output_video = output_video.move_as(output_video.with_suffix('.dat')) 301 | target_video = output_video.move_as(self.t2p(output_video)) 302 | self.emit_info(f'{target_video} saved!') 303 | 304 | """ 305 | ================================================== 306 | Artemis引擎封包文件:pfs(pf8) 307 | ================================================== 308 | """ 309 | 310 | def batch_extract_pfs(self, input_path, output_folder, encoding='utf-8') -> list: 311 | """ 312 | @brief pfs批量解包 313 | 314 | @param input_path 输入文件夹 315 | @param output_folder 输出文件夹 316 | @param encoding 编码格式 317 | 318 | @return 输出文件列表 319 | """ 320 | # 计时 321 | start_time = time.time() 322 | input_path = Path(input_path) 323 | output_folder = Path(output_folder) 324 | output_file_ls = [] 325 | if input_path.is_dir(): 326 | pfs_file_ls = [str(pfs_file) for pfs_file in input_path.file_list(walk_mode=False) if pfs_file.readbs(3) == b'pf8'] 327 | pfs_file_ls.sort() 328 | for pfs_file in pfs_file_ls: 329 | self.emit_info(f'{pfs_file} extracting......') 330 | output_file_ls += self.extract_pfs(pfs_file, output_folder, encoding) 331 | else: 332 | output_file_ls = self.extract_pfs(input_path, output_folder, encoding) 333 | # 输出耗时 334 | timing_count = time.time() - start_time 335 | self.emit_info(f'耗时{seconds_format(timing_count)}!') 336 | return output_file_ls 337 | 338 | def extract_pfs(self, pfs_file, output_folder, encoding='UTF-8') -> list: 339 | pfs_file = Path(pfs_file) 340 | output_folder = Path(output_folder) 341 | pfs = PF8Struct.parse_file(pfs_file) 342 | digest = hashlib.sha1(pfs.hash_data).digest() 343 | name_data_dict = {} 344 | for entry in pfs.entries: 345 | file_path = output_folder/entry.file_name.decode(encoding) 346 | name_data_dict[file_path] = bytearray(entry.file_data) 347 | file_path_ls = self.pool_run(self._decrypt_pfs_and_save_file, name_data_dict.items(), digest) 348 | return file_path_ls 349 | 350 | def _decrypt_pfs_and_save_file(self, entry_name_data, digest) -> Path: 351 | file_path, contents = entry_name_data 352 | len_contents = len(contents) 353 | len_digest = len(digest) 354 | new_file_data = self._decrypt_pfs_contents(contents, digest, len_contents, len_digest) 355 | file_path.parent.mkdir(parents=True, exist_ok=True) 356 | with open(file_path, 'wb') as f: 357 | f.write(new_file_data) 358 | return file_path 359 | 360 | @jit(fastmath=True) 361 | def _decrypt_pfs_contents(self, contents, digest, len_contents, len_digest): 362 | for i in range(len_contents): 363 | contents[i] ^= digest[i % len_digest] 364 | return contents 365 | -------------------------------------------------------------------------------- /VNEngines/Artemis/pf8_struct.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from construct import * 4 | 5 | PF8Struct = Struct( 6 | 'header'/Struct( 7 | 'magic'/Const(b'pf8'), 8 | 'hash_data_size'/Int32ul, 9 | 'file_entry_cnt'/Int32ul 10 | ), 11 | 'entries'/Array( 12 | this.header.file_entry_cnt, Struct( 13 | 'file_name_size'/Int32ul, 14 | 'file_name'/Bytes(this.file_name_size), 15 | 'unk1'/Int32ul, 16 | 'file_offset'/Int32ul, 17 | 'file_size'/Int32ul, 18 | 'file_data'/Pointer(this.file_offset, Bytes(this.file_size)) 19 | ) 20 | ), 21 | 'hash_data'/Pointer(7, Bytes(this.header.hash_data_size)), 22 | ) 23 | -------------------------------------------------------------------------------- /VNEngines/Kirikiri/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /VNEngines/Kirikiri/amv_struct.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from construct import * 4 | 5 | AMVStruct = Struct( 6 | 'header'/Struct( 7 | 'magic'/Const(b'AJPM'), 8 | 'size_of_file'/Int64ul, 9 | 'quantaization_table_size_plus_hdr_size'/Int32ul, 10 | 'unk1'/Int32ul, 11 | 'frame_cnt'/Int32ul, 12 | 'unk2'/Int32ul, 13 | 'frame_rate'/Int32ul, 14 | 'width'/Int16ul, 15 | 'height'/Int16ul, 16 | 'alpha_decode_attr'/Int32ul 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /VNEngines/Majiro/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /VNEngines/Majiro/majiro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ..upscaler import * 4 | 5 | 6 | class Majiro(Upscaler): 7 | """Majiro Script Engine""" 8 | 9 | def __init__(self, game_ui_runner=None): 10 | Upscaler.__init__(self, game_ui_runner) 11 | self.encoding = 'Shift_JIS' 12 | 13 | """ 14 | ================================================== 15 | Majiro引擎脚本文件:mjo, cfg, env, winmerge, csv 16 | ================================================== 17 | """ 18 | 19 | def _script2x(self): 20 | pass 21 | 22 | def mjo_de_batch(self, input_path, output_folder) -> list: 23 | """ 24 | @brief 拆分mjo 25 | 26 | @param input_path The input path 27 | @param output_folder The output folder 28 | 29 | @return 拆分出的mjil文件路径列表 30 | """ 31 | input_path = Path(input_path) 32 | output_folder = Path(output_folder) 33 | mjo_out_path_dict = {} 34 | if input_path.is_file(): 35 | out_dir = input_path.reio_path(input_path.parent, output_folder, mk_dir=True).parent 36 | mjo_out_path_dict[input_path] = out_dir 37 | else: 38 | mjo_file_ls = input_path.file_list('mjo') 39 | for mjo_file in mjo_file_ls: 40 | out_dir = mjo_file.reio_path(input_path, output_folder, mk_dir=True).parent 41 | mjo_out_path_dict[mjo_file] = out_dir 42 | out_mjil_file_ls = self.pool_run(self._mjo_de, mjo_out_path_dict.items()) 43 | return out_mjil_file_ls 44 | 45 | def _mjo_de(self, mjo_out_path) -> Path: 46 | mjo_file, out_dir = mjo_out_path 47 | with tempfile.TemporaryDirectory() as mjo_de_tmp_folder: 48 | mjo_de_tmp_folder = Path(mjo_de_tmp_folder) 49 | tmp_mjo_file = mjo_file.copy_to(mjo_de_tmp_folder) 50 | mjo_de_p = subprocess.run([self.mjotool_exe, 'disassemble', tmp_mjo_file], capture_output=True, shell=True) 51 | tmp_mjil_file = tmp_mjo_file.with_suffix('.mjil') 52 | tmp_mjres_file = tmp_mjo_file.with_suffix('.mjres') 53 | mjil_file = tmp_mjil_file.move_to(out_dir) 54 | if tmp_mjres_file.exists(): 55 | mjres_file = tmp_mjres_file.move_to(out_dir) 56 | return mjil_file 57 | 58 | def mjo_en_batch(self, input_path, output_folder) -> list: 59 | """ 60 | @brief 组合mjo,需要有mjres文件 61 | 62 | @param input_path The input dir 63 | @param output_folder The output dir 64 | 65 | @return mjo path list 66 | """ 67 | input_path = Path(input_path) 68 | output_folder = Path(output_folder) 69 | mjil_out_path_dict = {} 70 | if input_path.is_file(): 71 | out_dir = input_path.reio_path(input_path.parent, output_folder, mk_dir=True).parent 72 | mjil_out_path_dict[input_path] = out_dir 73 | else: 74 | mjil_file_ls = input_path.file_list('mjil') 75 | for mjil_file in mjil_file_ls: 76 | out_dir = mjil_file.reio_path(input_path, output_folder, mk_dir=True).parent 77 | mjil_out_path_dict[mjil_file] = out_dir 78 | out_mjo_file_ls = self.pool_run(self._mjo_en, mjil_out_path_dict.items()) 79 | return out_mjo_file_ls 80 | 81 | def _mjo_en(self, mjil_out_path) -> Path: 82 | mjil_file, out_dir = mjil_out_path 83 | mjres_file = mjil_file.with_suffix('.mjres') 84 | with tempfile.TemporaryDirectory() as mjo_en_tmp_folder: 85 | mjo_en_tmp_folder = Path(mjo_en_tmp_folder) 86 | tmp_mjil_file = mjil_file.copy_to(mjo_en_tmp_folder) 87 | if mjres_file.exists(): 88 | tmp_mjres_file = mjres_file.copy_to(mjo_en_tmp_folder) 89 | mjo_en_p = subprocess.run([self.mjotool_exe, 'assemble', tmp_mjil_file], capture_output=True, shell=True) 90 | tmp_mjo_file = tmp_mjil_file.with_suffix('.mjo') 91 | mjo_file = tmp_mjo_file.move_to(out_dir) 92 | return mjo_file 93 | 94 | """ 95 | ================================================== 96 | Majiro引擎图片文件:png, bmp, jpg, rct, rc8 97 | ================================================== 98 | """ 99 | 100 | def _image2x(self): 101 | pass 102 | 103 | """ 104 | ================================================== 105 | Majiro引擎视频文件:wmv(WMV3), mpg(MPEG1、MPEG2) 106 | ================================================== 107 | """ 108 | 109 | def _video2x(self): 110 | pass 111 | -------------------------------------------------------------------------------- /VNEngines/Majiro/majiro_arc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import construct 4 | construct.core.possiblestringencodings['shift_jis'] = 1 5 | from construct import * 6 | from pathlib import Path 7 | 8 | ArcStruct = Struct( 9 | 'magic'/Const(b'MajiroArc'), 10 | 'header'/Struct( 11 | 'version'/Enum(CString('Shift_JIS'), one='V1.000', two='V2.000', three='V3.000'), 12 | 'file_cnt'/Int32ul, 13 | 'file_names_offset'/Int32ul, 14 | 'file_datas_offset'/Int32ul, 15 | # 'file_names_length'/Computed(this.file_datas_offset-this.file_names_offset), 16 | ), 17 | 'file_names'/Pointer( 18 | this.header.file_names_offset, Array( 19 | this.header.file_cnt, CString('Shift_JIS') 20 | ) 21 | ), 22 | 'file_entries'/Switch( 23 | this.header.version, { 24 | 'one': Array( 25 | this.header.file_cnt, Struct( 26 | 'file_index'/Index, 27 | 'file_hash'/Int32ul, 28 | 'file_data_offset'/Int32ul, 29 | # 'file_data'/Pointer(this.file_data_offset, lambda this: this.file_index) 30 | ) 31 | ), 32 | 'two': Array( 33 | this.header.file_cnt, Struct( 34 | 'file_index'/Index, 35 | 'unk1'/Int32ul, 36 | 'unk2'/Int32ul, 37 | # 'file_data'/Pointer(this.file_data_offset, GreedyRange(Byte)) 38 | ) 39 | ), 40 | 'three': Array( 41 | this.header.file_cnt, Struct( 42 | 'file_index'/Index, 43 | 'unk1'/Int32ul, 44 | 'unk2'/Int32ul, 45 | 'file_data_offset'/Int32ul, 46 | 'file_size'/Int32ul, 47 | 'file_data'/Pointer(this.file_data_offset, Bytes(this.file_size)), 48 | # 'file_name'/this._root.file_names[this.file_index], 49 | # 'file_name'/Computed(lambda this:this._root.file_names[this.file_index]), 50 | ) 51 | ) 52 | } 53 | ), 54 | 'Others'/Struct( 55 | 'unk1'/Int32ul, 56 | 'unk2'/Int32ul, 57 | ) 58 | ) 59 | 60 | 61 | if __name__ == '__main__': 62 | ex_dir = Path('unpack') 63 | arc_path = Path('data1.arc') 64 | arc_path = Path('unpack2.arc') 65 | st = ArcStruct.parse_file(arc_path) 66 | print(st) 67 | # for file_entry in st.file_entries: 68 | # # for i,j in file_entry.values(): 69 | # for i in file_entry: 70 | # if file_entry[i] == 1443597: 71 | # print(i) 72 | 73 | # 拆包 74 | # for file_entry in st.file_entries: 75 | # file_path = ex_dir/st.file_names[file_entry.file_index] 76 | # with open(file_path, 'wb') as f: 77 | # f.write(file_entry.file_data) 78 | 79 | # 封包 80 | # ArcStruct.build_file(st,'test.arc') 81 | -------------------------------------------------------------------------------- /VNEngines/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .Kirikiri.kirikiri import Kirikiri 4 | from .Artemis.artemis import Artemis 5 | from .Majiro.majiro import Majiro 6 | -------------------------------------------------------------------------------- /VNEngines/upscaler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from Core import * 4 | 5 | 6 | class Upscaler(Core): 7 | """ 8 | @brief 其它游戏引擎的放大处理器需继承此类 9 | """ 10 | 11 | def __init__(self, game_ui_runner=None): 12 | Core.__init__(self) 13 | self.load_config() 14 | self.__class__.game_ui_runner = game_ui_runner 15 | self.encoding = 'Shift_JIS' 16 | self.run_dict = {'script': False, 'image': False, 'animation': False, 'video': False} 17 | 18 | def emit_info(self, info_str): 19 | """ 20 | @brief 输出信息到窗口 21 | 22 | @param info_str 字符串信息 23 | """ 24 | print(info_str) 25 | logging.info(info_str) 26 | if self.game_ui_runner is not None: 27 | self.game_ui_runner.info_sig.emit(info_str) 28 | 29 | def emit_progress(self, _percent, _left_time): 30 | """ 31 | @brief 输出进度和剩余时间到进度条 32 | 33 | @param _percent 进度(范围:0-100) 34 | @param _left_time 剩余时间(单位:s) 35 | """ 36 | print(_percent, _left_time, sep='\t') 37 | if self.game_ui_runner is not None: 38 | self.game_ui_runner.progress_sig.emit(_percent, _left_time) 39 | 40 | def a2p(self, file_path) -> Path: 41 | """ 42 | @brief 游戏数据文件夹到补丁文件夹,保持目录结构路径 43 | 44 | @param file_path 文件路径对象 45 | 46 | @return 目标文件路径对象 47 | """ 48 | return file_path.reio_path(self.game_data, self.patch_folder, mk_dir=True) 49 | 50 | def a2t(self, file_path) -> Path: 51 | """ 52 | @brief 游戏数据文件夹到临时文件夹,保持目录结构路径 53 | 54 | @param file_path 文件路径对象 55 | 56 | @return 目标文件路径对象 57 | """ 58 | return file_path.reio_path(self.game_data, self.tmp_folder, mk_dir=True) 59 | 60 | def t2p(self, file_path) -> Path: 61 | """ 62 | @brief 临时文件夹到补丁文件夹,保持目录结构路径 63 | 64 | @param file_path 文件路径对象 65 | 66 | @return 目标文件路径对象 67 | """ 68 | return file_path.reio_path(self.tmp_folder, self.patch_folder, mk_dir=True) 69 | 70 | def upscale(self, game_data, patch_folder, scale_ratio, run_dict=None, encoding=None, resolution=None, **advanced_option): 71 | """ 72 | @brief 对游戏进行放大处理 73 | 74 | @param game_data 输入游戏数据文件夹 75 | @param patch_folder 输出游戏补丁文件夹 76 | @param scale_ratio 放大倍率 77 | @param run_dict 可选放大部分字典 78 | @param encoding 默认文本编码 79 | @param resolution 游戏原生分辨率 80 | @param advanced_option 高级处理选项 81 | """ 82 | # 参数处理 83 | self.game_data = Path(game_data) 84 | self.patch_folder = Path(patch_folder) 85 | self.scale_ratio = scale_ratio 86 | if run_dict is not None: 87 | for _key, _value in run_dict.items(): 88 | self.run_dict[_key] = _value 89 | if encoding is not None: 90 | self.encoding = encoding 91 | if resolution is not None: 92 | self.scwidth, self.scheight = resolution 93 | # 高级选项 94 | self.advanced_option = advanced_option 95 | # 计时 96 | start_time = time.time() 97 | # 创建补丁文件夹 98 | if not self.patch_folder.exists(): 99 | self.patch_folder.mkdir(parents=True) 100 | # 开始放大 101 | if self.run_dict['script']: 102 | self._script2x() 103 | self.emit_info('文本文件处理完成') 104 | if self.run_dict['image']: 105 | self._image2x() 106 | self.emit_info('图片文件放大完成') 107 | if self.run_dict['animation']: 108 | self._animation2x() 109 | self.emit_info('动画文件处理完成') 110 | if self.run_dict['video']: 111 | self._video2x() 112 | self.emit_info('视频文件处理完成') 113 | timing_count = time.time() - start_time 114 | self.emit_info(f'共耗时:{seconds_format(timing_count)}') 115 | 116 | def _script2x(self): 117 | """ 118 | @brief 对脚本进行处理,需复写 119 | """ 120 | pass 121 | 122 | def _image2x(self): 123 | """ 124 | @brief 对图像进行处理,需复写 125 | """ 126 | pass 127 | 128 | def _animation2x(self): 129 | """ 130 | @brief 对动画进行处理,需复写 131 | """ 132 | pass 133 | 134 | def _video2x(self): 135 | """ 136 | @brief 对视频进行处理,需复写 137 | """ 138 | pass 139 | -------------------------------------------------------------------------------- /VisualNovelUpscaler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from Core import * 4 | from VNEngines import * 5 | from GUI import * 6 | from ImagePageUIConnection import ImagePageUIConnection 7 | from GamePageUIConnection import GamePageUIConnection 8 | from SettingPageUIConnection import SettingPageUIConnection 9 | 10 | 11 | class VisualNovelUpscaler(Core, SettingPageUIConnection, GamePageUIConnection, ImagePageUIConnection): 12 | 13 | __version__ = 'v0.2.1' 14 | 15 | def __init__(self): 16 | Core.__init__(self) 17 | self.initUI() 18 | # 错误日志 19 | logging.basicConfig(filename=self.vnu_log_file, encoding='UTF-8', level=logging.DEBUG, filemode='a+', format='[%(asctime)s] [%(levelname)s] >>> %(message)s', datefmt='%Y-%m-%d %I:%M:%S') 20 | # 捕获异常 21 | sys.excepthook = self.catch_exceptions 22 | 23 | def initUI(self): 24 | self.ui = MainUI() 25 | SettingPageUIConnection.__init__(self) 26 | GamePageUIConnection.__init__(self) 27 | ImagePageUIConnection.__init__(self) 28 | self.ui.set_version(self.__version__) 29 | 30 | def catch_exceptions(self, excType, excValue, tb): 31 | error_info = ''.join(traceback.format_exception(excType, excValue, tb)) 32 | logging.error(error_info) 33 | error_msg = QMessageBox() 34 | reply = error_msg.critical(self.ui, '错误!', error_info, QMessageBox.Yes) 35 | 36 | 37 | if __name__ == '__main__': 38 | # 防止打包运行后多进程内存泄漏 39 | freeze_support() 40 | # 防止打包后拖拽运行工作路径改变 41 | os.chdir(Path(sys.argv[0]).parent) 42 | # 启动 43 | app = QApplication(sys.argv) 44 | visual_novel_upscaler = VisualNovelUpscaler() 45 | visual_novel_upscaler.ui.show() 46 | sys.exit(app.exec()) 47 | --------------------------------------------------------------------------------