├── .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 |
62 |
--------------------------------------------------------------------------------
/Icons/icon_folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
63 |
--------------------------------------------------------------------------------
/Icons/icon_folder_open.svg:
--------------------------------------------------------------------------------
1 |
2 |
64 |
--------------------------------------------------------------------------------
/Icons/icon_maximize.svg:
--------------------------------------------------------------------------------
1 |
2 |
62 |
--------------------------------------------------------------------------------
/Icons/icon_minimize.svg:
--------------------------------------------------------------------------------
1 |
2 |
62 |
--------------------------------------------------------------------------------
/Icons/icon_send.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------