docs: 更新 README 并新增 pngquant 自动安装脚本
This commit is contained in:
121
README.md
121
README.md
@@ -4,86 +4,83 @@
|
|||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
- 📊 **PPT 转长图**:将 PPTX 文件导出为一张连续的长图
|
- 将 PPTX 文件的所有幻灯片垂直拼接为一张长图
|
||||||
- 🖼️ **图片优化**:支持使用 `pngquant` 进行无损压缩
|
- 支持 JPG 和 PNG 格式输出
|
||||||
- ⚙️ **灵活配置**:可自定义输出尺寸、格式、保存路径等
|
- 支持自定义输出宽度和目录
|
||||||
|
- 内置图片优化功能,使用图片优化引擎进行无损压缩
|
||||||
## 环境要求
|
|
||||||
|
|
||||||
- **Windows 系统**(依赖 PowerPoint COM 接口)
|
|
||||||
- Python ≥ 3.13
|
|
||||||
- 已安装 Microsoft PowerPoint
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
```bash
|
### 安装 uv
|
||||||
pip install pptopic
|
|
||||||
```
|
|
||||||
|
|
||||||
如需图片优化功能,请安装 `pngquant`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scoop install pngquant
|
# 使用 scoop
|
||||||
|
scoop install uv
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以使用你熟悉的图片优化引擎替代 `pngquant` ,但一般情况下,我更推荐 `pngquant` 。
|
### 安装 pptopic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
### 导出长图
|
### 导出 PPTX 为长图
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 基本用法
|
# 基本用法
|
||||||
pptopic export presentation.pptx
|
pptopic export presentation.pptx
|
||||||
|
|
||||||
# 指定输出文件名和目录
|
# 指定输出文件名
|
||||||
pptopic export presentation.pptx -o result.png -d ./output
|
pptopic export presentation.pptx --output result.png
|
||||||
|
|
||||||
# 自定义宽度并启用优化
|
# 优化导出的图片
|
||||||
pptopic export presentation.pptx --width 1200 --optimize
|
|
||||||
```
|
|
||||||
|
|
||||||
### 优化图片
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 优化现有图片
|
|
||||||
pptopic optimize image.png
|
|
||||||
|
|
||||||
# 指定输出路径和最大高度
|
|
||||||
pptopic optimize image.png -o optimized.png --max-height 20000
|
|
||||||
```
|
|
||||||
|
|
||||||
## 命令选项
|
|
||||||
|
|
||||||
### export 命令
|
|
||||||
|
|
||||||
| 选项 | 简写 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `--output` | `-o` | 输出文件名 |
|
|
||||||
| `--dir` | `-d` | 输出目录(默认当前目录) |
|
|
||||||
| `--width` | `-w` | 导出图片宽度(像素) |
|
|
||||||
| `--format` | `-f` | 输出格式:JPG 或 PNG(默认 PNG) |
|
|
||||||
| `--optimize` | - | 启用图片优化 |
|
|
||||||
| `--max-height` | - | 优化时最大高度(默认 29999) |
|
|
||||||
|
|
||||||
### optimize 命令
|
|
||||||
|
|
||||||
| 选项 | 简写 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `--output` | `-o` | 输出文件路径 |
|
|
||||||
| `--max-height` | - | 最大高度限制 |
|
|
||||||
| `--engine` | - | 优化引擎(默认 pngquant) |
|
|
||||||
|
|
||||||
## 示例
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 导出并优化
|
|
||||||
pptopic export presentation.pptx --optimize
|
pptopic export presentation.pptx --optimize
|
||||||
|
|
||||||
# 导出为 JPG 格式
|
# 自定义宽度和最大高度
|
||||||
pptopic export presentation.pptx -f JPG -o result
|
pptopic export presentation.pptx --width 1080 --optimize --max-height 20000
|
||||||
```
|
```
|
||||||
|
|
||||||
## 许可证
|
### 单独优化图片
|
||||||
|
|
||||||
MIT License
|
```bash
|
||||||
|
# 优化图片
|
||||||
|
pptopic optimize image.png
|
||||||
|
|
||||||
|
# 指定输出文件
|
||||||
|
pptopic optimize image.png --output optimized.png
|
||||||
|
|
||||||
|
# 自定义优化参数
|
||||||
|
pptopic optimize image.png --max-height 20000 --engine pngquant
|
||||||
|
```
|
||||||
|
|
||||||
|
## 系统要求
|
||||||
|
|
||||||
|
- Windows 系统(使用 win32com 调用 PowerPoint)
|
||||||
|
- Python >= 3.13
|
||||||
|
- Microsoft PowerPoint
|
||||||
|
|
||||||
|
## 图片优化
|
||||||
|
|
||||||
|
推荐使用 [pngquant](https://pngquant.org/) 进行图片压缩。
|
||||||
|
|
||||||
|
### 自动安装脚本
|
||||||
|
|
||||||
|
使用提供的 PowerShell 脚本自动安装 pngquant:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\install-pngquant.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
该脚本会自动:
|
||||||
|
1. 检查 pngquant 是否已安装
|
||||||
|
2. 如果未安装,检查 scoop 是否已安装
|
||||||
|
3. 如果 scoop 未安装,自动安装 scoop
|
||||||
|
4. 使用 scoop 安装 pngquant
|
||||||
|
|
||||||
|
### 手动安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scoop install pngquant
|
||||||
|
```
|
||||||
|
|||||||
114
install-pngquant.ps1
Normal file
114
install-pngquant.ps1
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
param(
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Test-PngquantInstalled {
|
||||||
|
try {
|
||||||
|
$result = Get-Command pngquant -ErrorAction Stop
|
||||||
|
Write-Host "pngquant 已安装: $($result.Source)" -ForegroundColor Green
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-Host "pngquant 未安装" -ForegroundColor Yellow
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ScoopInstalled {
|
||||||
|
try {
|
||||||
|
$result = Get-Command scoop -ErrorAction Stop
|
||||||
|
Write-Host "scoop 已安装: $($result.Source)" -ForegroundColor Green
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-Host "scoop 未安装" -ForegroundColor Yellow
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-Scoop {
|
||||||
|
Write-Host "正在安装 scoop..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$scoopInstallScript = "irm get.scoop.sh | iex"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-Expression $scoopInstallScript
|
||||||
|
Write-Host "scoop 安装成功" -ForegroundColor Green
|
||||||
|
|
||||||
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","User") + ";" + [System.Environment]::GetEnvironmentVariable("Path","Machine")
|
||||||
|
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-Host "scoop 安装失败: $_" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-Pngquant {
|
||||||
|
Write-Host "正在使用 scoop 安装 pngquant..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
try {
|
||||||
|
scoop install pngquant
|
||||||
|
Write-Host "pngquant 安装成功" -ForegroundColor Green
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-Host "pngquant 安装失败: $_" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " pngquant 安装脚本" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if (Test-PngquantInstalled) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "pngquant 已经安装,无需重复安装。" -ForegroundColor Green
|
||||||
|
if (-not $Force) {
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
Write-Host "使用 -Force 参数强制重新安装..." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if (-not (Test-ScoopInstalled)) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "scoop 未安装,正在安装 scoop..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
if (-not (Install-Scoop)) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "scoop 安装失败,请手动安装后重试。" -ForegroundColor Red
|
||||||
|
Write-Host "手动安装命令: irm get.scoop.sh | iex" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "请重新运行此脚本以继续安装 pngquant。" -ForegroundColor Yellow
|
||||||
|
Write-Host "或者手动运行: scoop install pngquant" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if (Install-Pngquant) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
|
Write-Host " pngquant 安装完成!" -ForegroundColor Green
|
||||||
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
|
|
||||||
|
$pngquantPath = Get-Command pngquant -ErrorAction SilentlyContinue
|
||||||
|
if ($pngquantPath) {
|
||||||
|
Write-Host "安装路径: $($pngquantPath.Source)" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "验证安装:" -ForegroundColor Cyan
|
||||||
|
pngquant --version
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "pngquant 安装失败。" -ForegroundColor Red
|
||||||
|
Write-Host "请尝试手动安装: scoop install pngquant" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pptopic"
|
name = "pptopic"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -16,9 +20,47 @@ dependencies = [
|
|||||||
"typer>=0.21.2",
|
"typer>=0.21.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"pytest-cov>=4.0.0",
|
||||||
|
"pytest-mock>=3.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
pptopic = "pptopic:main"
|
pptopic = "pptopic:main"
|
||||||
|
|
||||||
[build-system]
|
[tool.pytest.ini_options]
|
||||||
requires = ["uv_build>=0.9.18,<0.10.0"]
|
testpaths = ["tests"]
|
||||||
build-backend = "uv_build"
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"-ra",
|
||||||
|
"-q",
|
||||||
|
"--strict-markers",
|
||||||
|
"--strict-config",
|
||||||
|
]
|
||||||
|
markers = [
|
||||||
|
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||||
|
"integration: marks tests as integration tests",
|
||||||
|
"unit: marks tests as unit tests",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["src/pptopic"]
|
||||||
|
omit = [
|
||||||
|
"*/tests/*",
|
||||||
|
"*/test_*.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"def __repr__",
|
||||||
|
"raise AssertionError",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"if __name__ == .__main__.:",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
"@abstractmethod",
|
||||||
|
]
|
||||||
|
|||||||
16
pytest.ini
Normal file
16
pytest.ini
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"-ra",
|
||||||
|
"-q",
|
||||||
|
"--strict-markers",
|
||||||
|
"--strict-config",
|
||||||
|
]
|
||||||
|
markers = [
|
||||||
|
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||||
|
"integration: marks tests as integration tests",
|
||||||
|
"unit: marks tests as unit tests",
|
||||||
|
]
|
||||||
@@ -22,17 +22,17 @@ class convertPPT:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
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":
|
if sys.platform != "win32":
|
||||||
raise SystemError("Only support Windows system.")
|
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.__file = Path(file)
|
||||||
self.__saveType = (convertPPT.TTYPES[trans.upper()], trans.upper())
|
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")
|
self.__inUsing = wcc.Dispatch("PowerPoint.Application")
|
||||||
|
|
||||||
def __enter__(self) -> Self:
|
def __enter__(self) -> Self:
|
||||||
@@ -50,21 +50,19 @@ class convertPPT:
|
|||||||
return cls(file, trans=trans.upper())
|
return cls(file, trans=trans.upper())
|
||||||
|
|
||||||
def saveAs(self, saveType: str | None = None) -> None:
|
def saveAs(self, saveType: str | None = None) -> None:
|
||||||
if saveType not in convertPPT.TTYPES.keys():
|
if saveType is None:
|
||||||
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")
|
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:
|
def trans(self, saveto: str | Path = ".", width: int | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
saveto : 保存路径,默认为当前路径。
|
saveto : 保存路径,默认为当前路径。
|
||||||
"""
|
"""
|
||||||
ppt = self.__inUsing.Presentations.Open(self.__file.absolute())
|
ppt = self.__inUsing.Presentations.Open(self.__file.absolute())
|
||||||
if Path(saveto) == Path("."):
|
|
||||||
output = self.__file.absolute()
|
|
||||||
else:
|
|
||||||
output = Path(saveto).absolute()
|
output = Path(saveto).absolute()
|
||||||
if width is not None:
|
if width is not None:
|
||||||
ppt.Export(output, self.__saveType[1], width)
|
ppt.Export(output, self.__saveType[1], width)
|
||||||
@@ -85,40 +83,44 @@ def PPT_longPic(
|
|||||||
(700; "22.1%")
|
(700; "22.1%")
|
||||||
指定为None的时候,不进行图像缩放。
|
指定为None的时候,不进行图像缩放。
|
||||||
"""
|
"""
|
||||||
|
pptFile = Path(pptFile)
|
||||||
if saveName:
|
if saveName:
|
||||||
sType = saveName.split(".")[-1] if "." in saveName else "JPG"
|
sType = Path(saveName).suffix[1:].upper() if Path(saveName).suffix else "JPG"
|
||||||
if sType.upper() not in ["JPG", "PNG"]:
|
if sType not in ["JPG", "PNG"]:
|
||||||
raise ValueError(f"Unable to save this type `{sType}` of image.")
|
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 = list(Path(tmpdirname).glob(f"*.{sType}"))
|
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:
|
with Image.open(picList[0]) as img:
|
||||||
if isinstance(width, str):
|
if isinstance(width, str):
|
||||||
qw = float(width[:-1]) / 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
|
||||||
else:
|
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)))
|
canvas = Image.new(img.mode, (nwidth, nheight * len(picList)))
|
||||||
for i in range(1, len(picList) + 1):
|
|
||||||
with Image.open(Path(tmpdirname).joinpath(f"幻灯片{i}.{sType}")) as img:
|
for i, picPath in enumerate(picList, 1):
|
||||||
|
with Image.open(picPath) as img:
|
||||||
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))
|
||||||
if Path(saveto) == Path("."):
|
|
||||||
filepath = Path(pptFile).parent
|
filepath = pptFile.parent if Path(saveto) == Path(".") else Path(saveto)
|
||||||
else:
|
|
||||||
filepath = Path(saveto)
|
|
||||||
if saveName:
|
if saveName:
|
||||||
if "." in saveName:
|
if Path(saveName).suffix:
|
||||||
canvas.save(filepath.joinpath(saveName))
|
canvas.save(filepath / saveName)
|
||||||
else:
|
else:
|
||||||
canvas.save(filepath.joinpath(saveName), format=sType)
|
canvas.save(filepath / saveName, format=sType)
|
||||||
else:
|
else:
|
||||||
canvas.save(filepath.joinpath(Path(pptFile).stem), format=sType)
|
canvas.save(filepath / pptFile.name, format=sType)
|
||||||
|
|
||||||
|
|
||||||
def imageOptimization(
|
def imageOptimization(
|
||||||
@@ -136,40 +138,48 @@ def imageOptimization(
|
|||||||
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)
|
||||||
|
|
||||||
if max_width or max_height:
|
if max_width or max_height:
|
||||||
height, width, _ = img.shape
|
height, width, _ = img.shape
|
||||||
oShape = (width, height)
|
new_width, new_height = width, height
|
||||||
|
|
||||||
if 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)
|
new_width = int(width * max_width)
|
||||||
height = int(height * 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:
|
||||||
if height > max_height:
|
|
||||||
width = int(width * max_height / height)
|
|
||||||
height = max_height
|
|
||||||
if max_height < 1:
|
if max_height < 1:
|
||||||
if width < oShape[0]:
|
if new_width < width:
|
||||||
if height / oShape[1] > max_height:
|
if new_height / height > max_height:
|
||||||
height = int(oShape[1] * max_height)
|
new_height = int(height * max_height)
|
||||||
width = int(oShape[0] * max_height)
|
new_width = int(width * max_height)
|
||||||
else:
|
else:
|
||||||
height = int(height * max_height)
|
new_height = int(new_height * max_height)
|
||||||
width = int(width * max_height)
|
new_width = int(new_width * max_height)
|
||||||
img = cv2.resize(img, (width, 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))
|
res = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
||||||
with TemporaryDirectory(prefix="pytoolsz") as tmpFolder:
|
with TemporaryDirectory(prefix="pytoolsz") as tmpFolder:
|
||||||
res.save(Path(tmpFolder) / "tmp.png", compress_level=2, quality=100)
|
tmpPath = Path(tmpFolder) / "tmp.png"
|
||||||
|
res.save(tmpPath, compress_level=2, quality=100)
|
||||||
|
|
||||||
if engine:
|
if engine:
|
||||||
try:
|
try:
|
||||||
subprocess.run([engine, engine_conf, Path(tmpFolder) / "tmp.png"], shell=True, check=True)
|
subprocess.run([engine, engine_conf, tmpPath], shell=True, check=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("未安装pngquant,不能进行图片优化压缩。\n可使用`scoop install pngquant`进行安装。")
|
print("未安装pngquant,不能进行图片优化压缩。\n可使用`scoop install pngquant`进行安装。")
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
res.save(Path(tmpFolder) / "tmp.png", compress_level=7, quality=95)
|
res.save(tmpPath, compress_level=7, quality=95)
|
||||||
|
|
||||||
if saveFile is None:
|
if saveFile is None:
|
||||||
res = Image.open(lastFile(Path(tmpFolder), "*.*"))
|
res = Image.open(lastFile(Path(tmpFolder), "*.*"))
|
||||||
return res
|
return res
|
||||||
|
|||||||
49
tests/README.md
Normal file
49
tests/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 测试
|
||||||
|
|
||||||
|
此目录包含 pptopic 库的测试文件。
|
||||||
|
|
||||||
|
## 运行测试
|
||||||
|
|
||||||
|
运行所有测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
运行测试并生成覆盖率报告:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest --cov=pptopic --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
运行特定测试文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_convert.py
|
||||||
|
```
|
||||||
|
|
||||||
|
运行特定测试类:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_convert.py::TestConvertPPTClass
|
||||||
|
```
|
||||||
|
|
||||||
|
运行特定测试函数:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_convert.py::TestConvertPPTClass::test_init_with_valid_file
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试结构
|
||||||
|
|
||||||
|
- `test_convert.py` - `convertPPT` 类的测试
|
||||||
|
- `test_longpic.py` - `PPT_longPic` 函数的测试
|
||||||
|
- `test_optimization.py` - `imageOptimization` 函数的测试
|
||||||
|
|
||||||
|
## 测试标记
|
||||||
|
|
||||||
|
测试可以使用以下标记:
|
||||||
|
|
||||||
|
- `slow` - 慢速测试(可以使用 `-m "not slow"` 跳过)
|
||||||
|
- `integration` - 集成测试
|
||||||
|
- `unit` - 单元测试
|
||||||
3
tests/__init__.py
Normal file
3
tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""Test suite for pptopic library."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
150
tests/test_convert.py
Normal file
150
tests/test_convert.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""Tests for convertPPT class."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock, MagicMock, patch
|
||||||
|
from pptopic.lib import convertPPT
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertPPTClass:
|
||||||
|
"""Test convertPPT class initialization and properties."""
|
||||||
|
|
||||||
|
def test_ttypes_mapping(self):
|
||||||
|
"""Test that TTYPES mapping is correct."""
|
||||||
|
assert convertPPT.TTYPES["JPG"] == 17
|
||||||
|
assert convertPPT.TTYPES["PNG"] == 18
|
||||||
|
assert convertPPT.TTYPES["PDF"] == 32
|
||||||
|
assert convertPPT.TTYPES["XPS"] == 33
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_init_with_valid_file(self, tmp_path):
|
||||||
|
"""Test initialization with a valid file path."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with patch('pptopic.lib.wcc.Dispatch') as mock_dispatch:
|
||||||
|
mock_app = MagicMock()
|
||||||
|
mock_dispatch.return_value = mock_app
|
||||||
|
|
||||||
|
ppt = convertPPT(test_file, trans="JPG")
|
||||||
|
assert ppt.savetype == "JPG"
|
||||||
|
mock_dispatch.assert_called_once_with('PowerPoint.Application')
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_init_with_path_object(self, tmp_path):
|
||||||
|
"""Test initialization with Path object."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with patch('pptopic.lib.wcc.Dispatch'):
|
||||||
|
ppt = convertPPT(Path(test_file), trans="PNG")
|
||||||
|
assert ppt.savetype == "PNG"
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_init_with_nonexistent_file(self):
|
||||||
|
"""Test initialization with nonexistent file raises FileNotFoundError."""
|
||||||
|
with pytest.raises(FileNotFoundError, match="File not found"):
|
||||||
|
convertPPT("nonexistent.pptx")
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_init_with_invalid_trans_type(self, tmp_path):
|
||||||
|
"""Test initialization with invalid save type raises ValueError."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Save type is not supported"):
|
||||||
|
convertPPT(test_file, trans="BMP")
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_init_with_case_insensitive_trans(self, tmp_path):
|
||||||
|
"""Test that trans parameter is case insensitive."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with patch('pptopic.lib.wcc.Dispatch'):
|
||||||
|
ppt = convertPPT(test_file, trans="jpg")
|
||||||
|
assert ppt.savetype == "JPG"
|
||||||
|
|
||||||
|
ppt = convertPPT(test_file, trans="Pdf")
|
||||||
|
assert ppt.savetype == "PDF"
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertPTTNonWindows:
|
||||||
|
"""Test convertPPT behavior on non-Windows systems."""
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'linux')
|
||||||
|
def test_init_on_non_windows_raises_error(self):
|
||||||
|
"""Test that initialization on non-Windows raises SystemError."""
|
||||||
|
with pytest.raises(SystemError, match="Only support Windows system"):
|
||||||
|
convertPPT("test.pptx")
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertPPTContextManager:
|
||||||
|
"""Test convertPPT as context manager."""
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_context_manager_enter_exit(self, tmp_path):
|
||||||
|
"""Test __enter__ and __exit__ methods."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with patch('pptopic.lib.wcc.Dispatch') as mock_dispatch:
|
||||||
|
mock_app = MagicMock()
|
||||||
|
mock_dispatch.return_value = mock_app
|
||||||
|
|
||||||
|
with convertPPT(test_file) as ppt:
|
||||||
|
assert ppt.savetype == "JPG"
|
||||||
|
|
||||||
|
mock_app.Quit.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertPPTSaveAs:
|
||||||
|
"""Test saveAs method."""
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_saveas_default(self, tmp_path):
|
||||||
|
"""Test saveAs with default parameter."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with patch('pptopic.lib.wcc.Dispatch'):
|
||||||
|
ppt = convertPPT(test_file, trans="PNG")
|
||||||
|
ppt.saveAs()
|
||||||
|
assert ppt.savetype == "JPG"
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_saveas_with_valid_type(self, tmp_path):
|
||||||
|
"""Test saveAs with valid save type."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with patch('pptopic.lib.wcc.Dispatch'):
|
||||||
|
ppt = convertPPT(test_file, trans="JPG")
|
||||||
|
ppt.saveAs("PDF")
|
||||||
|
assert ppt.savetype == "PDF"
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_saveas_with_invalid_type(self, tmp_path):
|
||||||
|
"""Test saveAs with invalid save type raises ValueError."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with patch('pptopic.lib.wcc.Dispatch'):
|
||||||
|
ppt = convertPPT(test_file, trans="JPG")
|
||||||
|
with pytest.raises(ValueError, match="Save type is not supported"):
|
||||||
|
ppt.saveAs("BMP")
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertPTTOpenClassmethod:
|
||||||
|
"""Test open class method."""
|
||||||
|
|
||||||
|
@patch('pptopic.lib.sys.platform', 'win32')
|
||||||
|
def test_open_classmethod(self, tmp_path):
|
||||||
|
"""Test open class method creates instance."""
|
||||||
|
test_file = tmp_path / "test.pptx"
|
||||||
|
test_file.touch()
|
||||||
|
|
||||||
|
with patch('pptopic.lib.wcc.Dispatch'):
|
||||||
|
ppt = convertPPT.open(test_file, trans="PNG")
|
||||||
|
assert ppt.savetype == "PNG"
|
||||||
|
assert isinstance(ppt, convertPPT)
|
||||||
244
tests/test_longpic.py
Normal file
244
tests/test_longpic.py
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
"""Tests for PPT_longPic function."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock, MagicMock, patch
|
||||||
|
from PIL import Image
|
||||||
|
from pptopic.lib import PPT_longPic
|
||||||
|
|
||||||
|
|
||||||
|
class TestPPTLongPic:
|
||||||
|
"""Test PPT_longPic function."""
|
||||||
|
|
||||||
|
@patch('pptopic.lib.convertPPT')
|
||||||
|
def test_ppt_longpic_with_save_name(self, mock_convert_ppt, tmp_path):
|
||||||
|
"""Test PPT_longPic with saveName parameter."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
mock_ppt = MagicMock()
|
||||||
|
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Path.glob') as mock_glob:
|
||||||
|
mock_img_path = MagicMock()
|
||||||
|
mock_img_path.suffix = '.jpg'
|
||||||
|
mock_glob.return_value = [mock_img_path]
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.open') as mock_open:
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_img.width = 100
|
||||||
|
mock_img.height = 100
|
||||||
|
mock_img.resize.return_value = mock_img
|
||||||
|
mock_open.return_value.__enter__.return_value = mock_img
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.new') as mock_new:
|
||||||
|
mock_canvas = MagicMock()
|
||||||
|
mock_new.return_value = mock_canvas
|
||||||
|
|
||||||
|
PPT_longPic(ppt_file, saveName="output.jpg", saveto=tmp_path)
|
||||||
|
|
||||||
|
mock_canvas.save.assert_called_once()
|
||||||
|
|
||||||
|
@patch('pptopic.lib.convertPPT')
|
||||||
|
def test_ppt_longpic_without_save_name(self, mock_convert_ppt, tmp_path):
|
||||||
|
"""Test PPT_longPic without saveName uses default JPG."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
mock_ppt = MagicMock()
|
||||||
|
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Path.glob') as mock_glob:
|
||||||
|
mock_img_path = MagicMock()
|
||||||
|
mock_img_path.suffix = '.jpg'
|
||||||
|
mock_glob.return_value = [mock_img_path]
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.open') as mock_open:
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_img.width = 100
|
||||||
|
mock_img.height = 100
|
||||||
|
mock_img.resize.return_value = mock_img
|
||||||
|
mock_open.return_value.__enter__.return_value = mock_img
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.new') as mock_new:
|
||||||
|
mock_canvas = MagicMock()
|
||||||
|
mock_new.return_value = mock_canvas
|
||||||
|
|
||||||
|
PPT_longPic(ppt_file, saveto=tmp_path)
|
||||||
|
|
||||||
|
mock_canvas.save.assert_called_once()
|
||||||
|
|
||||||
|
def test_ppt_longpic_invalid_image_type(self, tmp_path):
|
||||||
|
"""Test PPT_longPic with invalid image type raises ValueError."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Unable to save this type"):
|
||||||
|
PPT_longPic(ppt_file, saveName="output.bmp")
|
||||||
|
|
||||||
|
@patch('pptopic.lib.convertPPT')
|
||||||
|
def test_ppt_longpic_no_images_generated(self, mock_convert_ppt, tmp_path):
|
||||||
|
"""Test PPT_longPic when no images are generated raises ValueError."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
mock_ppt = MagicMock()
|
||||||
|
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Path.glob', return_value=[]):
|
||||||
|
with pytest.raises(ValueError, match="No images generated"):
|
||||||
|
PPT_longPic(ppt_file)
|
||||||
|
|
||||||
|
@patch('pptopic.lib.convertPPT')
|
||||||
|
def test_ppt_longpic_with_percentage_width(self, mock_convert_ppt, tmp_path):
|
||||||
|
"""Test PPT_longPic with percentage width string."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
mock_ppt = MagicMock()
|
||||||
|
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Path.glob') as mock_glob:
|
||||||
|
mock_img_path = MagicMock()
|
||||||
|
mock_img_path.suffix = '.jpg'
|
||||||
|
mock_glob.return_value = [mock_img_path]
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.open') as mock_open:
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_img.width = 100
|
||||||
|
mock_img.height = 100
|
||||||
|
mock_img.resize.return_value = mock_img
|
||||||
|
mock_open.return_value.__enter__.return_value = mock_img
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.new') as mock_new:
|
||||||
|
mock_canvas = MagicMock()
|
||||||
|
mock_new.return_value = mock_canvas
|
||||||
|
|
||||||
|
PPT_longPic(ppt_file, width="50%", saveto=tmp_path)
|
||||||
|
|
||||||
|
mock_new.assert_called_once()
|
||||||
|
|
||||||
|
@patch('pptopic.lib.convertPPT')
|
||||||
|
def test_ppt_longpic_with_pixel_width(self, mock_convert_ppt, tmp_path):
|
||||||
|
"""Test PPT_longPic with pixel width."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
mock_ppt = MagicMock()
|
||||||
|
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Path.glob') as mock_glob:
|
||||||
|
mock_img_path = MagicMock()
|
||||||
|
mock_img_path.suffix = '.jpg'
|
||||||
|
mock_glob.return_value = [mock_img_path]
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.open') as mock_open:
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_img.width = 100
|
||||||
|
mock_img.height = 100
|
||||||
|
mock_img.resize.return_value = mock_img
|
||||||
|
mock_open.return_value.__enter__.return_value = mock_img
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.new') as mock_new:
|
||||||
|
mock_canvas = MagicMock()
|
||||||
|
mock_new.return_value = mock_canvas
|
||||||
|
|
||||||
|
PPT_longPic(ppt_file, width=50, saveto=tmp_path)
|
||||||
|
|
||||||
|
mock_new.assert_called_once()
|
||||||
|
|
||||||
|
@patch('pptopic.lib.convertPPT')
|
||||||
|
def test_ppt_longpic_with_none_width(self, mock_convert_ppt, tmp_path):
|
||||||
|
"""Test PPT_longPic with None width (no scaling)."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
mock_ppt = MagicMock()
|
||||||
|
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Path.glob') as mock_glob:
|
||||||
|
mock_img_path = MagicMock()
|
||||||
|
mock_img_path.suffix = '.jpg'
|
||||||
|
mock_glob.return_value = [mock_img_path]
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.open') as mock_open:
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_img.width = 100
|
||||||
|
mock_img.height = 100
|
||||||
|
mock_img.resize.return_value = mock_img
|
||||||
|
mock_open.return_value.__enter__.return_value = mock_img
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.new') as mock_new:
|
||||||
|
mock_canvas = MagicMock()
|
||||||
|
mock_new.return_value = mock_canvas
|
||||||
|
|
||||||
|
PPT_longPic(ppt_file, width=None, saveto=tmp_path)
|
||||||
|
|
||||||
|
mock_new.assert_called_once()
|
||||||
|
|
||||||
|
@patch('pptopic.lib.convertPPT')
|
||||||
|
def test_ppt_longpic_save_to_current_dir(self, mock_convert_ppt, tmp_path):
|
||||||
|
"""Test PPT_longPic saving to current directory."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
mock_ppt = MagicMock()
|
||||||
|
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Path.glob') as mock_glob:
|
||||||
|
mock_img_path = MagicMock()
|
||||||
|
mock_img_path.suffix = '.jpg'
|
||||||
|
mock_glob.return_value = [mock_img_path]
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.open') as mock_open:
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_img.width = 100
|
||||||
|
mock_img.height = 100
|
||||||
|
mock_img.resize.return_value = mock_img
|
||||||
|
mock_open.return_value.__enter__.return_value = mock_img
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.new') as mock_new:
|
||||||
|
mock_canvas = MagicMock()
|
||||||
|
mock_new.return_value = mock_canvas
|
||||||
|
|
||||||
|
PPT_longPic(ppt_file, saveto=".")
|
||||||
|
|
||||||
|
mock_canvas.save.assert_called_once()
|
||||||
|
|
||||||
|
@patch('pptopic.lib.convertPPT')
|
||||||
|
def test_ppt_longpic_multiple_images(self, mock_convert_ppt, tmp_path):
|
||||||
|
"""Test PPT_longPic with multiple images."""
|
||||||
|
ppt_file = tmp_path / "test.pptx"
|
||||||
|
ppt_file.touch()
|
||||||
|
|
||||||
|
mock_ppt = MagicMock()
|
||||||
|
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Path.glob') as mock_glob:
|
||||||
|
mock_img_path1 = MagicMock()
|
||||||
|
mock_img_path1.suffix = '.jpg'
|
||||||
|
mock_img_path2 = MagicMock()
|
||||||
|
mock_img_path2.suffix = '.jpg'
|
||||||
|
mock_glob.return_value = [mock_img_path1, mock_img_path2]
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.open') as mock_open:
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_img.width = 100
|
||||||
|
mock_img.height = 100
|
||||||
|
mock_img.resize.return_value = mock_img
|
||||||
|
mock_open.return_value.__enter__.return_value = mock_img
|
||||||
|
|
||||||
|
with patch('pptopic.lib.Image.new') as mock_new:
|
||||||
|
mock_canvas = MagicMock()
|
||||||
|
mock_new.return_value = mock_canvas
|
||||||
|
|
||||||
|
PPT_longPic(ppt_file, saveto=tmp_path)
|
||||||
|
|
||||||
|
mock_canvas.paste.assert_called()
|
||||||
227
tests/test_optimization.py
Normal file
227
tests/test_optimization.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"""Tests for imageOptimization function."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock, MagicMock, patch
|
||||||
|
from PIL import Image
|
||||||
|
import numpy as np
|
||||||
|
from pptopic.lib import imageOptimization
|
||||||
|
|
||||||
|
|
||||||
|
class TestImageOptimization:
|
||||||
|
"""Test imageOptimization function."""
|
||||||
|
|
||||||
|
@patch('pptopic.lib.Image.open')
|
||||||
|
@patch('pptopic.lib.cv2.cvtColor')
|
||||||
|
@patch('pptopic.lib.cv2.resize')
|
||||||
|
@patch('pptopic.lib.subprocess.run')
|
||||||
|
@patch('pptopic.lib.lastFile')
|
||||||
|
def test_image_optimization_with_file_path(self, mock_last_file, mock_subprocess,
|
||||||
|
mock_resize, mock_cvtcolor, mock_open, tmp_path):
|
||||||
|
"""Test imageOptimization with file path input."""
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.size = (100, 100)
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_open.return_value = mock_img
|
||||||
|
|
||||||
|
mock_array = np.array([[[255, 0, 0]]])
|
||||||
|
mock_cvtcolor.return_value = mock_array
|
||||||
|
|
||||||
|
mock_resized = np.array([[[255, 0, 0]]])
|
||||||
|
mock_resize.return_value = mock_resized
|
||||||
|
|
||||||
|
mock_pil_img = MagicMock()
|
||||||
|
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
|
||||||
|
with patch('pptopic.lib.Image.open', return_value=mock_img):
|
||||||
|
mock_last_file.return_value = tmp_path / "optimized.png"
|
||||||
|
|
||||||
|
result = imageOptimization(tmp_path / "test.png")
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@patch('pptopic.lib.cv2.cvtColor')
|
||||||
|
@patch('pptopic.lib.cv2.resize')
|
||||||
|
@patch('pptopic.lib.subprocess.run')
|
||||||
|
@patch('pptopic.lib.lastFile')
|
||||||
|
def test_image_optimization_with_pil_image(self, mock_last_file, mock_subprocess,
|
||||||
|
mock_resize, mock_cvtcolor, tmp_path):
|
||||||
|
"""Test imageOptimization with PIL Image input."""
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.size = (100, 100)
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
|
||||||
|
mock_array = np.array([[[255, 0, 0]]])
|
||||||
|
mock_cvtcolor.return_value = mock_array
|
||||||
|
|
||||||
|
mock_resized = np.array([[[255, 0, 0]]])
|
||||||
|
mock_resize.return_value = mock_resized
|
||||||
|
|
||||||
|
mock_pil_img = MagicMock()
|
||||||
|
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
|
||||||
|
with patch('pptopic.lib.Image.open', return_value=mock_img):
|
||||||
|
mock_last_file.return_value = tmp_path / "optimized.png"
|
||||||
|
|
||||||
|
result = imageOptimization(mock_img)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@patch('pptopic.lib.Image.open')
|
||||||
|
@patch('pptopic.lib.cv2.cvtColor')
|
||||||
|
@patch('pptopic.lib.cv2.resize')
|
||||||
|
@patch('pptopic.lib.subprocess.run')
|
||||||
|
@patch('pptopic.lib.shutil.copyfile')
|
||||||
|
def test_image_optimization_with_save_file(self, mock_copyfile, mock_subprocess,
|
||||||
|
mock_resize, mock_cvtcolor, mock_open, tmp_path):
|
||||||
|
"""Test imageOptimization with saveFile parameter."""
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.size = (100, 100)
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_open.return_value = mock_img
|
||||||
|
|
||||||
|
mock_array = np.array([[[255, 0, 0]]])
|
||||||
|
mock_cvtcolor.return_value = mock_array
|
||||||
|
|
||||||
|
mock_resized = np.array([[[255, 0, 0]]])
|
||||||
|
mock_resize.return_value = mock_resized
|
||||||
|
|
||||||
|
mock_pil_img = MagicMock()
|
||||||
|
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
|
||||||
|
with patch('pptopic.lib.lastFile') as mock_last_file:
|
||||||
|
mock_last_file.return_value = tmp_path / "optimized.png"
|
||||||
|
|
||||||
|
result = imageOptimization(tmp_path / "test.png", saveFile=tmp_path / "output.png")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
mock_copyfile.assert_called_once()
|
||||||
|
|
||||||
|
@patch('pptopic.lib.Image.open')
|
||||||
|
@patch('pptopic.lib.cv2.cvtColor')
|
||||||
|
@patch('pptopic.lib.cv2.resize')
|
||||||
|
@patch('pptopic.lib.subprocess.run')
|
||||||
|
@patch('pptopic.lib.lastFile')
|
||||||
|
def test_image_optimization_with_max_width(self, mock_last_file, mock_subprocess,
|
||||||
|
mock_resize, mock_cvtcolor, mock_open, tmp_path):
|
||||||
|
"""Test imageOptimization with max_width parameter."""
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.size = (200, 100)
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_open.return_value = mock_img
|
||||||
|
|
||||||
|
mock_array = np.zeros((100, 200, 3), dtype=np.uint8)
|
||||||
|
mock_cvtcolor.return_value = mock_array
|
||||||
|
|
||||||
|
mock_resized = np.zeros((50, 100, 3), dtype=np.uint8)
|
||||||
|
mock_resize.return_value = mock_resized
|
||||||
|
|
||||||
|
mock_pil_img = MagicMock()
|
||||||
|
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
|
||||||
|
with patch('pptopic.lib.Image.open', return_value=mock_img):
|
||||||
|
mock_last_file.return_value = tmp_path / "optimized.png"
|
||||||
|
|
||||||
|
result = imageOptimization(tmp_path / "test.png", max_width=100)
|
||||||
|
|
||||||
|
mock_resize.assert_called_once()
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@patch('pptopic.lib.Image.open')
|
||||||
|
@patch('pptopic.lib.cv2.cvtColor')
|
||||||
|
@patch('pptopic.lib.cv2.resize')
|
||||||
|
@patch('pptopic.lib.subprocess.run')
|
||||||
|
@patch('pptopic.lib.lastFile')
|
||||||
|
def test_image_optimization_with_max_height(self, mock_last_file, mock_subprocess,
|
||||||
|
mock_resize, mock_cvtcolor, mock_open, tmp_path):
|
||||||
|
"""Test imageOptimization with max_height parameter."""
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.size = (100, 200)
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_open.return_value = mock_img
|
||||||
|
|
||||||
|
mock_array = np.zeros((200, 100, 3), dtype=np.uint8)
|
||||||
|
mock_cvtcolor.return_value = mock_array
|
||||||
|
|
||||||
|
mock_resized = np.zeros((100, 50, 3), dtype=np.uint8)
|
||||||
|
mock_resize.return_value = mock_resized
|
||||||
|
|
||||||
|
mock_pil_img = MagicMock()
|
||||||
|
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
|
||||||
|
with patch('pptopic.lib.Image.open', return_value=mock_img):
|
||||||
|
mock_last_file.return_value = tmp_path / "optimized.png"
|
||||||
|
|
||||||
|
result = imageOptimization(tmp_path / "test.png", max_height=100)
|
||||||
|
|
||||||
|
mock_resize.assert_called_once()
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@patch('pptopic.lib.Image.open')
|
||||||
|
@patch('pptopic.lib.cv2.cvtColor')
|
||||||
|
@patch('pptopic.lib.cv2.resize')
|
||||||
|
@patch('pptopic.lib.subprocess.run')
|
||||||
|
@patch('pptopic.lib.lastFile')
|
||||||
|
def test_image_optimization_with_fractional_width(self, mock_last_file, mock_subprocess,
|
||||||
|
mock_resize, mock_cvtcolor, mock_open, tmp_path):
|
||||||
|
"""Test imageOptimization with fractional max_width (< 1)."""
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.size = (200, 100)
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_open.return_value = mock_img
|
||||||
|
|
||||||
|
mock_array = np.zeros((100, 200, 3), dtype=np.uint8)
|
||||||
|
mock_cvtcolor.return_value = mock_array
|
||||||
|
|
||||||
|
mock_resized = np.zeros((50, 100, 3), dtype=np.uint8)
|
||||||
|
mock_resize.return_value = mock_resized
|
||||||
|
|
||||||
|
mock_pil_img = MagicMock()
|
||||||
|
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
|
||||||
|
with patch('pptopic.lib.Image.open', return_value=mock_img):
|
||||||
|
mock_last_file.return_value = tmp_path / "optimized.png"
|
||||||
|
|
||||||
|
result = imageOptimization(tmp_path / "test.png", max_width=0.5)
|
||||||
|
|
||||||
|
mock_resize.assert_called_once()
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@patch('pptopic.lib.Image.open')
|
||||||
|
@patch('pptopic.lib.cv2.cvtColor')
|
||||||
|
@patch('pptopic.lib.subprocess.run')
|
||||||
|
@patch('pptopic.lib.lastFile')
|
||||||
|
def test_image_optimization_without_engine(self, mock_last_file, mock_subprocess,
|
||||||
|
mock_cvtcolor, mock_open, tmp_path):
|
||||||
|
"""Test imageOptimization with engine=None."""
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.size = (100, 100)
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_open.return_value = mock_img
|
||||||
|
|
||||||
|
mock_array = np.array([[[255, 0, 0]]])
|
||||||
|
mock_cvtcolor.return_value = mock_array
|
||||||
|
|
||||||
|
mock_pil_img = MagicMock()
|
||||||
|
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
|
||||||
|
with patch('pptopic.lib.Image.open', return_value=mock_img):
|
||||||
|
mock_last_file.return_value = tmp_path / "optimized.png"
|
||||||
|
|
||||||
|
result = imageOptimization(tmp_path / "test.png", engine=None)
|
||||||
|
|
||||||
|
mock_subprocess.assert_not_called()
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
@patch('pptopic.lib.Image.open')
|
||||||
|
@patch('pptopic.lib.cv2.cvtColor')
|
||||||
|
@patch('pptopic.lib.subprocess.run')
|
||||||
|
def test_image_optimization_engine_failure(self, mock_subprocess, mock_cvtcolor, mock_open):
|
||||||
|
"""Test imageOptimization when engine subprocess fails."""
|
||||||
|
mock_img = MagicMock()
|
||||||
|
mock_img.size = (100, 100)
|
||||||
|
mock_img.mode = 'RGB'
|
||||||
|
mock_open.return_value = mock_img
|
||||||
|
|
||||||
|
mock_array = np.array([[[255, 0, 0]]])
|
||||||
|
mock_cvtcolor.return_value = mock_array
|
||||||
|
|
||||||
|
mock_subprocess.side_effect = Exception("Engine not found")
|
||||||
|
|
||||||
|
mock_pil_img = MagicMock()
|
||||||
|
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
imageOptimization("test.png", engine="pngquant", engine_conf="--quality=85")
|
||||||
Reference in New Issue
Block a user