Skip to content

Commit

Permalink
自定义后处理命令
Browse files Browse the repository at this point in the history
  • Loading branch information
TransparentLC committed Aug 29, 2023
1 parent 44045f8 commit 66ddfb3
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 14 deletions.
15 changes: 13 additions & 2 deletions README.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,23 @@ This option was added to resolve these issues. It adds the following actions:

This option is experimental and it is recommended to enable it only when upscaling GIFs with transparency.

### About lossy compression and compression quality
### About lossy compression, compression quality and custom compression/post-processing command

If lossy compression is enabled and the output format is JPEG or WebP, you can control the compression quality of the output image to the set value. If the input is a directory, the output compression quality will also be affected by this option when upscaling JPEG or WebP images in the directory.
If lossy compression is enabled and the output format is JPEG or WebP, you can control the compression quality of the output image to the set value. If the input is a directory, the output compression quality will also be affected by this option when upscaling JPEG or WebP images in the directory. The compression is done using Pillow.

If this option is not turned on, lossless compression is used when the output is in WebP format.

If custom compression/post-processing command is set, the Pillow's compression will not be performed. You can set a command to compress the upscaled image or do other processing with it.

* `{input}` represents the path of the input file.
* `{output}` represents the path of the output file.
* `{output:ext}` represents the path of the output file with the extension `ext`.
* Cookbook:
* Use [avifenc (libavif)](https://github.com/AOMediaCodec/libavif/blob/main/doc/avifenc.1.md) to convert to AVIF: `avifenc --speed 6 --jobs all --depth 8 --yuv 420 --min 0 --max 63 -a end-usage=q -a cq-level=30 -a enable-chroma-deltaq=1 --autotiling --ignore-icc --ignore-xmp --ignore-exif {input} {output:avif}`
* Use [cjxl (libjxl)](https://github.com/libjxl/libjxl#usage) to convert to JPEG XL: `cjxl {input} {output:jxl} --quality=80 --effort=9 --progressive --verbose`
* Use [gif2webp (libwebp)](https://developers.google.com/speed/webp/docs/gif2webp) to convert the output GIF to WebP: `gif2webp -lossy -q 80 -m 6 -min_size -mt -v {input} -o {output:webp}`
* Use [ImageMagick](https://imagemagick.org/) to add a text watermark in the lower-right corner and then convert to AVIF: `magick convert -fill white -pointsize 24 -gravity SouthEast -draw "text 16 16 'https://github.com/TransparentLC/realesrgan-gui'" -quality 80 {input} {output:avif}`

### Where the configuration file is saved?

`config.ini` in the repository's directory or in the directory where Real-ESRGAN GUI's executable is located, without this file the default configuration is used.
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,25 @@ GIF 只支持最多 256 种 RGB 颜色的调色板并设定其中一种颜色为

这个选项是实验性的,建议在放大存在透明部分的 GIF 时手动开启,在放大不存在透明部分的 GIF 时关闭。可能是由于这里的实现或 Pillow 对 GIF 的处理存在问题,在开启时处理后者会出现一些奇怪的问题(主要是出现不该出现的透明色以及仿色效果非常差)。也许会有更好的处理方法。

### 高级设定中的“使用有损压缩”“有损压缩质量”是什么?
### 高级设定中的“使用有损压缩”“有损压缩质量”和“自定义压缩/后期处理命令”是什么?

开启这个选项以后,如果输出的文件是 JPEG 或 WebP 格式,就可以根据设定的值(0-100 表示从低质量到高质量)控制输出的文件的压缩质量了。如果输入的是文件夹,则放大文件夹中 JPEG 或 WebP 格式的图片时输出的压缩质量也会受这个选项影响。
开启“使用有损压缩”以后,如果输出的文件是 JPEG 或 WebP 格式,就可以根据设定的值(0-100 表示从低质量到高质量)控制输出的文件的压缩质量了。如果输入的是文件夹,则放大文件夹中 JPEG 或 WebP 格式的图片时输出的压缩质量也会受这个选项影响。压缩使用 Python 的图像处理库 Pillow 完成

不开启这个选项的话,输出为 WebP 格式时使用的是无损压缩。

如果设定了“自定义压缩/后期处理命令”,则不会进行上面的压缩操作。在这里你可以输入一条命令对放大后的图片进行压缩或其他的处理,还可以自定义命令中的参数。

* `{input}` 表示输入文件的路径。
* `{output}` 表示输出文件的路径。
* `{output:ext}` 表示输出文件的路径,但把扩展名修改为 `ext`
* 命令示例:
* 使用 [avifenc (libavif)](https://github.com/AOMediaCodec/libavif/blob/main/doc/avifenc.1.md) 转换为 AVIF 格式:`avifenc --speed 6 --jobs all --depth 8 --yuv 420 --min 0 --max 63 -a end-usage=q -a cq-level=30 -a enable-chroma-deltaq=1 --autotiling --ignore-icc --ignore-xmp --ignore-exif {input} {output:avif}`
* 使用 [cjxl (libjxl)](https://github.com/libjxl/libjxl#usage) 转换为 JPEG XL 格式:`cjxl {input} {output:jxl} --quality=80 --effort=9 --progressive --verbose`
* 使用 [gif2webp (libwebp)](https://developers.google.com/speed/webp/docs/gif2webp) 将输出的 GIF 转换为 WebP 格式:`gif2webp -lossy -q 80 -m 6 -min_size -mt -v {input} -o {output:webp}`
* 使用 [ImageMagick](https://imagemagick.org/) 在右下角添加文字水印,然后转换为 AVIF 格式:`magick convert -fill white -pointsize 24 -gravity SouthEast -draw "text 16 16 'https://github.com/TransparentLC/realesrgan-gui'" -quality 80 {input} {output:avif}`

请忽略“基本设定”的“输出”的扩展名,实际的输出文件扩展名由设定的命令决定。

### 配置文件的保存位置

项目目录或打包后的可执行文件所在目录下的 `config.ini`,没有这个文件的情况下会使用默认的配置。在退出程序时会自动保存配置。
Expand Down
5 changes: 5 additions & 0 deletions i18n.ini
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ EnableTTA = 使用 TTA 模式(速度大幅下降,稍微提高质量)
GIFOptimizeTransparency = 针对 GIF 的透明色进行额外处理(实验性功能)
EnableLossyMode = 使用有损压缩(JPEG/WebP)
LossyModeQuality = 有损压缩质量(0-100)
CustomCommand = 自定义压缩/后期处理命令
EnableIgnoreError = 在批处理过程中忽略错误并继续处理
ViewREGUISource = 查看源代码
ViewRESource = 查看 Real-ESRGAN 介绍
Expand Down Expand Up @@ -58,6 +59,7 @@ EnableTTA = 使用 TTA 模式(速度大幅下降,稍微提高質量)
GIFOptimizeTransparency = 針對 GIF 的透明色進行額外處理(實驗性功能)
EnableLossyMode = 使用有損壓縮(JPEG/WebP)
LossyModeQuality = 有損壓縮質量(0-100)
CustomCommand = 自定義壓縮/後期處理命令
EnableIgnoreError = 在批處理過程中忽略錯誤並繼續處理
ViewREGUISource = 查看源代碼
ViewRESource = 查看 Real-ESRGAN 介紹
Expand Down Expand Up @@ -99,6 +101,7 @@ EnableTTA = 使用 TTA 模式(速度大幅下降,稍微提高品質)
GIFOptimizeTransparency = 針對 GIF 的透明色進行額外處理(實驗性功能)
EnableLossyMode = 使用有損壓縮(JPEG/WebP)
LossyModeQuality = 有損壓縮質量(0-100)
CustomCommand = 自訂壓縮/後期處理命令
EnableIgnoreError = 在批處理過程中忽略錯誤並繼續處理
ViewREGUISource = 查看原始碼
ViewRESource = 查看 Real-ESRGAN 介紹
Expand Down Expand Up @@ -140,6 +143,7 @@ EnableTTA = Enable TTA mode (extremely slow, slightly better quality)
GIFOptimizeTransparency = Enable additional processing for GIF with transparency (Experimantal)
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
ViewREGUISource = View source code
ViewRESource = About Real-ESRGAN
Expand Down Expand Up @@ -181,6 +185,7 @@ EnableTTA = Увімкнути режим TTA (вкрай повільно, тр
GIFOptimizeTransparency = Увімкнути додаткову обробку для GIF з прозорістю (експериментально)
EnableLossyMode = Увімкнути стиснення з втратами (JPEG/WebP)
LossyModeQuality = Якість стиснення з втратами (0-100)
CustomCommand = Користувацькі команди стиснення/постобробки
EnableIgnoreError = Ігнорувати помилки під час пакетної обробки та продовжувати обробку
ViewREGUISource = Переглянути вихідний код
ViewRESource = Про Real-ESRGAN
Expand Down
41 changes: 32 additions & 9 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def __init__(self, parent: tk.Tk):
'OptimizeGIF': False,
'LossyMode': False,
'IgnoreError': False,
'CustomCommand': '',
})
self.config['Config'] = {}
self.config.read(define.APP_CONFIG_PATH)
Expand Down Expand Up @@ -115,6 +116,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.varstrCustomCommand = tk.StringVar(value=self.config['Config'].get('CustomCommand'))
self.varintLossyQuality = tk.IntVar(value=self.config['Config'].getint('LossyQuality'))

def setupWidgets(self):
Expand Down Expand Up @@ -180,28 +182,39 @@ def setupWidgets(self):
self.frameAdvancedConfig = ttk.Frame(self.notebookConfig, padding=5)
self.frameAdvancedConfig.grid(row=0, column=0, padx=5, pady=5, sticky=tk.NSEW)
self.frameAdvancedConfig.columnconfigure(0, weight=1)
self.frameAdvancedConfig.columnconfigure(1, weight=1)
self.frameAdvancedConfig.columnconfigure(1, weight=3)
self.frameAdvancedConfigLeft = ttk.Frame(self.frameAdvancedConfig)
self.frameAdvancedConfigLeft.grid(row=0, column=0, sticky=tk.NSEW)
self.frameAdvancedConfigRight = ttk.Frame(self.frameAdvancedConfig)
self.frameAdvancedConfigRight.grid(row=0, column=1, sticky=tk.NSEW)
ttk.Label(self.frameAdvancedConfigLeft, text=i18n.getTranslatedString('DownsampleMode')).pack(padx=10, pady=5, fill=tk.X)
self.comboDownsample = ttk.Combobox(self.frameAdvancedConfigLeft, state='readonly', values=tuple(x[0] for x in self.downsample))
self.frameAdvancedConfigLeftSub = ttk.Frame(self.frameAdvancedConfigLeft)
self.frameAdvancedConfigLeftSub.pack(fill=tk.X)
self.frameAdvancedConfigLeftSub.columnconfigure(0, weight=1)
self.frameAdvancedConfigLeftSub.columnconfigure(1, weight=1)
self.frameAdvancedConfigLeftSubLeft = ttk.Frame(self.frameAdvancedConfigLeftSub)
self.frameAdvancedConfigLeftSubLeft.grid(row=0, column=0, sticky=tk.NSEW)
self.frameAdvancedConfigLeftSubRight = ttk.Frame(self.frameAdvancedConfigLeftSub)
self.frameAdvancedConfigLeftSubRight.grid(row=0, column=1, sticky=tk.NSEW)
ttk.Label(self.frameAdvancedConfigLeftSubLeft, text=i18n.getTranslatedString('DownsampleMode')).pack(padx=10, pady=5, fill=tk.X)
self.comboDownsample = ttk.Combobox(self.frameAdvancedConfigLeftSubLeft, state='readonly', values=tuple(x[0] for x in self.downsample), width=12)
self.comboDownsample.current(self.varintDownsampleIndex.get())
self.comboDownsample.pack(padx=10, pady=5, fill=tk.X)
self.comboDownsample.bind('<<ComboboxSelected>>', self.comboDownsample_click)
ttk.Label(self.frameAdvancedConfigLeftSubRight, text=i18n.getTranslatedString('TileSize')).pack(padx=10, pady=5, fill=tk.X)
self.comboTileSize = ttk.Combobox(self.frameAdvancedConfigLeftSubRight, state='readonly', values=(i18n.getTranslatedString('TileSizeAuto'), *self.tileSize[1:]), width=12)
self.comboTileSize.current(self.varintTileSizeIndex.get())
self.comboTileSize.pack(padx=10, pady=5, fill=tk.X)
ttk.Label(self.frameAdvancedConfigLeft, text=i18n.getTranslatedString('UsedGPUID')).pack(padx=10, pady=5, fill=tk.X)
self.spinGPUID = ttk.Spinbox(self.frameAdvancedConfigLeft, from_=-1, to=7, increment=1, width=12, textvariable=self.varintGPUID)
self.spinGPUID.pack(padx=10, pady=5, fill=tk.X)
ttk.Label(self.frameAdvancedConfigLeft, text=i18n.getTranslatedString('TileSize')).pack(padx=10, pady=5, fill=tk.X)
self.comboTileSize = ttk.Combobox(self.frameAdvancedConfigLeft, state='readonly', values=(i18n.getTranslatedString('TileSizeAuto'), *self.tileSize[1:]))
self.comboTileSize.current(self.varintTileSizeIndex.get())
self.comboTileSize.pack(padx=10, pady=5, fill=tk.X)
ttk.Label(self.frameAdvancedConfigLeft, text=i18n.getTranslatedString('LossyModeQuality')).pack(padx=10, pady=5, fill=tk.X)
self.spinLossyQuality = ttk.Spinbox(self.frameAdvancedConfigLeft, from_=0, to=100, increment=5, width=12, textvariable=self.varintLossyQuality)
self.spinLossyQuality.set(self.varintLossyQuality.get())
self.spinLossyQuality.pack(padx=10, pady=5, fill=tk.X)
self.comboTileSize.bind('<<ComboboxSelected>>', self.comboTileSize_click)
ttk.Label(self.frameAdvancedConfigLeft, text=i18n.getTranslatedString('CustomCommand')).pack(padx=10, pady=5, fill=tk.X)
self.entryCustomCommand = ttk.Entry(self.frameAdvancedConfigLeft, textvariable=self.varstrCustomCommand)
self.entryCustomCommand.pack(padx=10, pady=5, fill=tk.X)
self.checkUseWebP = ttk.Checkbutton(self.frameAdvancedConfigRight, text=i18n.getTranslatedString('PreferWebP'), style='Switch.TCheckbutton', variable=self.varboolUseWebP)
self.checkUseWebP.pack(padx=10, pady=5, fill=tk.X)
self.checkUseTTA = ttk.Checkbutton(self.frameAdvancedConfigRight, text=i18n.getTranslatedString('EnableTTA'), style='Switch.TCheckbutton', variable=self.varboolUseTTA)
Expand Down Expand Up @@ -255,6 +268,7 @@ def close(self):
'UseTTA': self.varboolUseTTA.get(),
'OptimizeGIF': self.varboolOptimizeGIF.get(),
'LossyMode': self.varboolLossyMode.get(),
'CustomCommand': self.varstrCustomCommand.get(),
}
with open(define.APP_CONFIG_PATH, 'w', encoding='utf-8') as f:
self.config.write(f)
Expand Down Expand Up @@ -307,6 +321,10 @@ def buttonProcess_click(self):
g = os.path.join(outputPath, f.removeprefix(inputPath + os.path.sep))
if os.path.splitext(f)[1].lower() == '.gif':
queue.append(task.SplitGIFTask(self.writeToOutput, f, g, initialConfigParams, queue, self.varboolOptimizeGIF.get()))
elif self.varstrCustomCommand.get().strip():
t = task.buildTempPath('.png')
queue.append(task.RESpawnTask(self.writeToOutput, f, t, initialConfigParams))
queue.append(task.CustomCompressTask(self.writeToOutput, t, g, self.varstrCustomCommand.get().strip(), True))
elif self.varboolLossyMode.get() and os.path.splitext(g)[1].lower() in {'.jpg', '.jpeg', '.webp'}:
t = task.buildTempPath('.webp')
queue.append(task.RESpawnTask(self.writeToOutput, f, t, initialConfigParams))
Expand All @@ -318,6 +336,10 @@ def buttonProcess_click(self):
elif os.path.splitext(inputPath)[1].lower() in {'.jpg', '.jpeg', '.png', '.gif', '.webp'}:
if os.path.splitext(inputPath)[1].lower() == '.gif':
queue.append(task.SplitGIFTask(self.writeToOutput, inputPath, outputPath, initialConfigParams, queue, self.varboolOptimizeGIF.get()))
elif self.varstrCustomCommand.get().strip():
t = task.buildTempPath('.png')
queue.append(task.RESpawnTask(self.writeToOutput, inputPath, t, initialConfigParams))
queue.append(task.CustomCompressTask(self.writeToOutput, t, outputPath, self.varstrCustomCommand.get().strip(), True))
elif self.varboolLossyMode.get() and os.path.splitext(outputPath)[1].lower() in {'.jpg', '.jpeg', '.webp'}:
t = task.buildTempPath('.webp')
queue.append(task.RESpawnTask(self.writeToOutput, inputPath, t, initialConfigParams))
Expand Down Expand Up @@ -394,16 +416,17 @@ def getConfigParams(self) -> param.REConfigParams:
self.tileSize[self.varintTileSizeIndex.get()],
self.varintGPUID.get(),
self.varboolUseTTA.get(),
self.varstrCustomCommand.get().strip(),
)

def getOutputPath(self, p: str) -> str:
if os.path.isdir(p):
base, ext = p, ''
else:
base, ext = os.path.splitext(p)
if ext.lower() == '.jpg':
if ext.lower() == '.jpg' or self.varstrCustomCommand.get().strip():
ext = '.png'
if ext.lower() == '.png' and self.varboolUseWebP.get():
elif ext.lower() == '.png' and self.varboolUseWebP.get():
ext = '.webp'
suffix = ''
match self.varintResizeMode.get():
Expand Down
1 change: 1 addition & 0 deletions param.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ class REConfigParams(typing.NamedTuple):
tileSize: int
gpuID: int
useTTA: bool
customCommand: str
49 changes: 48 additions & 1 deletion task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import subprocess
import io
import os
import re
import shlex
import shutil
import tempfile
import time
Expand Down Expand Up @@ -200,7 +202,12 @@ def run(self) -> None:
frames.append(frameDstPath)
durations.append(d)
tasks.append(RESpawnTask(self.outputCallback, frameSrcPath, frameDstPath, self.config, True))
tasks.append(MergeGIFTask(self.outputCallback, self.outputPath, frames, durations, self.optimizeTransparency))
if self.config.customCommand:
t = buildTempPath('.gif')
tasks.append(MergeGIFTask(self.outputCallback, t, frames, durations, self.optimizeTransparency))
tasks.append(CustomCompressTask(self.outputCallback, t, self.outputPath, self.config.customCommand, True))
else:
tasks.append(MergeGIFTask(self.outputCallback, self.outputPath, frames, durations, self.optimizeTransparency))
tasks.reverse()
for t in tasks:
self.queue.appendleft(t)
Expand Down Expand Up @@ -231,6 +238,46 @@ def run(self) -> None:
if self.removeInput:
os.remove(self.inputPath)

class CustomCompressTask(AbstractTask):
def __init__(
self,
outputCallback: typing.Callable[[str], None],
inputPath: str, outputPath: str,
commandTemplate: str,
removeInput: bool = False,
) -> None:
super().__init__(outputCallback)
self.inputPath = inputPath
self.outputPath = outputPath
self.commandTemplate = commandTemplate
self.removeInput = removeInput

def run(self) -> None:
cmd = []
for x in shlex.split(self.commandTemplate):
if x == '{input}':
cmd.append(self.inputPath)
elif x == '{output}':
cmd.append(self.outputPath)
elif (m := re.search(r'^{output:(.+)}$', x)):
cmd.append(f'{os.path.splitext(self.outputPath)[0]}.{m.group(1)}')
else:
cmd.append(x)
self.outputCallback(f'Compressing {self.inputPath} with command: {shlex.join(cmd)}\n')
os.makedirs(os.path.split(self.outputPath)[0], exist_ok=True)
with subprocess.Popen(
cmd,
stderr=subprocess.PIPE,
universal_newlines=True,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0,
) as p:
for line in p.stderr:
self.outputCallback(line)
if p.returncode:
raise subprocess.CalledProcessError(p.returncode, cmd)
if self.removeInput:
os.remove(self.inputPath)

def taskRunner(
queue: collections.deque[AbstractTask],
outputCallback: typing.Callable[[str], None],
Expand Down

0 comments on commit 66ddfb3

Please sign in to comment.