263 lines
9.5 KiB
Python
263 lines
9.5 KiB
Python
import shutil
|
||
import subprocess
|
||
import sys
|
||
import threading
|
||
import time
|
||
from pathlib import Path
|
||
from tempfile import TemporaryDirectory
|
||
from typing import Self
|
||
|
||
import cv2
|
||
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", "batchImageOptimization"]
|
||
|
||
|
||
class convertPPT:
|
||
"""
|
||
win32com使用VBA的API,可从官方教程中看到:
|
||
https://learn.microsoft.com/en-us/office/vba/api/PowerPoint.Presentation.SaveAs
|
||
编码来源:https://my.oschina.net/zxcholmes/blog/484789
|
||
"""
|
||
|
||
TTYPES = {"JPG": 17, "PNG": 18, "PDF": 32, "XPS": 33}
|
||
|
||
def __init__(self, file: str | Path, trans: str = "JPG") -> None:
|
||
if sys.platform != "win32":
|
||
raise SystemError("Only support Windows system.")
|
||
self.__file = Path(file)
|
||
if not self.__file.exists():
|
||
raise FileNotFoundError("File not found! Please check the file path.")
|
||
trans_upper = trans.upper()
|
||
if trans_upper not in convertPPT.TTYPES:
|
||
raise ValueError("Save type is not supported.")
|
||
self.__saveType = (convertPPT.TTYPES[trans_upper], trans_upper)
|
||
self.__inUsing = wcc.Dispatch("PowerPoint.Application")
|
||
|
||
def __enter__(self) -> Self:
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||
self.close()
|
||
|
||
@property
|
||
def savetype(self) -> str:
|
||
return self.__saveType[1]
|
||
|
||
@classmethod
|
||
def open(cls, file: str | Path, trans: str = "JPG") -> Self:
|
||
return cls(file, trans=trans.upper())
|
||
|
||
def saveAs(self, saveType: str | None = None) -> None:
|
||
if saveType is None:
|
||
self.__saveType = (convertPPT.TTYPES["JPG"], "JPG")
|
||
else:
|
||
saveType_upper = saveType.upper()
|
||
if saveType_upper not in convertPPT.TTYPES:
|
||
raise ValueError("Save type is not supported.")
|
||
self.__saveType = (convertPPT.TTYPES[saveType_upper], saveType_upper)
|
||
|
||
def trans(self, saveto: str | Path = ".", width: int | None = None) -> None:
|
||
"""
|
||
saveto : 保存路径,默认为当前路径。
|
||
"""
|
||
ppt = self.__inUsing.Presentations.Open(self.__file.absolute())
|
||
output = Path(saveto).absolute()
|
||
if width is not None:
|
||
ppt.Export(output, self.__saveType[1], width)
|
||
else:
|
||
ppt.SaveAs(output, self.__saveType[0])
|
||
|
||
def close(self, to_console: bool = False) -> None:
|
||
self.__inUsing.Quit()
|
||
if to_console:
|
||
print("File converted successfully.")
|
||
|
||
|
||
def PPT_longPic(
|
||
pptFile: str | Path, saveName: str | None = None, width: int | str | None = None, saveto: str | Path = "."
|
||
) -> None:
|
||
"""
|
||
width : 画幅宽度,可以直接指定宽度像素,也可以使用字符串数据输入百分比。
|
||
(700; "22.1%")
|
||
指定为None的时候,不进行图像缩放。
|
||
"""
|
||
pptFile = Path(pptFile)
|
||
if saveName:
|
||
sType = Path(saveName).suffix[1:].upper() if Path(saveName).suffix else "JPG"
|
||
if sType not in ["JPG", "PNG"]:
|
||
raise ValueError(f"Unable to save this type `{sType}` of image.")
|
||
else:
|
||
sType = "JPG"
|
||
|
||
with TemporaryDirectory() as tmpdirname:
|
||
with convertPPT(pptFile, trans=sType) as ppt:
|
||
ppt.trans(saveto=tmpdirname)
|
||
picList = sorted(Path(tmpdirname).glob(f"*.{sType}"))
|
||
if not picList:
|
||
raise ValueError("No images generated from PPT conversion.")
|
||
|
||
with Image.open(picList[0]) as img:
|
||
if isinstance(width, str):
|
||
qw = float(width.rstrip("%")) / 100.0
|
||
nwidth, nheight = (int(img.width * qw), int(img.height * qw))
|
||
elif width is None:
|
||
nwidth, nheight = img.size
|
||
else:
|
||
nwidth, nheight = (width, int(img.height * width / img.width))
|
||
canvas = Image.new(img.mode, (nwidth, nheight * len(picList)))
|
||
|
||
for i, picPath in enumerate(picList, 1):
|
||
with Image.open(picPath) as img:
|
||
new_img = img.resize((nwidth, nheight), resample=Image.Resampling.LANCZOS)
|
||
canvas.paste(new_img, box=(0, (i - 1) * nheight))
|
||
|
||
filepath = Path(saveto).resolve()
|
||
if saveName:
|
||
if Path(saveName).suffix:
|
||
canvas.save(filepath / saveName)
|
||
else:
|
||
canvas.save(filepath / saveName, format=sType)
|
||
else:
|
||
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,
|
||
max_width: int = None,
|
||
max_height: int = None,
|
||
engine: str | None = "pngquant",
|
||
engine_conf: str | None = None,
|
||
progress_desc: str | None = None,
|
||
) -> Image.Image | None:
|
||
"""图片优化、无损压缩
|
||
默认建议使用pngquant进行无损压缩,也可以设置为其他图片无损压缩引擎,
|
||
不需要针对性压缩,可设定engine为None。
|
||
"""
|
||
if isinstance(imageFile, str | Path):
|
||
imageFile = Image.open(imageFile)
|
||
img = cv2.cvtColor(np.array(imageFile), cv2.COLOR_RGB2BGR)
|
||
|
||
if max_width or max_height:
|
||
height, width, _ = img.shape
|
||
new_width, new_height = width, height
|
||
|
||
if max_width:
|
||
if max_width < 1:
|
||
new_width = int(width * max_width)
|
||
new_height = int(height * max_width)
|
||
elif width > max_width:
|
||
new_width = max_width
|
||
new_height = int(height * max_width / width)
|
||
|
||
if max_height:
|
||
if max_height < 1:
|
||
if new_width < width:
|
||
if new_height / height > max_height:
|
||
new_height = int(height * max_height)
|
||
new_width = int(width * max_height)
|
||
else:
|
||
new_height = int(new_height * max_height)
|
||
new_width = int(new_width * max_height)
|
||
elif new_height > max_height:
|
||
new_width = int(new_width * max_height / new_height)
|
||
new_height = max_height
|
||
|
||
img = cv2.resize(img, (new_width, new_height))
|
||
|
||
res = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
||
with TemporaryDirectory(prefix="pytoolsz") as tmpFolder:
|
||
tmpPath = Path(tmpFolder) / "tmp.png"
|
||
res.save(tmpPath, compress_level=2, quality=100)
|
||
|
||
if engine:
|
||
try:
|
||
_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
|
||
else:
|
||
res.save(tmpPath, compress_level=7, quality=95)
|
||
|
||
if saveFile is None:
|
||
res = Image.open(lastFile(Path(tmpFolder), "*.*"))
|
||
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
|