diff --git a/i18n.ini b/i18n.ini index 6e2b1c5..dd6de2f 100644 --- a/i18n.ini +++ b/i18n.ini @@ -21,6 +21,7 @@ EnableLossyMode = 使用有损压缩(JPEG/WebP) LossyModeQuality = 有损压缩质量(0-100) CustomCommand = 自定义压缩/后期处理命令 EnableIgnoreError = 在批处理过程中忽略错误并继续处理 +EnablePreupscale = 尝试预先使用常规算法放大 ViewREGUISource = 查看源代码 ViewRESource = 查看 Real-ESRGAN 介绍 ViewAdditionalModel = 下载附加模型 @@ -65,6 +66,7 @@ EnableLossyMode = 使用有損壓縮(JPEG/WebP) LossyModeQuality = 有損壓縮質量(0-100) CustomCommand = 自定義壓縮/後期處理命令 EnableIgnoreError = 在批處理過程中忽略錯誤並繼續處理 +EnablePreupscale = 嘗試預先使用常規算法放大 ViewREGUISource = 查看源代碼 ViewRESource = 查看 Real-ESRGAN 介紹 ViewAdditionalModel = 下載附加模型 @@ -109,6 +111,7 @@ EnableLossyMode = 使用有損壓縮(JPEG/WebP) LossyModeQuality = 有損壓縮質量(0-100) CustomCommand = 自訂壓縮/後期處理命令 EnableIgnoreError = 在批處理過程中忽略錯誤並繼續處理 +EnablePreupscale = 嘗試預先使用常規算法放大 ViewREGUISource = 查看原始碼 ViewRESource = 查看 Real-ESRGAN 介紹 ViewAdditionalModel = 下載附加模型 @@ -153,6 +156,7 @@ EnableLossyMode = Enable lossy compression (JPEG/WebP) LossyModeQuality = Lossy compression quality (0-100) CustomCommand = Custom compression/post-processing command EnableIgnoreError = Ignore error and continue during batch processing +EnablePreupscale = Try to pre-upscale with general algorithm ViewREGUISource = View source code ViewRESource = About Real-ESRGAN ViewAdditionalModel = Download additional models diff --git a/main.py b/main.py index 190c49c..1892031 100644 --- a/main.py +++ b/main.py @@ -155,6 +155,7 @@ def outputPathTraceCallback(var: tk.IntVar | tk.StringVar, index: str, mode: str self.varboolOptimizeGIF = tk.BooleanVar(value=self.config['Config'].getboolean('OptimizeGIF')) self.varboolLossyMode = tk.BooleanVar(value=self.config['Config'].getboolean('LossyMode')) self.varboolIgnoreError = tk.BooleanVar(value=self.config['Config'].getboolean('IgnoreError')) + self.varboolPreupscale = tk.BooleanVar(value=self.config['Config'].getboolean('Preupscale')) self.varboolProcessing = tk.BooleanVar(value=False) self.varboolProcessingPaused = tk.BooleanVar(value=False) self.varstrCustomCommand = tk.StringVar(value=self.config['Config'].get('CustomCommand')) @@ -182,6 +183,7 @@ def outputPathTraceCallback(var: tk.IntVar | tk.StringVar, index: str, mode: str self.varstrLabelGIFOptimizeTransparency = tk.StringVar(value=i18n.getTranslatedString('GIFOptimizeTransparency')) self.varstrLabelEnableLossyMode = tk.StringVar(value=i18n.getTranslatedString('EnableLossyMode')) self.varstrLabelEnableIgnoreError = tk.StringVar(value=i18n.getTranslatedString('EnableIgnoreError')) + self.varstrLabelEnablePreupscale = tk.StringVar(value=i18n.getTranslatedString('EnablePreupscale')) self.varstrLabelViewREGUISource = tk.StringVar(value=i18n.getTranslatedString('ViewREGUISource')) self.varstrLabelViewRESource = tk.StringVar(value=i18n.getTranslatedString('ViewRESource')) self.varstrLabelViewAdditionalModel = tk.StringVar(value=i18n.getTranslatedString('ViewAdditionalModel')) @@ -294,6 +296,8 @@ def setupWidgets(self): self.checkLossyMode.pack(padx=10, pady=5, fill=tk.X) self.checkIgnoreError = ttk.Checkbutton(self.frameAdvancedConfigRight, textvariable=self.varstrLabelEnableIgnoreError, style='Switch.TCheckbutton', variable=self.varboolIgnoreError) self.checkIgnoreError.pack(padx=10, pady=5, fill=tk.X) + self.checkPreupscale = ttk.Checkbutton(self.frameAdvancedConfigRight, textvariable=self.varstrLabelEnablePreupscale, style='Switch.TCheckbutton', variable=self.varboolPreupscale) + self.checkPreupscale.pack(padx=10, pady=5, fill=tk.X) self.comboLanguage = ttk.Combobox(self.frameAdvancedConfigRight, state='readonly', values=tuple(i18n.locales_map.keys())) self.comboLanguage.current(i18n.get_current_locale_display_name()) self.comboLanguage.pack(padx=10, pady=5, fill=tk.X) @@ -361,6 +365,7 @@ def change_app_lang(self, event: tk.Event): self.varstrLabelGIFOptimizeTransparency.set(i18n.getTranslatedString('GIFOptimizeTransparency')) self.varstrLabelEnableLossyMode.set(i18n.getTranslatedString('EnableLossyMode')) self.varstrLabelEnableIgnoreError.set(i18n.getTranslatedString('EnableIgnoreError')) + self.varstrLabelEnablePreupscale.set(i18n.getTranslatedString('EnablePreupscale')) self.varstrLabelViewREGUISource.set(i18n.getTranslatedString('ViewREGUISource')) self.varstrLabelViewRESource.set(i18n.getTranslatedString('ViewRESource')) self.varstrLabelViewAdditionalModel.set(i18n.getTranslatedString('ViewAdditionalModel')) @@ -385,6 +390,8 @@ def close(self): 'UseTTA': self.varboolUseTTA.get(), 'OptimizeGIF': self.varboolOptimizeGIF.get(), 'LossyMode': self.varboolLossyMode.get(), + 'IgnoreError': self.varboolIgnoreError.get(), + 'Preupscale': self.varboolPreupscale.get(), 'CustomCommand': self.varstrCustomCommand.get(), 'AppLanguage': i18n.current_language } @@ -621,6 +628,7 @@ def getConfigParams(self) -> param.REConfigParams: self.tileSize[self.varintTileSizeIndex.get()], self.varintGPUID.get(), self.varboolUseTTA.get(), + self.varboolPreupscale.get(), self.varstrCustomCommand.get().strip(), ) @@ -665,6 +673,7 @@ def init_config_and_model_paths() -> tuple[configparser.ConfigParser, list[str]] 'OptimizeGIF': False, 'LossyMode': False, 'IgnoreError': False, + 'Preupscale': False, 'CustomCommand': '', 'AppLanguage': locale.getdefaultlocale()[0], }) diff --git a/param.py b/param.py index 5eb6d36..774eaac 100644 --- a/param.py +++ b/param.py @@ -17,4 +17,5 @@ class REConfigParams(typing.NamedTuple): tileSize: int gpuID: int useTTA: bool + preupscale: bool customCommand: str diff --git a/task.py b/task.py index 21ddee8..4e5ee4d 100644 --- a/task.py +++ b/task.py @@ -1,7 +1,7 @@ import collections -import secrets import subprocess import io +import math import os import re import shlex @@ -18,9 +18,6 @@ import define import param -def buildTempPath(ext: str) -> str: - return os.path.join(tempfile.gettempdir(), secrets.token_urlsafe(12) + ext) - class AbstractTask: def __init__(self, outputCallback: typing.Callable[[str], None]) -> None: self.outputCallback = outputCallback @@ -52,7 +49,7 @@ def run(self) -> None: srcWidth, srcHeight = img.size srcRatio = srcWidth / srcHeight if img.mode == 'P': - self.inputPath = buildTempPath('.png') + self.inputPath = tempfile.mktemp('.png') img.convert('RGBA').save(self.inputPath) self.removeInput = True match self.config.resizeMode: @@ -65,6 +62,26 @@ def run(self) -> None: case param.ResizeMode.HEIGHT: dstHeight = self.config.resizeModeValue dstWidth = round(dstHeight * srcRatio) + inputPathPreupscaled: str = None + if self.config.preupscale: + match self.config.resizeMode: + case param.ResizeMode.RATIO: + scaleRatio = self.config.resizeModeValue + case param.ResizeMode.WIDTH: + scaleRatio = self.config.resizeModeValue / srcWidth + case param.ResizeMode.HEIGHT: + scaleRatio = self.config.resizeModeValue / srcHeight + frac, intg = math.modf(math.log(scaleRatio, self.config.modelFactor)) + preWidth = math.ceil(dstWidth / (self.config.modelFactor ** intg)) + preHeight = math.ceil(dstHeight / (self.config.modelFactor ** intg)) + if frac < .5 and (srcWidth != preWidth or srcHeight != preHeight): + self.outputCallback(f'Pre-upscale from {srcWidth}x{srcHeight} to {preWidth}x{preHeight}.\n') + inputPathPreupscaled = tempfile.mktemp('.webp' if os.path.splitext(self.inputPath)[1] == '.webp' else '.png') + with Image.open(self.inputPath) as img: + resized = img.resize((preWidth, preHeight), Image.LANCZOS) + resized.save(inputPathPreupscaled, lossless=True) + resized.close() + srcWidth, srcHeight = preWidth, preHeight scalePass = 0 while srcWidth < dstWidth and srcHeight < dstHeight: scalePass += 1 @@ -75,7 +92,7 @@ def run(self) -> None: # input -> temp0 -> output # input -> temp0 -> temp1 -> output outputExt = os.path.splitext(self.outputPath)[1] - files = (self.inputPath, *(buildTempPath(outputExt) for _ in range(scalePass))) + files = (inputPathPreupscaled or self.inputPath, *(tempfile.mktemp(outputExt) for _ in range(scalePass))) for i in range(len(files) - 1): inputPath, outputPath = files[i:(i + 2)] alphaOverridePath = None @@ -132,7 +149,7 @@ def run(self) -> None: self.outputCallback(line) if p.returncode: raise subprocess.CalledProcessError(p.returncode, cmd) - if i > 0 or self.removeInput: + if i > 0 or inputPath == inputPathPreupscaled or self.removeInput: os.remove(inputPath) if alphaOverridePath: shutil.move(alphaOverridePath, outputPath) @@ -146,7 +163,7 @@ def run(self) -> None: else: with Image.open(files[-1]) as img: self.outputCallback(f'Downsample from {img.size[0]}x{img.size[1]} to {dstWidth}x{dstHeight}.\n') - resized: Image.Image = img.resize((dstWidth, dstHeight), self.config.downsample) + resized = img.resize((dstWidth, dstHeight), self.config.downsample) resized.save(self.outputPath) resized.close() if scalePass: @@ -233,8 +250,8 @@ def run(self) -> None: with Image.open(self.inputPath) as img: for f in ImageSequence.Iterator(img): f: Image.Image - frameSrcPath = buildTempPath('.png' if self.optimizeTransparency else '.webp') - frameDstPath = buildTempPath('.png' if self.optimizeTransparency else '.webp') + frameSrcPath = tempfile.mktemp('.png' if self.optimizeTransparency else '.webp') + frameDstPath = tempfile.mktemp('.png' if self.optimizeTransparency else '.webp') d = f.info.get('duration', 0) if self.optimizeTransparency: f = f.convert('RGBA') @@ -251,7 +268,7 @@ def run(self) -> None: self.progressValue[2] += 1 self.progressValue[2] -= 1 if self.config.customCommand: - t = buildTempPath('.gif') + t = tempfile.mktemp('.gif') tasks.append(MergeGIFTask(self.outputCallback, t, frames, durations, self.optimizeTransparency)) tasks.append(CustomCompressTask(self.outputCallback, t, self.outputPath, self.config.customCommand, True)) else: