feat(lib): 添加批量图片优化功能与实时进度显示
- 新增 `batchImageOptimization` 函数支持批量处理图片 - 为图片优化添加 spinner 进度指示器和实时计时显示 - 更新版本号至 0.3.2
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from pptopic.main import main
|
||||
|
||||
__version__ = "0.3.0"
|
||||
__version__ = "0.3.2"
|
||||
|
||||
__author__ = "Sidney Zhang <zly@lyzhang.me>"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
"""显示版本信息"""
|
||||
|
||||
Reference in New Issue
Block a user