Files
pptopic/src/pptopic/lib.py

263 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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