@ww_nero/skills 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/{pptx_to_pdf.py → pptx_to_png.py} +31 -17
- package/index.js +11 -10
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""使用 LibreOffice 无头模式将 PPT/PPTX
|
|
1
|
+
"""使用 LibreOffice 无头模式将 PPT/PPTX 转换为 PNG 图片。"""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
@@ -9,6 +9,9 @@ import subprocess
|
|
|
9
9
|
import sys
|
|
10
10
|
import tempfile
|
|
11
11
|
from pathlib import Path
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
from pdf2image import convert_from_path
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def _detect_libreoffice() -> str:
|
|
@@ -27,7 +30,6 @@ def _detect_libreoffice() -> str:
|
|
|
27
30
|
if base:
|
|
28
31
|
candidates.append(str(Path(base) / 'LibreOffice' / 'program' / 'soffice.exe'))
|
|
29
32
|
|
|
30
|
-
# 通用命令放在末尾,优先尝试绝对路径
|
|
31
33
|
candidates.extend(['soffice', 'libreoffice'])
|
|
32
34
|
|
|
33
35
|
for candidate in candidates:
|
|
@@ -42,21 +44,22 @@ def _detect_libreoffice() -> str:
|
|
|
42
44
|
return ''
|
|
43
45
|
|
|
44
46
|
|
|
45
|
-
def
|
|
47
|
+
def pptx_to_png(input_file: str | Path, output_dir: str | Path, dpi: int = 300) -> List[Path]:
|
|
48
|
+
"""将 PPTX 转换为 PNG 图片,返回生成的文件路径列表。"""
|
|
46
49
|
pptx_path = Path(input_file).expanduser().resolve()
|
|
47
50
|
if not pptx_path.exists():
|
|
48
51
|
raise FileNotFoundError(f'PPT/PPTX 文件不存在: {pptx_path}')
|
|
49
52
|
if pptx_path.is_dir():
|
|
50
53
|
raise ValueError(f'输入路径不是文件: {pptx_path}')
|
|
51
54
|
|
|
52
|
-
output_path = Path(
|
|
53
|
-
output_path.
|
|
55
|
+
output_path = Path(output_dir).expanduser().resolve()
|
|
56
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
54
57
|
|
|
55
58
|
libreoffice_cmd = _detect_libreoffice()
|
|
56
59
|
if not libreoffice_cmd:
|
|
57
60
|
raise RuntimeError('未找到 LibreOffice,请先安装(例如 apt install libreoffice 或从官网安装包)。')
|
|
58
61
|
|
|
59
|
-
with tempfile.TemporaryDirectory(prefix='
|
|
62
|
+
with tempfile.TemporaryDirectory(prefix='pptx_to_png_') as temp_dir:
|
|
60
63
|
cmd = [
|
|
61
64
|
libreoffice_cmd,
|
|
62
65
|
'--headless',
|
|
@@ -71,33 +74,44 @@ def pptx_to_pdf(input_file: str | Path, output_file: str | Path) -> Path:
|
|
|
71
74
|
message = result.stderr.strip() or result.stdout.strip() or 'LibreOffice 返回非零退出码'
|
|
72
75
|
raise RuntimeError(f'LibreOffice 转换失败: {message}')
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
if not
|
|
77
|
+
generated_pdf = Path(temp_dir) / f'{pptx_path.stem}.pdf'
|
|
78
|
+
if not generated_pdf.exists():
|
|
76
79
|
pdf_candidates = list(Path(temp_dir).glob('*.pdf'))
|
|
77
80
|
if len(pdf_candidates) == 1:
|
|
78
|
-
|
|
81
|
+
generated_pdf = pdf_candidates[0]
|
|
79
82
|
else:
|
|
80
83
|
raise RuntimeError('未找到 LibreOffice 生成的 PDF 文件')
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
images = convert_from_path(str(generated_pdf), dpi=dpi, fmt='png')
|
|
86
|
+
|
|
87
|
+
saved_files: List[Path] = []
|
|
88
|
+
for index, image in enumerate(images, start=1):
|
|
89
|
+
file_name = f'page_{index:04d}.png'
|
|
90
|
+
target = output_path / file_name
|
|
91
|
+
image.save(str(target), 'PNG')
|
|
92
|
+
saved_files.append(target)
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
return output_path
|
|
94
|
+
return saved_files
|
|
87
95
|
|
|
88
96
|
|
|
89
97
|
def _parse_args() -> argparse.Namespace:
|
|
90
|
-
parser = argparse.ArgumentParser(description='将 PPT/PPTX 转换为
|
|
98
|
+
parser = argparse.ArgumentParser(description='将 PPT/PPTX 转换为 PNG 图片(调用 LibreOffice + pdf2image)。')
|
|
91
99
|
parser.add_argument('--input', required=True, help='PPT/PPTX 文件路径')
|
|
92
|
-
parser.add_argument('--output', required=True, help='
|
|
100
|
+
parser.add_argument('--output-dir', required=True, help='保存图片的目录')
|
|
101
|
+
parser.add_argument('--dpi', type=int, default=300, help='输出图片的 DPI,默认 300')
|
|
93
102
|
return parser.parse_args()
|
|
94
103
|
|
|
95
104
|
|
|
96
105
|
def main() -> None:
|
|
97
106
|
args = _parse_args()
|
|
98
107
|
try:
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
results = pptx_to_png(args.input, args.output_dir, dpi=args.dpi)
|
|
109
|
+
if not results:
|
|
110
|
+
print('未生成任何图片')
|
|
111
|
+
return
|
|
112
|
+
print(f'成功生成 {len(results)} 张图片:')
|
|
113
|
+
for path in results:
|
|
114
|
+
print(path)
|
|
101
115
|
except Exception as error:
|
|
102
116
|
print(f'转换失败: {error}', file=sys.stderr)
|
|
103
117
|
sys.exit(1)
|
package/index.js
CHANGED
|
@@ -96,14 +96,15 @@ const SNIPPETS = {
|
|
|
96
96
|
'如需大批量转换,可将 convert_from_path 的线程参数打开以提升性能。'
|
|
97
97
|
]
|
|
98
98
|
},
|
|
99
|
-
|
|
100
|
-
title: 'PPTX 转
|
|
101
|
-
description: '使用 LibreOffice
|
|
102
|
-
codeFile: '
|
|
99
|
+
pptx_to_png: {
|
|
100
|
+
title: 'PPTX 转 PNG',
|
|
101
|
+
description: '使用 LibreOffice 无头模式转换为 PDF,再通过 pdf2image 转成 PNG 图片,兼容 Windows/macOS/Linux。',
|
|
102
|
+
codeFile: 'pptx_to_png.py',
|
|
103
103
|
notes: [
|
|
104
104
|
'脚本会自动探测 soffice 路径,如未安装需先安装 LibreOffice。',
|
|
105
|
-
'
|
|
106
|
-
'
|
|
105
|
+
'内部先转 PDF 再转 PNG,中间文件在临时目录处理,不会残留。',
|
|
106
|
+
'支持自定义 dpi,默认 300dpi 用于打印级清晰度。',
|
|
107
|
+
'输出目录会自动创建,文件名形如 page_0001.png,便于批量处理。'
|
|
107
108
|
]
|
|
108
109
|
},
|
|
109
110
|
svg_to_pptx: {
|
|
@@ -156,7 +157,7 @@ const GUIDES = {
|
|
|
156
157
|
|---------|-------------|------|
|
|
157
158
|
| HTML → PNG | \`html_to_png\` | 使用 Playwright 渲染并截图 |
|
|
158
159
|
| PDF → PNG | \`pdf_to_png\` | 使用 pdf2image 分页转换 |
|
|
159
|
-
| PPTX →
|
|
160
|
+
| PPTX → PNG | \`pptx_to_png\` | 使用 LibreOffice + pdf2image |
|
|
160
161
|
| SVG → PPTX | \`svg_to_pptx\` | 转化为可编辑的 PPTX 元素 |
|
|
161
162
|
|
|
162
163
|
调用示例:使用 \`snippet\` 工具获取参考代码,如 \`snippet({ title: "pdf_to_png" })\`
|
|
@@ -215,7 +216,7 @@ const buildSnippet = (title, workingDirectory) => {
|
|
|
215
216
|
fs.writeFileSync(outputPath, code);
|
|
216
217
|
|
|
217
218
|
const lines = [];
|
|
218
|
-
lines.push(
|
|
219
|
+
lines.push(`示例脚本已经存储到工作目录下,文件名: ${entry.codeFile},可直接作为工具模块导入使用。`);
|
|
219
220
|
if (entry.notes && entry.notes.length > 0) {
|
|
220
221
|
lines.push('');
|
|
221
222
|
lines.push('注意事项:');
|
|
@@ -255,7 +256,7 @@ const listTools = () => ({
|
|
|
255
256
|
},
|
|
256
257
|
title: {
|
|
257
258
|
type: 'string',
|
|
258
|
-
description: '示例类型,可选: html_to_png | pdf_to_png |
|
|
259
|
+
description: '示例类型,可选: html_to_png | pdf_to_png | pptx_to_png | svg_to_pptx',
|
|
259
260
|
enum: Object.keys(SNIPPETS)
|
|
260
261
|
}
|
|
261
262
|
},
|
|
@@ -280,7 +281,7 @@ const listTools = () => ({
|
|
|
280
281
|
]
|
|
281
282
|
});
|
|
282
283
|
|
|
283
|
-
const server = new Server({ name: 'skills', version: '2.4.
|
|
284
|
+
const server = new Server({ name: 'skills', version: '2.4.2' }, { capabilities: { tools: {} } });
|
|
284
285
|
|
|
285
286
|
server.setRequestHandler(ListToolsRequestSchema, async () => listTools());
|
|
286
287
|
|