Compare commits

..

2 Commits

7 changed files with 89 additions and 64 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ wheels/
# Virtual environments # Virtual environments
.venv .venv
.vscode

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2026 SidneyZhang
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "pptopic" name = "pptopic"
version = "0.1.0" version = "0.3.0"
description = "导出长图" description = "导出长图"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"

View File

@@ -1 +1,7 @@
from pptopic.main import main from pptopic.main import main
__version__ = "0.3.0"
__author__ = "Sidney Zhang <zly@lyzhang.me>"
__all__ = ["main"]

View File

@@ -1,29 +1,27 @@
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 shutil
import numpy as np
from simtoolsz.utils import lastFile
import sys
import subprocess import subprocess
import sys
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
__all__ = ["convertPPT", "PPT_longPic", "imageOptimization"] __all__ = ["convertPPT", "PPT_longPic", "imageOptimization"]
class convertPPT(object):
class convertPPT:
""" """
win32com使用VBA的API可从官方教程中看到 win32com使用VBA的API可从官方教程中看到
https://learn.microsoft.com/en-us/office/vba/api/PowerPoint.Presentation.SaveAs https://learn.microsoft.com/en-us/office/vba/api/PowerPoint.Presentation.SaveAs
编码来源https://my.oschina.net/zxcholmes/blog/484789 编码来源https://my.oschina.net/zxcholmes/blog/484789
""" """
TTYPES = {
"JPG" : 17, TTYPES = {"JPG": 17, "PNG": 18, "PDF": 32, "XPS": 33}
"PNG" : 18,
"PDF" : 32,
"XPS" : 33
}
def __init__(self, file: str | Path, trans: str = "JPG") -> None: def __init__(self, file: str | Path, trans: str = "JPG") -> None:
if sys.platform != "win32": if sys.platform != "win32":
@@ -35,7 +33,7 @@ class convertPPT(object):
if trans_upper not in convertPPT.TTYPES: if trans_upper not in convertPPT.TTYPES:
raise ValueError("Save type is not supported.") raise ValueError("Save type is not supported.")
self.__saveType = (convertPPT.TTYPES[trans_upper], trans_upper) self.__saveType = (convertPPT.TTYPES[trans_upper], trans_upper)
self.__inUsing = wcc.Dispatch('PowerPoint.Application') self.__inUsing = wcc.Dispatch("PowerPoint.Application")
def __enter__(self) -> Self: def __enter__(self) -> Self:
return self return self
@@ -77,8 +75,9 @@ class convertPPT(object):
print("File converted successfully.") print("File converted successfully.")
def PPT_longPic(pptFile: str | Path, saveName: str | None = None, width: int | str | None = None, def PPT_longPic(
saveto: str | Path = ".") -> None: pptFile: str | Path, saveName: str | None = None, width: int | str | None = None, saveto: str | Path = "."
) -> None:
""" """
width : 画幅宽度,可以直接指定宽度像素,也可以使用字符串数据输入百分比。 width : 画幅宽度,可以直接指定宽度像素,也可以使用字符串数据输入百分比。
700 "22.1%" 700 "22.1%"
@@ -88,20 +87,20 @@ def PPT_longPic(pptFile: str | Path, saveName: str | None = None, width: int | s
if saveName: if saveName:
sType = Path(saveName).suffix[1:].upper() if Path(saveName).suffix else "JPG" sType = Path(saveName).suffix[1:].upper() if Path(saveName).suffix else "JPG"
if sType not in ["JPG", "PNG"]: if sType not in ["JPG", "PNG"]:
raise ValueError("Unable to save this type `{}` of image.".format(sType)) raise ValueError(f"Unable to save this type `{sType}` of image.")
else: else:
sType = "JPG" sType = "JPG"
with TemporaryDirectory() as tmpdirname: with TemporaryDirectory() as tmpdirname:
with convertPPT(pptFile, trans=sType) as ppt: with convertPPT(pptFile, trans=sType) as ppt:
ppt.trans(saveto=tmpdirname) ppt.trans(saveto=tmpdirname)
picList = sorted(Path(tmpdirname).glob("*.{}".format(sType))) picList = sorted(Path(tmpdirname).glob(f"*.{sType}"))
if not picList: if not picList:
raise ValueError("No images generated from PPT conversion.") raise ValueError("No images generated from PPT conversion.")
with Image.open(picList[0]) as img: with Image.open(picList[0]) as img:
if isinstance(width, str): if isinstance(width, str):
qw = float(width.rstrip('%')) / 100.0 qw = float(width.rstrip("%")) / 100.0
nwidth, nheight = (int(img.width * qw), int(img.height * qw)) nwidth, nheight = (int(img.width * qw), int(img.height * qw))
elif width is None: elif width is None:
nwidth, nheight = img.size nwidth, nheight = img.size
@@ -114,7 +113,7 @@ def PPT_longPic(pptFile: str | Path, saveName: str | None = None, width: int | s
new_img = img.resize((nwidth, nheight), resample=Image.Resampling.LANCZOS) new_img = img.resize((nwidth, nheight), resample=Image.Resampling.LANCZOS)
canvas.paste(new_img, box=(0, (i - 1) * nheight)) canvas.paste(new_img, box=(0, (i - 1) * nheight))
filepath = pptFile.parent if Path(saveto) == Path('.') else Path(saveto) filepath = pptFile.parent if Path(saveto) == Path(".") else Path(saveto)
if saveName: if saveName:
if Path(saveName).suffix: if Path(saveName).suffix:
canvas.save(filepath / saveName) canvas.save(filepath / saveName)
@@ -123,15 +122,20 @@ def PPT_longPic(pptFile: str | Path, saveName: str | None = None, width: int | s
else: else:
canvas.save(filepath / pptFile.name, format=sType) canvas.save(filepath / pptFile.name, format=sType)
def imageOptimization(imageFile: str | Path | Image.Image, saveFile: str | Path | None = None,
max_width: int = None, max_height: int = None, def imageOptimization(
engine: str | None = "pngquant", imageFile: str | Path | Image.Image,
engine_conf: str | None = None) -> Image.Image | None: 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进行无损压缩也可以设置为其他图片无损压缩引擎 默认建议使用pngquant进行无损压缩也可以设置为其他图片无损压缩引擎
不需要针对性压缩可设定engine为None。 不需要针对性压缩可设定engine为None。
""" """
if isinstance(imageFile, (str, Path)): if isinstance(imageFile, str | Path):
imageFile = Image.open(imageFile) imageFile = Image.open(imageFile)
img = cv2.cvtColor(np.array(imageFile), cv2.COLOR_RGB2BGR) img = cv2.cvtColor(np.array(imageFile), cv2.COLOR_RGB2BGR)

View File

@@ -1,12 +1,13 @@
from pathlib import Path from pathlib import Path
from simtoolsz.utils import today
import typer import typer
from simtoolsz.utils import today
from pptopic.lib import PPT_longPic, imageOptimization from pptopic.lib import PPT_longPic, imageOptimization
app = typer.Typer() app = typer.Typer()
@app.command() @app.command()
def export( def export(
input_file: Path = typer.Argument(..., help="输入的PPTX文件路径", exists=True), input_file: Path = typer.Argument(..., help="输入的PPTX文件路径", exists=True),
@@ -16,8 +17,10 @@ def export(
optimize: bool = typer.Option(False, "--optimize", help="是否优化导出的图片"), optimize: bool = typer.Option(False, "--optimize", help="是否优化导出的图片"),
max_height: int = typer.Option(29999, "--max-height", help="优化图片的最大高度默认29999"), max_height: int = typer.Option(29999, "--max-height", help="优化图片的最大高度默认29999"),
engine: str = typer.Option("pngquant", "--engine", help="图片优化引擎默认pngquant"), engine: str = typer.Option("pngquant", "--engine", help="图片优化引擎默认pngquant"),
engine_conf: str = typer.Option("--skip-if-larger", "--engine-conf", help="图片优化引擎配置参数(默认--skip-if-larger"), engine_conf: str = typer.Option(
format: str = typer.Option("PNG", "--format", "-f", help="输出格式JPG或PNG默认PNG") "--skip-if-larger", "--engine-conf", help="图片优化引擎配置参数(默认--skip-if-larger"
),
format: str = typer.Option("PNG", "--format", "-f", help="输出格式JPG或PNG默认PNG"),
): ):
""" """
将PPTX文件导出为长图 将PPTX文件导出为长图
@@ -31,14 +34,9 @@ def export(
if output_name: if output_name:
save_name = f"{output_name}.{format.lower()}" if "." not in output_name else output_name save_name = f"{output_name}.{format.lower()}" if "." not in output_name else output_name
else: else:
save_name = f"{input_file.stem}_{today(fmt="YYYYMMDDHHmm",addtime=True)}.{format.lower()}" save_name = f"{input_file.stem}_{today(fmt='YYYYMMDDHHmm', addtime=True)}.{format.lower()}"
PPT_longPic( PPT_longPic(pptFile=input_file, saveName=save_name, width=width, saveto=output_dir)
pptFile=input_file,
saveName=save_name,
width=width,
saveto=output_dir
)
if optimize: if optimize:
output_path = output_dir / save_name output_path = output_dir / save_name
@@ -48,7 +46,7 @@ def export(
saveFile=output_path, saveFile=output_path,
max_height=max_height, max_height=max_height,
engine=engine, engine=engine,
engine_conf=engine_conf engine_conf=engine_conf,
) )
typer.echo(f"成功导出长图: {output_dir / save_name}") typer.echo(f"成功导出长图: {output_dir / save_name}")
@@ -56,13 +54,16 @@ def export(
typer.echo(f"导出失败: {e}", err=True) typer.echo(f"导出失败: {e}", err=True)
raise typer.Exit(code=1) raise typer.Exit(code=1)
@app.command() @app.command()
def optimize( def optimize(
input_file: Path = typer.Argument(..., help="输入的图片文件路径", exists=True), input_file: Path = typer.Argument(..., help="输入的图片文件路径", exists=True),
output_file: Path = typer.Option(None, "--output", "-o", help="输出图片文件路径,默认覆盖原文件"), output_file: Path = typer.Option(None, "--output", "-o", help="输出图片文件路径,默认覆盖原文件"),
max_height: int = typer.Option(29999, "--max-height", help="优化图片的最大高度默认29999"), max_height: int = typer.Option(29999, "--max-height", help="优化图片的最大高度默认29999"),
engine: str = typer.Option("pngquant", "--engine", help="图片优化引擎默认pngquant"), engine: str = typer.Option("pngquant", "--engine", help="图片优化引擎默认pngquant"),
engine_conf: str = typer.Option("--skip-if-larger", "--engine-conf", help="图片优化引擎配置参数(默认--skip-if-larger") engine_conf: str = typer.Option(
"--skip-if-larger", "--engine-conf", help="图片优化引擎配置参数(默认--skip-if-larger"
),
): ):
""" """
单独优化图片 单独优化图片
@@ -76,11 +77,7 @@ def optimize(
save_file = output_file if output_file else input_file save_file = output_file if output_file else input_file
imageOptimization( imageOptimization(
imageFile=input_file, imageFile=input_file, saveFile=save_file, max_height=max_height, engine=engine, engine_conf=engine_conf
saveFile=save_file,
max_height=max_height,
engine=engine,
engine_conf=engine_conf
) )
typer.echo(f"成功优化图片: {save_file}") typer.echo(f"成功优化图片: {save_file}")
@@ -88,5 +85,15 @@ def optimize(
typer.echo(f"优化失败: {e}", err=True) typer.echo(f"优化失败: {e}", err=True)
raise typer.Exit(code=1) raise typer.Exit(code=1)
@app.command()
def version():
"""显示版本信息"""
from pptopic import __version__
typer.echo(f"pptopic version: {__version__}")
typer.echo("Under the MIT License.")
def main(): def main():
app() app()

2
uv.lock generated
View File

@@ -291,7 +291,7 @@ wheels = [
[[package]] [[package]]
name = "pptopic" name = "pptopic"
version = "0.1.0" version = "0.3.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "numpy" }, { name = "numpy" },