一个拖放式批量图片压缩工具箱,提供两套界面:
- GUI(推荐):基于 PySide6 的 Windows 桌面图形界面 — 双击
run_gui.vbs(无任何终端窗口)或run_gui.bat,也可运行python -m gui.app - TUI:基于 Textual 的纯终端界面 — 运行
python main.py或run.bat
支持 JPEG / PNG / WebP / AVIF / HEIC / GIF / BMP / TIFF,自动按目标体积循环缩小,可选覆盖原文件,动图自动转为 MP4 (H.265),同目录同分辨率视频可合并转码为 AV1,并利用 NVIDIA / Intel 硬件加速。两套界面共用同一套核心引擎,功能一致。
所有可调参数(质量、目标体积、缩放比例、输出格式、动图质量、并行数、输出模式、UI 截断长度、日志目录等)都集中在
config.toml,GUI 内可在「设置」对话框直接修改(保留注释),也可以手改文件。
| 方式 | 说明 |
|---|---|
双击 run_gui.vbs |
推荐。完全无终端窗口,连黑框闪烁都没有。可建桌面快捷方式并把图标设为 assets/app_icon.ico |
双击 run_gui.bat |
用 pythonw.exe 启动,脚本窗口随即退出,不残留空终端(启动时可能有一闪) |
python -m gui.app |
命令行启动,保留控制台输出,便于调试 |
启动器会自动寻找装有 PySide6 的解释器(依次检查 ICT_PYTHON 环境变量 → 项目内 .venv → PATH 上的 python.exe),
找到后用同目录的 pythonw.exe 启动,因此既不依赖 PATH 顺序,也不会留下终端。
如需固定解释器,先执行一次 set ICT_PYTHON=D:\Python312\python.exe(可写入系统环境变量持久生效)。
无控制台模式下(
pythonw),stdout/stderr/ 未捕获异常 / Qt 警告会被自动重定向到logs/gui_console.log,不会出现"窗口一闪而过但查不到原因"的情况。用python.exe启动时不启用该兜底。 程序内所有 ffprobe / ffmpeg / nvidia-smi 等子进程调用均带CREATE_NO_WINDOW标志, 扫描、合并、转码全程不会弹出任何终端窗口。每次启动会把实际选中的解释器记入
logs/launch.log;启动失败则生成logs/launch_error.txt并用记事本打开。
⚠️ 维护注意(改动这两个文件前必读)
run_gui.bat必须保存为 GBK(936) 编码。cmd 按系统代码页解析 bat,若存成 UTF-8, 中文注释被 GBK 解码后,第二字节会落在 ASCII 可打印区(0x40–0x7E),凭空产生"、&、>, 破坏整段脚本语法 —— 表现为双击后毫无反应。run_gui.vbs保持纯 ASCII,理由同上(VBScript 同样按系统代码页解析)。- 脚本内不要加
pause:VBS 以隐藏窗口运行,pause会永久等待键盘输入,用户无从察觉。
| 区域 | 内容 |
|---|---|
| 标题栏 | 窗口标题 + 右上角 CPU 实时功耗(需 HWiNFO,见下)+ 硬件徽标 NVENC ✓/✗ · QSV ✓/✗ |
| 工具栏 | 执行: 一键开始 / 仅压缩图片 / 停止 · 文件: 添加文件 / 添加文件夹 / 移除选中 / 清空列表 · 视频: 合并视频 / 恢复覆盖 · 工具: 打开输出目录 · 右侧: 设置 / 主题切换 |
| 输出模式 | 「子目录」与「覆盖」分段开关(默认跟随 config.toml,当前为覆盖) |
| 图片队列 | 表格:文件名 / 格式 / 原始 / 压缩后 / 节省% / 状态;支持多选、右键菜单(移除、打开所在目录、重试失败项)、双击打开图片。节省列符号:体积变小显示 -x%(绿色),变大显示 +x%(橙色,即"越压越大则跳过"的行) |
| 视频合并 | 合并流图:平时仅显示右侧目标视频节点(两行文本:文件名 + 所在目录末级目录名,同目录目标相邻排列);左键单击目标节点在左列展开/收起其来源视频链路(同一时间只展开一个,单击空白处收起);合并开始后自动跟随正在合并的目标展开链路(流动动画始终可见,单击某个"合并中"目标可钉住视角);S 形曲线连接(待合并=红虚线 / 合并中=红虚线自源向目标流动 / 已合并=绿实线 / 跳过·超限=灰虚线 / 失败=红实线);合并完成后自动校验成片时长,异常的目标节点带红色感叹号徽标;信息悬停浮窗显示,节点右键菜单(源节点:移除选中 / 打开所在文件夹;目标节点:打开所在文件夹) |
| 运行日志 | 带时间戳的彩色实时日志(与 logs/*.log 同源) |
| 底部 | 进度条 + 当前文件 + 统计(成功/跳过/失败/节省体积/视频组数/模式/线程数) |
- 拖放:文件或文件夹拖到窗口任意位置即可入队(可多次拖入自动去重);
Ctrl+V粘贴路径亦可。 - 一键开始:先压缩图片(阶段1),再合并视频(阶段2);覆盖模式下启动前会弹确认框。
- 合并前复查(动图转码产物纳入流图):阶段2 启动前会用与合并器完全相同的 扫描规则复查各合并目录,把列表里还没有的视频补进来(典型来源:阶段1 把多帧动图转成 MP4 后,覆盖模式下它们被移回所在目录)——先探测分辨率、再重新分组,然后才开始合并。 因此流图显示的范围 = 实际合并的范围;复查是顶层扫描,开销与目录数成正比、与目录体积无关。 已「移除选中」的视频保持排除,不会被复查加回。
- 仅压缩图片:只执行阶段1,不合并视频(当次会话内生效,下次"一键开始"恢复完整流程)。
- 清空列表:一键移除图片与视频的队列记录(不删除任何文件),同时清空视频合并排除与残留检测状态。
- 设置单位下拉:带单位的设置项(目标体积、预读阈值、单文件超时、关键帧间隔、合并超时)均为「数字输入框 + 单位下拉框」(如关键帧间隔 2 秒可切为 500 毫秒),保存时自动换算回配置基准单位。
- 停止:协作式取消 — 已开始的任务会完成,未提交的任务跳过;被截断的批次不执行覆盖。
- 恢复覆盖:拖入目录时自动检测崩溃残留的
compressed/,检测到后按钮亮起,一键移回覆盖原图。 - 设置:图形化编辑
config.toml全部参数(5 个页签),保存时逐行写回并保留中文注释,下一批次生效。 - 主题:浅色 / 深色一键切换,选择记忆在
gui_state.json。 - CPU 功耗徽标(标题栏右上角):每秒读取 HWiNFO 共享内存中的 "CPU Package Power"。
前提:正在运行 HWiNFO64 且已在
Settings → Safety勾选 Shared Memory Support; 未运行 / 未启用时该标签自动隐藏,不影响其它功能。功耗取值优先级:CPU 传感器的 Package 功耗 → CPU 传感器的首个功耗读数。 - 视频移除说明:原始视频右键「移除选中」只把它移出合并列表(不删除文件),且该文件 在本次会话内不会再被扫描加回(防止误扫描复活);重启程序后排除清空。
- 合并流图交互:平时只显示目标视频(
V1.mp4一类同名文件用末级目录名区分); 左键单击目标节点展开它的来源链路(互斥,展开新的自动收起旧的),再点一次或点空白处收起。 合并开始后自动跟随正在合并的目标展开其链路,流动动画始终可见;单击某个"合并中"的目标 可钉住视角(跟随不抢占"合并中"的链路)。 - 编码不一致的组不再跳过:复制流拼接(
-c copy)要求组内编码与参数完全一致。 遇到组内编码不同(如h264+hevc)时,改为先逐源统一规格再拼接、只编码一次为 AV1: concat 滤镜逐个解码 → 统一到组内代表分辨率(缩放+居中补边)/像素格式/帧率/时基 → 拼成一条流编码 AV1;音轨统一采样率与声道布局,个别源没有音轨就用等长静音补齐。 相比"先把每条转成 AV1 中间片再拼接"省掉一整次编解码(画质与耗时都更优), 也不会让多个并发硬件编码挤爆编码器会话。复制流拼接因其它参数差异失败的组同样退回这条路径。 由mixed_codec_merge控制(默认开);关掉则回到"跳过该组"。 - 前期合并产物不再重复处理:对同一个目录再跑一次时,已经是成品的输出文件不会被当成原料
重新合并/编码。判据(三条同时满足):① 文件名符合输出命名(
V+ 序号 +.mp4,大小写不敏感); ② 视频编码已经是 AV1;③ 本目录内它的分辨率组只有它自己(没有别的视频能跟它拼)。 满足即在合并规划阶段剔除(GUI 流图里标为「已合并产物」且不生成目标),日志与汇总会给出 "N 个已合并产物未重复处理"。只要还有同分辨率视频能跟它合并,就照常参与,不会误伤。 - 两级并发合并:所有目录先批量规划(扫描/探测/分组),再进入全局流水线——
阶段1 复制流合并并发
concat_workers(默认 10),每组拼完立即进入阶段2 AV1 转码并发transcode_workers(默认 2,QSV 硬编建议 1-2); 两组并发数均可在 设置 → 视频合并 调整。 - 合并时长校验与修复:concat 产物/QSV 编码器对不干净的时间戳(PTS 空洞)会放大为
成片时长虚增 + 中段冻结帧(ShanaEncoder 等同类工具也有此问题)。本程序默认:
① concat 输入加
-fflags +genpts;② 转码前setpts=N/fps/TB重排视频时间戳 (normalize_pts,成片时长 = 帧数/fps);③ 完成后校验成片视频流时长 vs 源视频时长之和 (duration_check,容差duration_tolerance_pct% /duration_tolerance_min_s秒取大者), 偏差过大先尝试从 concat 列表重转码自动修复(duration_repair),仍异常则在该目标节点 标红色感叹号并在悬停浮窗给出期望/实际时长。
- 拖放式操作:把文件或文件夹直接拖进终端即可加入队列,可多次拖入自动去重。
- 魔数识别格式:基于文件头字节判断格式,忽略扩展名欺骗。
- 智能压缩:解码 → 转 RGB(透明像素用白色/配置底色填充)→ 编码 → 超目标体积则按比例循环缩小,直到达标或达到上限。
- 跳过策略:压缩后不比原图更小则不保存直接跳过;动图在无 FFmpeg 时也会跳过(不报错)。
- 体积变化口径(GUI / TUI 统一):日志与表格里的百分比一律「变小 = 节省 =
-x%(绿)、 变大 =+x%(红/橙,即"越压越大则跳过"的记录)」—— 由compressor.fmt_saving_pct单点实现, 两套界面共用,避免各写一份符号逻辑导致口径相反。 - 两种输出模式:
- 子目录模式(默认):输出到
compressed/子目录,原文件只读、不受影响。 - 覆盖模式:压缩完成后把
compressed/内的文件移回原目录并覆盖原图;原图与压缩图后缀不同(如.png→.jpg)时自动删除旧原图;全部成功才删除compressed/,任一失败则保留。
- 子目录模式(默认):输出到
- 动图转 MP4 (H.265):多帧 GIF / WebP / APNG / AVIF 通过 FFmpeg 转成 MP4(编码器按
hevc_nvenc→hevc_qsv→libx265优先级自动选择)。其中动画 WebP 由 Pillow 逐帧解码后以原始 RGB 视频流管道喂给 FFmpeg(FFmpeg 内置 WebP 解码器仅支持静态帧,无法读取ANIM/ANMF动画块)。 - 硬件加速:自动检测 NVIDIA(NVENC / CUDA 缩放)、Intel QuickSync,否则回退 CPU。
- 多线程并行:用线程池并行压缩,线程数自动取 CPU 核心数(可配置)。
- 运行日志:每次运行写入
./logs/YYYY-MM-DD.log,记录环境、每张图的压缩结果。
| 格式 | 识别 | 压缩/处理 | 备注 |
|---|---|---|---|
| JPEG | ✅ | 压缩 | — |
| PNG | ✅ | 压缩 | 透明区域填充底色 |
| WebP | ✅ | 压缩 / 动图转 MP4 | 静态压缩 + 多帧转视频 |
| AVIF | ✅ | 压缩 | 需 pillow-heif |
| HEIC | ✅ | 压缩 | 需 pillow-heif |
| GIF | ✅ | 动图转 MP4 | 多帧转视频,单帧当图片压 |
| BMP | ✅ | 压缩 | — |
| TIFF | ✅ | 压缩 | — |
HEIC / AVIF 解码依赖
pillow-heif。若未安装,这两类文件会被标记为失败并提示安装该包(不影响其它格式)。
- Python 3.11+(用到内置
tomllib,无需额外解析库) - 必需 Python 包(见
requirements.txt):textual>=0.47.0(TUI 框架)Pillow>=10.0.0(图片解码/缩放/编码)pillow-heif>=0.15.0(HEIC / AVIF 解码)numpy>=1.24.0(GPU 缩放路径的数组桥接)
- 可选:
torch(含 CUDA):开启 NVIDIA GPU 缩放加速;不装也能用,回退 CPU LANCZOS。- FFmpeg(系统二进制):开启动图 → MP4(H.265);不装则动图被跳过。
- Windows:
winget install FFmpeg - macOS:
brew install ffmpeg - Linux:
sudo apt install ffmpeg
- Windows:
# 1. 进入项目目录
cd image-compressor-toolbox
# 2. 安装依赖(建议使用虚拟环境)
pip install -r requirements.txt
# 3. 启动(Windows 也可直接双击 run.bat)
python main.py
# 可选:启动时直接带入初始路径(文件或文件夹)
python main.py "D:/Pictures/照片" "D:/a.jpg"首次运行会在程序目录下自动生成 config.toml。
- 拖放:将图片文件或文件夹直接拖进终端窗口(终端会把路径作为文本粘贴进来,程序自动识别)。
- 粘贴:复制路径后
Ctrl+V粘贴亦可。 - 拖入文件夹会递归扫描其中所有支持格式的图片并加入列表;可多次拖入,列表自动累积并去重。
| 按键 | 动作 | 说明 |
|---|---|---|
s |
开始压缩 | 对当前列表批量压缩,压缩中锁定队列 |
m |
切换模式 | 在「子目录」与「覆盖」模式间切换 |
x |
清空列表 | 移除队列内所有图片 |
c |
清空日志 | 清空右侧日志面板 |
q |
退出 | 退出程序 |
列表中无法单独删除某一项,如需移除请按
x清空后重新拖入。压缩完成后可继续拖入新文件进行下一批。
实时显示:队列数量、已完成数量、当前处理文件名、当前输出模式、检测到的硬件加速类型(如 NV RTX 3080 Ti)。
对每张静态图片:
- 魔数检测格式;非图片 / 不支持格式 → 失败。
- 解码原图(原文件只读,绝不修改)。
- 转 RGB:JPEG 无透明通道,PNG / WebP 等带透明区域时按
background_color填充底色(默认白色)。 - 编码:以
quality(默认 90)编码为 JPEG(或output_format指定的格式)。 - 体积检查:若输出 >
target_size_mb(默认 5 MB),按scale_step(默认 0.9,即每次缩 10%)缩小后重新编码,循环往复,最多max_scale_iterations(默认 50)轮。 - 跳过判定:若压缩后体积 ≥ 原图,则不保存直接跳过(避免越压越大)。
程序启动时自动检测(详见状态栏与启动日志):
- NVIDIA GPU:通过
nvidia-smi或torch.cuda检测;若同时装了torch,缩放环节走 GPU(torch.nn.functional.interpolate,area 模式)。 - Intel QuickSync:通过
ffmpeg -hwaccels检测,用于动图转 MP4。 - CPU 软件回退:Pillow LANCZOS 缩放,通用但较慢。
JPEG 的编解码始终在 CPU(Pillow)完成 —— NVENC 不支持 MJPEG 输出,所以硬件加速主要用于缩放与视频编码。
多帧 GIF / WebP / APNG / AVIF 会被交给 FFmpeg 转成 MP4:
- 编码器按
config.toml的video.encoder_priority顺序择优:hevc_nvenc(N卡) →hevc_qsv(Intel) →libx265(CPU)。 - 质量由
video.crf(默认 28,越低越好)控制;单文件转换超时video.timeout(默认 300 秒)。 - 转出的 MP4 比原图更大时同样会跳过。
- 动画 WebP 特殊处理:FFmpeg 的内置
webp解码器不支持动画 WebP(会报image data not found),因此动画 WebP 由 Pillow 逐帧解码(自动处理帧间合成/透明度),合成到配置的background_color背景上后,以原始 RGB24 视频流通过 stdin 管道喂给 FFmpeg 编码为 MP4。帧率从 WebP 的ANMF块中解析的真实帧时长计算(支持可变帧率的平均值),解析失败时回退 24 fps。 - 未安装 FFmpeg 时,动图会被直接跳过(不报错、不中断批处理)。
- 转码产物的合并范围(与视频合并的顶层扫描规则一致):
- 覆盖模式:
compressed/里的 MP4 会被移回原目录 → 与真实视频同等参与合并 (阶段2 复查时自动加入合并流图); - 子目录模式:产物留在
compressed/里 → 不参与合并(合并只扫合并目录顶层, 不递归子目录)。需要合并时请切到覆盖模式重跑一次(产物回到目录顶层即参与合并)。
- 覆盖模式:
修改配置后重启程序即生效。文件缺失时自动生成默认版本(带中文注释)。用户配置会与内置默认值深合并:缺项自动补默认、未知项忽略,升级默认配置不会破坏旧配置。
| 分组 | 字段 | 默认 | 说明 |
|---|---|---|---|
[compression] |
quality |
90 |
JPEG 质量 0–100 |
target_size_mb |
5.0 |
超过此体积则循环缩小 | |
scale_step |
0.9 |
每轮尺寸乘此比例(0.9 = 缩 10%) | |
max_scale_iterations |
50 |
缩小轮数上限(安全阀) | |
output_format |
"jpeg" |
输出格式(详见下方说明) | |
background_color |
[255,255,255] |
透明背景填充色 RGB | |
progressive |
true |
生成渐进式 JPEG | |
optimize |
true |
优化 Huffman 表 | |
[output] |
subdir_name |
"compressed" |
输出子目录名 |
default_mode |
"subdir" |
启动默认模式:subdir / overwrite |
|
[parallel] |
max_workers |
0 |
并行线程数,0 = 自动(CPU 核心数) |
[io] |
small_file_threshold_mb |
2.0 |
小于此体积的文件一次性读入内存(减少 SMB/网络往返) |
scan_workers |
8 |
目录扫描并行线程数(SMB/网络路径建议 8–16) | |
[video] |
crf |
28 |
H.265 质量(越低越好) |
encoder_priority |
["nvidia","intel_qsv","cpu"] |
动图编码硬件优先级 | |
timeout |
300 |
单文件转换超时(秒) | |
extension |
"mp4" |
视频输出扩展名 | |
[video_merge] |
encoder / fallback_encoder |
av1_qsv / libsvtav1 |
合并转码首选/回退 AV1 编码器 |
quality |
26 |
AV1 质量(qsv=global_quality,CPU=crf) | |
keyframe_interval |
2 |
关键帧间隔(秒) | |
concat_workers |
10 |
视频合并并发数(阶段1 复制流拼接) | |
transcode_workers |
2 |
转AV1并发数(阶段2 转码;QSV 硬编建议 1-2) | |
concat_genpts |
true |
concat 输入加 -fflags +genpts 补全缺失 PTS |
|
normalize_pts |
true |
转码前 setpts=N/fps/TB 重排视频时间戳(修复时长虚增/冻结帧;个别 VFR 源可能轻微变速,可关闭) |
|
duration_check |
true |
合并后校验成片时长 vs 源视频时长之和 | |
duration_tolerance_pct / duration_tolerance_min_s |
3.0 / 1.0 |
时长偏差容忍(百分比 / 绝对秒下限,取大者) | |
duration_repair |
true |
时长异常时自动从 concat 列表重转码修复,仍异常标红色感叹号 | |
tolerance_px |
100 |
分辨率分组容差(宽高差 ≤ 此值视为同分辨率) | |
max_videos_per_group / max_output_files |
100 / 10 |
单组视频数 / 目标文件数上限 | |
output_prefix / output_extension |
V / mp4 |
输出命名(V1.mp4…) | |
overwrite_existing |
false |
目标已存在时覆盖(否则跳过) | |
mixed_codec_merge |
true |
组内视频编码不一致(如 h264+hevc)时:逐源统一规格后拼接并转 AV1(只编码一次),而不是跳过该组;关掉则退回旧行为(跳过) | |
timeout |
3600 |
单次合并+转码超时(秒) | |
[ui] |
max_name_len |
22 |
状态栏文件名截断长度 |
max_path_len |
55 |
列表路径截断长度 | |
title |
"图片压缩工具箱" |
窗口标题 | |
[logging] |
log_dir |
"./logs" |
日志目录 |
关于
output_format:当前完整验证的输出为jpeg。该字段会透传给 Pillow 的编码格式与输出扩展名,改为webp等时静态图可尝试输出对应格式,但仅 JPEG 经过完整测试;若你启用其它格式遇到问题欢迎反馈。
- 日志目录:
config.toml的logging.log_dir(默认./logs)。 - 文件命名:
imgcompressor_YYYY-MM-DD.log。 - 记录内容:启动环境(硬件、核心数、FFmpeg 状态、支持格式)、每张图片的原始格式 / 压缩方式 / 缩放次数 / 体积变化、覆盖模式的移动结果。
- 终端内不刷屏,仅写文件;可在界面用
c清空屏幕日志面板。
image-compressor-toolbox/
├── main.py # TUI 主程序(拖放、队列、快捷键、状态栏、覆盖逻辑)
├── compressor.py # 压缩引擎(单图压缩流水线、跳过判定、输出路径)
├── video_converter.py # 动图 → MP4(H.265) 转换(FFmpeg 三级降级)
├── video_merge.py # 视频合并转码 AV1(同目录同分辨率分组)
├── detector.py # 魔数格式识别(8 种格式)
├── hwaccel.py # 硬件加速检测与 GPU 缩放
├── hwinfo_power.py # CPU 功耗读取(HWiNFO 共享内存)
├── winproc.py # 子进程启动标志(CREATE_NO_WINDOW, 无终端弹窗)
├── win_perf.py # Windows 功耗节流/EcoQoS 规避
├── logger.py # 日志模块(按日期写入 log_dir)
├── config.py # 配置加载(自动生成 + 深合并)
├── config.toml # 用户配置(首次运行自动生成, 不随仓库分发)
├── requirements.txt # Python 依赖
├── run.bat # Windows 一键启动 TUI
├── run_gui.bat # Windows 一键启动 GUI(pythonw, 无残留终端)
├── run_gui.vbs # GUI 无窗口启动器(双击零闪烁)
├── tools/make_icon.py # 生成 assets/app_icon.ico
├── assets/ # 应用图标(256/128/64/48/32/24/16 多尺寸 ICO)
├── docs/ # 界面预览图
├── test_core.py # 核心冒烟测试
├── gui/ # 图形界面包 (PySide6)
│ ├── app.py # 入口(含无控制台日志兜底)
│ ├── main_window.py # 主窗口(拖放、工具栏、进度、日志)
│ ├── merge_flow.py # 合并流图(目标节点常驻+单击展开来源链路、悬停详情、右键菜单、时长异常徽标)
│ ├── models.py # 图片队列 / 视频数据模型
│ ├── workers.py # 后台线程(扫描/探测/压缩/合并/恢复覆盖)
│ ├── overwrite.py # 覆盖/恢复覆盖逻辑(纯函数版)
│ ├── settings_dialog.py # 设置对话框(含数字框+单位下拉, 保留注释写回)
│ ├── style.py # 深/浅主题 QSS
│ └── smoke_test.py # GUI 冒烟测试 (QT_QPA_PLATFORM=offscreen)
└── logs/ # 运行日志(自动生成, 不随仓库分发)
python test_core.py覆盖:单图压缩、体积超限缩小、压后更大跳过、动图转 MP4(需 FFmpeg)、覆盖模式移动逻辑;
gui/smoke_test.py 另覆盖体积变化符号口径(变小 -x% / 变大 +x%)与后台线程释放安全
(终局信号时线程仍在运行时不得丢引用 —— 见 已知限制 中的崩溃说明)。
视频合并链路可用 tools/test_duration_fix.py 单独端到端验证(真实 ffmpeg:干净 CFR /
A·V 时长不匹配 / 校验检出 / 自动修复 / 混编组统一转码(h264+hevc、无音轨补齐、
容差内不同分辨率、不同帧率、开关关闭五种子场景),需 QSV 或 CPU AV1 编码器)。
- Windows 覆盖模式:移动文件前会强制
gc.collect()释放残留句柄,避免PermissionError;若仍失败,压缩结果保留在compressed/中不会丢失。 - 原文件只读:压缩与解码全程不修改原文件;覆盖模式通过先生成再移动的方式覆盖,失败可回退。
- 崩溃诊断:GUI 启动时会启用
faulthandler,C 级崩溃(访问违例 / abort)时把各线程的 Python 调用栈写入logs/faulthandler.log;同时 Qt 警告与 Python 异常写入logs/gui_console.log(pythonw无控制台运行时同样有效)。若窗口"无声消失",先看这两个文件。 - 动图需要 FFmpeg:未安装时动图仅被跳过,不会转换。
- HEIC / AVIF 需要
pillow-heif:未安装时这两类文件压缩失败。 - 输出子目录名固定为
compressed,扫描文件夹时会自动跳过已有的compressed/目录,避免自引用。
处理 SMB 共享、NAS、或其它高延迟网络路径上的大量小文件时,每个文件的多轮 I/O 往返会成为瓶颈。程序内置以下优化(通过 [io] 配置段控制):
- 并行目录扫描(
scan_workers):拖入文件夹后,格式检测(魔数读取)由线程池并行执行,而非逐文件串行。SMB 路径上 8 线程可带来 4–6 倍提速。 os.scandir缓存 stat:目录遍历使用os.scandir(),DirEntry.stat()的文件大小信息从目录列表缓存中直接获取,无需额外网络往返。- 元数据缓存到队列:扫描阶段检测到的格式 + 文件大小会缓存到待压缩队列中,压缩阶段直接复用,不再重复调用
detect_format()和stat()。 - 小文件预读入内存(
small_file_threshold_mb):体积低于阈值的文件在一次 I/O 中完整读入BytesIO,后续格式检测与 Pillow 解码均从内存缓冲区进行。旧流程每张小文件需要 3–4 次网络往返(检测开/读/关 + stat + Pillow 开 + 读),优化后仅 1 次。
对于大量小文件(如 500 张 ≤ 2 MB 的 JPEG),SMB 路径上的整体提速可达 5–10 倍。
