feat(lib): 添加批量图片优化功能与实时进度显示

- 新增 `batchImageOptimization` 函数支持批量处理图片
- 为图片优化添加 spinner 进度指示器和实时计时显示
- 更新版本号至 0.3.2
This commit is contained in:
2026-06-18 16:17:38 +08:00
parent ffcb2801b2
commit 97b30ace84
9 changed files with 553 additions and 479 deletions

View File

@@ -1,6 +1,6 @@
from pptopic.main import main
__version__ = "0.3.0"
__version__ = "0.3.2"
__author__ = "Sidney Zhang <zly@lyzhang.me>"

View File

@@ -1,6 +1,8 @@
import shutil
import subprocess
import sys
import threading
import time
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Self
@@ -10,8 +12,9 @@ import numpy as np
import win32com.client as wcc
from PIL import Image
from simtoolsz.utils import lastFile
from yaspin import yaspin
__all__ = ["convertPPT", "PPT_longPic", "imageOptimization"]
__all__ = ["convertPPT", "PPT_longPic", "imageOptimization", "batchImageOptimization"]
class convertPPT:
@@ -123,6 +126,31 @@ def PPT_longPic(
canvas.save(filepath / pptFile.name, format=sType)
def _run_engine_with_spinner(cmd: list, *, desc: str = "优化图片中") -> None:
"""运行外部优化引擎,显示 spinner 和实时耗时。"""
_stop = threading.Event()
_t0 = time.monotonic()
def _update_timer(sp: yaspin) -> None:
while not _stop.is_set():
elapsed = time.monotonic() - _t0
minutes, seconds = divmod(elapsed, 60)
sp.text = f"{desc}... {int(minutes):02d}:{seconds:05.2f}"
_stop.wait(0.2)
with yaspin(text=f"{desc}...") as spinner:
_timer = threading.Thread(target=_update_timer, args=(spinner,), daemon=True)
_timer.start()
try:
subprocess.run(cmd, shell=True, check=True)
except subprocess.CalledProcessError:
spinner.fail("图片优化失败")
raise
finally:
_stop.set()
_timer.join(timeout=1)
def imageOptimization(
imageFile: str | Path | Image.Image,
saveFile: str | Path | None = None,
@@ -130,6 +158,7 @@ def imageOptimization(
max_height: int = None,
engine: str | None = "pngquant",
engine_conf: str | None = None,
progress_desc: str | None = None,
) -> Image.Image | None:
"""图片优化、无损压缩
默认建议使用pngquant进行无损压缩也可以设置为其他图片无损压缩引擎
@@ -173,7 +202,12 @@ def imageOptimization(
if engine:
try:
subprocess.run([engine, engine_conf, tmpPath], shell=True, check=True)
_run_engine_with_spinner(
[engine, engine_conf, tmpPath],
desc=progress_desc or "优化图片中",
)
except subprocess.CalledProcessError:
raise
except Exception as e:
print("未安装pngquant不能进行图片优化压缩。\n可使用`scoop install pngquant`进行安装。")
raise e
@@ -185,3 +219,44 @@ def imageOptimization(
return res
else:
shutil.copyfile(lastFile(Path(tmpFolder), "*.*"), saveFile)
def batchImageOptimization(
image_files: list[str | Path],
save_dir: str | Path | None = None,
max_width: int = None,
max_height: int = None,
engine: str | None = "pngquant",
engine_conf: str | None = None,
) -> list[Path]:
"""批量优化图片,每张图片独立显示 spinner + 进度计数。
单个文件失败不中断整体流程,完成后返回成功处理的文件列表。
"""
results: list[Path] = []
total = len(image_files)
for i, image_file in enumerate(image_files, 1):
image_file = Path(image_file)
save_file: Path | None = None
if save_dir:
save_file = Path(save_dir) / image_file.name
try:
result = imageOptimization(
imageFile=image_file,
saveFile=save_file,
max_width=max_width,
max_height=max_height,
engine=engine,
engine_conf=engine_conf,
progress_desc=f"[{i}/{total}] 优化图片中",
)
if result is not None:
results.append(Path(image_file))
elif save_file:
results.append(save_file)
except Exception:
continue
return results

View File

@@ -3,7 +3,7 @@ from pathlib import Path
import typer
from simtoolsz.utils import today
from pptopic.lib import PPT_longPic, imageOptimization
from pptopic.lib import PPT_longPic, batchImageOptimization, imageOptimization
app = typer.Typer()
@@ -86,6 +86,45 @@ def optimize(
raise typer.Exit(code=1)
@app.command()
def batch_optimize(
input_dir: Path = typer.Argument(..., help="输入图片目录", exists=True, file_okay=False),
output_dir: Path = typer.Option(None, "--output", "-o", help="输出目录,默认覆盖原文件"),
pattern: str = typer.Option("*.png", "--pattern", "-p", help="文件匹配模式(默认 *.png"),
max_height: int = typer.Option(29999, "--max-height", help="优化图片的最大高度默认29999"),
engine: str = typer.Option("pngquant", "--engine", help="图片优化引擎默认pngquant"),
engine_conf: str = typer.Option(
"--skip-if-larger", "--engine-conf", help="图片优化引擎配置参数(默认--skip-if-larger"
),
):
"""
批量优化目录下的所有图片
示例:
pptopic batch-optimize ./images
pptopic batch-optimize ./images --pattern "*.jpg" --output ./optimized
pptopic batch-optimize ./images --max-height 20000 --engine pngquant
"""
try:
image_files = sorted(input_dir.glob(pattern))
if not image_files:
typer.echo(f"{input_dir} 中没有找到匹配 {pattern} 的文件", err=True)
raise typer.Exit(code=1)
results = batchImageOptimization(
image_files=image_files,
save_dir=output_dir,
max_height=max_height,
engine=engine,
engine_conf=engine_conf,
)
typer.echo(f"成功优化 {len(results)}/{len(image_files)} 张图片")
except Exception as e:
typer.echo(f"批量优化失败: {e}", err=True)
raise typer.Exit(code=1)
@app.command()
def version():
"""显示版本信息"""