feat:初始化pptopic项目,添加PPT转长图功能

📝 docs:添加项目文档和配置文件
🔧 chore:配置Python 3.13环境和依赖管理
 feat:实现PPT转换和图片优化核心功能
 feat:添加命令行接口支持导出长图
This commit is contained in:
2026-02-11 18:21:22 +08:00
commit b2dfcfe036
9 changed files with 734 additions and 0 deletions

1
src/pptopic/__init__.py Normal file
View File

@@ -0,0 +1 @@
from pptopic.main import main

4
src/pptopic/__main__.py Normal file
View File

@@ -0,0 +1,4 @@
from pptopic.main import main
if __name__ == "__main__":
main()

167
src/pptopic/lib.py Normal file
View File

@@ -0,0 +1,167 @@
from typing import Self
from pathlib import Path
import cv2
from PIL import Image
from tempfile import TemporaryDirectory
import win32com.client as wcc
import shutil
import numpy as np
from simtoolsz.utils import lastFile
import sys
import subprocess
__all__ = ["convertPPT", "PPT_longPic", "imageOptimization"]
class convertPPT(object):
"""
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
}
__all__ = ["savetype", "trans", "close", "open"]
def __init__(self, file:str|Path, trans:str = "JPG") -> None:
if sys.platform != "win32":
raise SystemError("Only support Windows system.")
if not Path(file).exists():
raise FileNotFoundError("File not found! Please check the file path.")
if trans.upper() not in convertPPT.TTYPES.keys():
raise ValueError("Save type is not supported.")
self.__file = Path(file)
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 not in convertPPT.TTYPES.keys():
raise ValueError("Save type is not supported.")
if saveType is not None:
self.__saveType = (convertPPT.TTYPES[saveType.upper()], saveType.upper())
else:
self.__saveType = (convertPPT.TTYPES["JPG"], "JPG")
def trans(self, saveto:str|Path = ".",
width:int|None = None) -> None :
"""
saveto : 保存路径,默认为当前路径。
"""
ppt = self.__inUsing.Presentations.Open(self.__file.absolute())
if Path(saveto) == Path('.') :
output = self.__file.absolute()
else :
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的时候不进行图像缩放。
"""
if saveName :
sType = saveName.split(".")[-1] if "." in saveName else "JPG"
if sType.upper() not in ["JPG", "PNG"] :
raise ValueError("Unable to save this type `{}` of image.".format(sType))
else:
sType = "JPG"
with TemporaryDirectory() as tmpdirname:
with convertPPT(pptFile, trans=sType) as ppt:
ppt.trans(saveto=tmpdirname)
picList = list(Path(tmpdirname).glob("*.{}".format(sType)))
with Image.open(picList[0]) as img :
if isinstance(width, str):
qw = float(width[:-1])/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 in range(1,len(picList)+1) :
with Image.open(Path(tmpdirname).joinpath("幻灯片{}.{}".format(i,sType))) as img :
new_img = img.resize((nwidth,nheight), resample=Image.Resampling.LANCZOS)
canvas.paste(new_img, box=(0, (i-1) * nheight))
if Path(saveto) == Path('.') :
filepath = Path(pptFile).parent
else :
filepath = Path(saveto)
if saveName :
if '.' in saveName :
canvas.save(filepath.joinpath(saveName))
else:
canvas.save(filepath.joinpath(saveName), format=sType)
else:
canvas.save(filepath.joinpath(Path(pptFile).stem), format=sType)
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) -> 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
oShape = (width, height)
if max_width :
if width > max_width :
height = int(height * max_width / width)
width = max_width
if max_width < 1 :
width = int(width * max_width)
height = int(height * max_width)
if max_height :
if height > max_height :
width = int(width * max_height / height)
height = max_height
if max_height < 1 :
if width < oShape[0] :
if height / oShape[1] > max_height :
height = int(oShape[1] * max_height)
width = int(oShape[0] * max_height)
else:
height = int(height * max_height)
width = int(width * max_height)
img = cv2.resize(img, (width, height))
res = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
with TemporaryDirectory(prefix="pytoolsz") as tmpFolder :
res.save(Path(tmpFolder)/"tmp.png", compress_level=2, quality=100)
if engine :
try:
subprocess.run([engine, engine_conf, Path(tmpFolder)/"tmp.png"], shell=True, check=True)
except Exception as e:
print("未安装pngquant不能进行图片优化压缩。\n可使用`scoop install pngquant`进行安装。")
raise e
else:
res.save(Path(tmpFolder)/"tmp.png", compress_level=7, quality=95)
if saveFile is None :
res = Image.open(lastFile(Path(tmpFolder), "*.*"))
return res
else :
shutil.copyfile(lastFile(Path(tmpFolder), "*.*"), saveFile)

57
src/pptopic/main.py Normal file
View File

@@ -0,0 +1,57 @@
from pathlib import Path
from simtoolsz.utils import today
import typer
from pptopic.lib import PPT_longPic, imageOptimization
app = typer.Typer()
@app.command()
def export(
input_file: Path = typer.Argument(..., help="输入的PPTX文件路径", exists=True),
output_name: str = typer.Option(None, "--output", "-o", help="输出文件名(不含扩展名或含扩展名)"),
output_dir: Path = typer.Option(".", "--dir", "-d", help="输出目录,默认为当前目录"),
width: int = typer.Option(None, "--width", "-w", help="导出图片的宽度(像素)"),
optimize: bool = typer.Option(False, "--optimize", help="是否优化导出的图片"),
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"),
format: str = typer.Option("PNG", "--format", "-f", help="输出格式JPG或PNG默认PNG")
):
"""
将PPTX文件导出为长图
示例:
pptopic export presentation.pptx
pptopic export presentation.pptx --optimize
pptopic export presentation.pptx --output result.png --optimize --max-height 20000
"""
try:
PPT_longPic(
pptFile=input_file,
saveName=f"{output_name}.{format.lower()}" if output_name and "." not in output_name else output_name,
width=width,
saveto=output_dir
)
if optimize:
output_path = output_dir / (output_name if output_name else input_file.stem)
if not output_path.suffix:
output_path = output_path.with_suffix(f".{format.lower()}")
imageOptimization(
imageFile=output_path,
saveFile=output_path,
max_height=max_height,
engine=engine,
engine_conf=engine_conf
)
typer.echo(f"成功导出长图: {output_dir / (output_name if output_name else input_file.stem)}")
except Exception as e:
typer.echo(f"导出失败: {e}", err=True)
raise typer.Exit(code=1)
def main():
app()