chore: 添加 MIT LICENSE、完善 README 并忽略 .vscode 文件夹
This commit is contained in:
@@ -1 +1,7 @@
|
||||
from pptopic.main import main
|
||||
|
||||
__version__ = "0.3.0"
|
||||
|
||||
__author__ = "Sidney Zhang <zly@lyzhang.me>"
|
||||
|
||||
__all__ = ["main"]
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
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
|
||||
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"]
|
||||
|
||||
class convertPPT(object):
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
TTYPES = {"JPG": 17, "PNG": 18, "PDF": 32, "XPS": 33}
|
||||
__all__ = ["savetype", "trans", "close", "open"]
|
||||
|
||||
def __init__(self, file:str|Path, trans:str = "JPG") -> None:
|
||||
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():
|
||||
@@ -35,114 +33,126 @@ class convertPPT(object):
|
||||
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')
|
||||
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 :
|
||||
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 :
|
||||
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 :
|
||||
|
||||
def trans(self, saveto: str | Path = ".", width: int | None = None) -> None:
|
||||
"""
|
||||
saveto : 保存路径,默认为当前路径。
|
||||
"""
|
||||
ppt = self.__inUsing.Presentations.Open(self.__file.absolute())
|
||||
if Path(saveto) == Path('.') :
|
||||
if Path(saveto) == Path("."):
|
||||
output = self.__file.absolute()
|
||||
else :
|
||||
else:
|
||||
output = Path(saveto).absolute()
|
||||
if width is not None :
|
||||
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 :
|
||||
|
||||
def close(self, to_console: bool = False) -> None:
|
||||
self.__inUsing.Quit()
|
||||
if to_console :
|
||||
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 :
|
||||
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 :
|
||||
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))
|
||||
if sType.upper() 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 = list(Path(tmpdirname).glob("*.{}".format(sType)))
|
||||
with Image.open(picList[0]) as img :
|
||||
picList = list(Path(tmpdirname).glob(f"*.{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))
|
||||
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))
|
||||
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('.') :
|
||||
for i in range(1, len(picList) + 1):
|
||||
with Image.open(Path(tmpdirname).joinpath(f"幻灯片{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 :
|
||||
else:
|
||||
filepath = Path(saveto)
|
||||
if saveName :
|
||||
if '.' in saveName :
|
||||
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 :
|
||||
|
||||
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)):
|
||||
if isinstance(imageFile, str | Path):
|
||||
imageFile = Image.open(imageFile)
|
||||
img = cv2.cvtColor(np.array(imageFile), cv2.COLOR_RGB2BGR)
|
||||
if max_width or max_height :
|
||||
if max_width or max_height:
|
||||
height, width, _ = img.shape
|
||||
oShape = (width, height)
|
||||
if max_width :
|
||||
if width > max_width :
|
||||
if max_width:
|
||||
if width > max_width:
|
||||
height = int(height * max_width / width)
|
||||
width = max_width
|
||||
if max_width < 1 :
|
||||
if max_width < 1:
|
||||
width = int(width * max_width)
|
||||
height = int(height * max_width)
|
||||
if max_height :
|
||||
if height > max_height :
|
||||
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 :
|
||||
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:
|
||||
@@ -150,18 +160,18 @@ def imageOptimization(imageFile:str|Path|Image.Image, saveFile:str|Path|None = N
|
||||
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 :
|
||||
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)
|
||||
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.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)
|
||||
else:
|
||||
shutil.copyfile(lastFile(Path(tmpFolder), "*.*"), saveFile)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from pathlib import Path
|
||||
from simtoolsz.utils import today
|
||||
|
||||
import typer
|
||||
from simtoolsz.utils import today
|
||||
|
||||
from pptopic.lib import PPT_longPic, imageOptimization
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def export(
|
||||
input_file: Path = typer.Argument(..., help="输入的PPTX文件路径", exists=True),
|
||||
@@ -16,12 +17,14 @@ def export(
|
||||
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)")
|
||||
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
|
||||
@@ -31,42 +34,40 @@ def export(
|
||||
if output_name:
|
||||
save_name = f"{output_name}.{format.lower()}" if "." not in output_name else output_name
|
||||
else:
|
||||
save_name = f"{input_file.stem}_{today(fmt="YYYYMMDDHHmm",addtime=True)}.{format.lower()}"
|
||||
|
||||
PPT_longPic(
|
||||
pptFile=input_file,
|
||||
saveName=save_name,
|
||||
width=width,
|
||||
saveto=output_dir
|
||||
)
|
||||
|
||||
save_name = f"{input_file.stem}_{today(fmt='YYYYMMDDHHmm', addtime=True)}.{format.lower()}"
|
||||
|
||||
PPT_longPic(pptFile=input_file, saveName=save_name, width=width, saveto=output_dir)
|
||||
|
||||
if optimize:
|
||||
output_path = output_dir / save_name
|
||||
|
||||
|
||||
imageOptimization(
|
||||
imageFile=output_path,
|
||||
saveFile=output_path,
|
||||
max_height=max_height,
|
||||
engine=engine,
|
||||
engine_conf=engine_conf
|
||||
engine_conf=engine_conf,
|
||||
)
|
||||
|
||||
|
||||
typer.echo(f"成功导出长图: {output_dir / save_name}")
|
||||
except Exception as e:
|
||||
typer.echo(f"导出失败: {e}", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def optimize(
|
||||
input_file: Path = typer.Argument(..., help="输入的图片文件路径", exists=True),
|
||||
output_file: Path = typer.Option(None, "--output", "-o", 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)")
|
||||
engine_conf: str = typer.Option(
|
||||
"--skip-if-larger", "--engine-conf", help="图片优化引擎配置参数(默认--skip-if-larger)"
|
||||
),
|
||||
):
|
||||
"""
|
||||
单独优化图片
|
||||
|
||||
|
||||
示例:
|
||||
pptopic optimize image.png
|
||||
pptopic optimize image.png --output optimized.png --max-height 20000
|
||||
@@ -74,19 +75,25 @@ def optimize(
|
||||
"""
|
||||
try:
|
||||
save_file = output_file if output_file else input_file
|
||||
|
||||
|
||||
imageOptimization(
|
||||
imageFile=input_file,
|
||||
saveFile=save_file,
|
||||
max_height=max_height,
|
||||
engine=engine,
|
||||
engine_conf=engine_conf
|
||||
imageFile=input_file, saveFile=save_file, max_height=max_height, engine=engine, engine_conf=engine_conf
|
||||
)
|
||||
|
||||
|
||||
typer.echo(f"成功优化图片: {save_file}")
|
||||
except Exception as e:
|
||||
typer.echo(f"优化失败: {e}", err=True)
|
||||
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():
|
||||
app()
|
||||
|
||||
Reference in New Issue
Block a user