@ww_nero/skills 2.2.0
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/README.md +45 -0
- package/assets/demo.jpg +0 -0
- package/assets/html_to_png.py +100 -0
- package/assets/pdf_to_png.py +58 -0
- package/assets/ppt_slide.html +159 -0
- package/assets/pptx_to_pdf.py +107 -0
- package/assets/svg_to_pptx.py +613 -0
- package/index.js +231 -0
- package/package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @ww_nero/snippet
|
|
2
|
+
|
|
3
|
+
MCP 服务器,不直接执行转换,而是根据 `type` 返回可直接运行的 Python 参考脚本和要点说明。支持四类示例:
|
|
4
|
+
|
|
5
|
+
- `html_to_png`:Playwright 渲染 HTML 后截图
|
|
6
|
+
- `pdf_to_png`:pdf2image 分页导出位图
|
|
7
|
+
- `pptx_to_pdf`:LibreOffice 无头模式转 PDF
|
|
8
|
+
- `svg_to_pptx`:附带 `ppt_slide.html` 与对应的构建脚本,演示渐变背景、发光文本和贝塞尔曲线
|
|
9
|
+
|
|
10
|
+
## MCP 配置示例
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
"snippet": {
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["-y", "@ww_nero/snippet@latest"],
|
|
16
|
+
"env": {},
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
工具列表中仅包含 `convert`,调用时传入:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"type": "html_to_png" // 可选: html_to_png | pdf_to_png | pptx_to_pdf | svg_to_pptx
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
参数说明:
|
|
30
|
+
- `type`:示例类型,取值仅限 `html_to_png` / `pdf_to_png` / `pptx_to_pdf` / `svg_to_pptx`。
|
|
31
|
+
|
|
32
|
+
返回内容包括:
|
|
33
|
+
|
|
34
|
+
1. 关键要点(参数、注意事项、风格处理建议等)。
|
|
35
|
+
2. 完整的 Python 示例脚本(位于 `assets/` 下)。
|
|
36
|
+
3. 如需额外资源(例如 SVG),会同时内联展示并标注路径。
|
|
37
|
+
|
|
38
|
+
## 运行脚本所需依赖(如需执行)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install playwright pdf2image pillow python-pptx
|
|
42
|
+
playwright install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
示例脚本默认使用简体中文注释,可直接复制到本地运行或按需修改。
|
package/assets/demo.jpg
ADDED
|
Binary file
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Convert a single HTML file to PNG via Playwright screenshot."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from playwright.sync_api import sync_playwright
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MIN_WIDTH = 320
|
|
12
|
+
MIN_HEIGHT = 240
|
|
13
|
+
DEVICE_SCALE_FACTOR = 4
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_target_element(page):
|
|
17
|
+
body = page.query_selector('body')
|
|
18
|
+
if body is None:
|
|
19
|
+
raise ValueError('未找到 body 元素')
|
|
20
|
+
|
|
21
|
+
top_level_elements = body.query_selector_all(':scope > *')
|
|
22
|
+
if not top_level_elements:
|
|
23
|
+
raise ValueError('body 中未找到可截图元素')
|
|
24
|
+
|
|
25
|
+
# 单个最外层节点时直接使用该节点,否则使用整个 body
|
|
26
|
+
return top_level_elements[0] if len(top_level_elements) == 1 else body
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _measure_element(element):
|
|
30
|
+
size = element.evaluate(
|
|
31
|
+
"""
|
|
32
|
+
(el) => {
|
|
33
|
+
const rect = el.getBoundingClientRect();
|
|
34
|
+
const width = Math.max(rect.width || 0, el.scrollWidth || 0, el.offsetWidth || 0);
|
|
35
|
+
const height = Math.max(rect.height || 0, el.scrollHeight || 0, el.offsetHeight || 0);
|
|
36
|
+
return { width, height };
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
)
|
|
40
|
+
width = max(1, int(size.get('width') or 0))
|
|
41
|
+
height = max(1, int(size.get('height') or 0))
|
|
42
|
+
return width, height
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def convert_html_to_png(input_file: str | Path, output_file: str | Path, wait_ms: int = 1500) -> Path:
|
|
46
|
+
"""Convert a single HTML file to PNG image."""
|
|
47
|
+
html_path = Path(input_file).expanduser().resolve()
|
|
48
|
+
if not html_path.exists():
|
|
49
|
+
raise FileNotFoundError(f'HTML 文件不存在: {html_path}')
|
|
50
|
+
|
|
51
|
+
output_path = Path(output_file).expanduser().resolve()
|
|
52
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
with sync_playwright() as playwright:
|
|
55
|
+
browser = playwright.chromium.launch(headless=True)
|
|
56
|
+
context = browser.new_context(
|
|
57
|
+
viewport={'width': MIN_WIDTH, 'height': MIN_HEIGHT},
|
|
58
|
+
device_scale_factor=DEVICE_SCALE_FACTOR
|
|
59
|
+
)
|
|
60
|
+
page = context.new_page()
|
|
61
|
+
try:
|
|
62
|
+
file_url = html_path.as_uri()
|
|
63
|
+
page.goto(file_url)
|
|
64
|
+
page.wait_for_timeout(wait_ms)
|
|
65
|
+
|
|
66
|
+
target = _find_target_element(page)
|
|
67
|
+
element_width, element_height = _measure_element(target)
|
|
68
|
+
viewport_width = max(MIN_WIDTH, element_width)
|
|
69
|
+
viewport_height = max(MIN_HEIGHT, element_height)
|
|
70
|
+
|
|
71
|
+
page.set_viewport_size({'width': viewport_width, 'height': viewport_height})
|
|
72
|
+
page.wait_for_timeout(200)
|
|
73
|
+
target.screenshot(path=str(output_path))
|
|
74
|
+
finally:
|
|
75
|
+
context.close()
|
|
76
|
+
browser.close()
|
|
77
|
+
|
|
78
|
+
return output_path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_args() -> argparse.Namespace:
|
|
82
|
+
parser = argparse.ArgumentParser(description='将 HTML 文件转换为 PNG 图片。')
|
|
83
|
+
parser.add_argument('--input', required=True, help='HTML 文件路径')
|
|
84
|
+
parser.add_argument('--output', required=True, help='输出 PNG 文件路径')
|
|
85
|
+
parser.add_argument('--wait', type=int, default=1500, help='页面加载等待时间,单位毫秒,默认 1500')
|
|
86
|
+
return parser.parse_args()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main() -> None:
|
|
90
|
+
args = _parse_args()
|
|
91
|
+
try:
|
|
92
|
+
result = convert_html_to_png(args.input, args.output, wait_ms=args.wait)
|
|
93
|
+
print(f'已生成 PNG: {result}')
|
|
94
|
+
except Exception as error:
|
|
95
|
+
print(f'转换失败: {error}', file=sys.stderr)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == '__main__':
|
|
100
|
+
main()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Convert PDF pages to PNG files."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from pdf2image import convert_from_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def convert_pdf_to_png(input_file: str | Path, output_dir: str | Path, dpi: int = 300, fmt: str = 'png') -> List[Path]:
|
|
13
|
+
"""Convert a PDF into PNG images and return saved file paths."""
|
|
14
|
+
input_path = Path(input_file).expanduser().resolve()
|
|
15
|
+
if not input_path.exists():
|
|
16
|
+
raise FileNotFoundError(f'PDF文件不存在: {input_path}')
|
|
17
|
+
|
|
18
|
+
output_path = Path(output_dir).expanduser().resolve()
|
|
19
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
|
|
21
|
+
format_lower = fmt.lower() or 'png'
|
|
22
|
+
images = convert_from_path(str(input_path), dpi=dpi, fmt=format_lower)
|
|
23
|
+
|
|
24
|
+
saved_files: List[Path] = []
|
|
25
|
+
for index, image in enumerate(images, start=1):
|
|
26
|
+
file_name = f'page_{index:04d}.{format_lower}'
|
|
27
|
+
target = output_path / file_name
|
|
28
|
+
image.save(str(target), format_lower.upper())
|
|
29
|
+
saved_files.append(target)
|
|
30
|
+
|
|
31
|
+
return saved_files
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_args() -> argparse.Namespace:
|
|
35
|
+
parser = argparse.ArgumentParser(description='将 PDF 转换为 PNG 图片。')
|
|
36
|
+
parser.add_argument('--input', required=True, help='待转换的 PDF 文件路径')
|
|
37
|
+
parser.add_argument('--output-dir', required=True, help='保存图片的目录')
|
|
38
|
+
parser.add_argument('--dpi', type=int, default=300, help='输出图片的 DPI,默认 300')
|
|
39
|
+
parser.add_argument('--format', default='png', help='输出图片格式,默认 PNG')
|
|
40
|
+
return parser.parse_args()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main() -> None:
|
|
44
|
+
args = _parse_args()
|
|
45
|
+
try:
|
|
46
|
+
results = convert_pdf_to_png(args.input, args.output_dir, dpi=args.dpi, fmt=args.format)
|
|
47
|
+
if not results:
|
|
48
|
+
print('未生成任何图片')
|
|
49
|
+
return
|
|
50
|
+
print(f'成功生成 {len(results)} 张图片:')
|
|
51
|
+
for path in results:
|
|
52
|
+
print(path)
|
|
53
|
+
except Exception as error:
|
|
54
|
+
print(f'转换失败: {error}', file=sys.stderr)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>大模型发展历程 - 神经网络复兴时期</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg">
|
|
10
|
+
<defs>
|
|
11
|
+
<!-- Background Gradient -->
|
|
12
|
+
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
13
|
+
<stop offset="0%" stop-color="#1a237e"/>
|
|
14
|
+
<stop offset="100%" stop-color="#0d1b60"/>
|
|
15
|
+
</linearGradient>
|
|
16
|
+
|
|
17
|
+
<!-- Accent Gradient for Tech Elements -->
|
|
18
|
+
<linearGradient id="accentGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
19
|
+
<stop offset="0%" stop-color="#00e5ff"/>
|
|
20
|
+
<stop offset="100%" stop-color="#2979ff"/>
|
|
21
|
+
</linearGradient>
|
|
22
|
+
|
|
23
|
+
<!-- Glow Effect -->
|
|
24
|
+
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
25
|
+
<feGaussianBlur stdDeviation="2" result="blur"/>
|
|
26
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
27
|
+
</filter>
|
|
28
|
+
|
|
29
|
+
</defs>
|
|
30
|
+
|
|
31
|
+
<!-- Background -->
|
|
32
|
+
<rect width="100%" height="100%" fill="url(#bgGrad)"/>
|
|
33
|
+
|
|
34
|
+
<!-- Decorative Neural Network Background (Right Side) -->
|
|
35
|
+
<g opacity="0.1" stroke="#ffffff" stroke-width="1">
|
|
36
|
+
<line x1="800" y1="200" x2="900" y2="300" />
|
|
37
|
+
<line x1="900" y1="300" x2="850" y2="450" />
|
|
38
|
+
<line x1="850" y1="450" x2="750" y2="400" />
|
|
39
|
+
<line x1="750" y1="400" x2="800" y2="200" />
|
|
40
|
+
<line x1="800" y1="200" x2="650" y2="250" />
|
|
41
|
+
<line x1="650" y1="250" x2="750" y2="400" />
|
|
42
|
+
<circle cx="800" cy="200" r="4" fill="#fff"/>
|
|
43
|
+
<circle cx="900" cy="300" r="4" fill="#fff"/>
|
|
44
|
+
<circle cx="850" cy="450" r="4" fill="#fff"/>
|
|
45
|
+
<circle cx="750" cy="400" r="4" fill="#fff"/>
|
|
46
|
+
<circle cx="650" cy="250" r="4" fill="#fff"/>
|
|
47
|
+
</g>
|
|
48
|
+
|
|
49
|
+
<!-- Image Placeholder - rect (左侧时间线区域右侧) -->
|
|
50
|
+
<rect id="image-placeholder" x="420" y="340" width="180" height="120" fill="rgba(255,255,255,0.1)" stroke="#00e5ff" stroke-width="2" stroke-dasharray="8 4"/>
|
|
51
|
+
|
|
52
|
+
<!-- Image Placeholder - circle (圆形头像占位) -->
|
|
53
|
+
<circle id="image-placeholder-circle" cx="1150" cy="120" r="50" fill="rgba(255,255,255,0.1)" stroke="#00e5ff" stroke-width="2" stroke-dasharray="8 4"/>
|
|
54
|
+
|
|
55
|
+
<!-- Header Section -->
|
|
56
|
+
<g transform="translate(60, 80)">
|
|
57
|
+
<text x="0" y="0" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="48" font-weight="bold" fill="#ffffff" letter-spacing="1">深度学习的崛起(1990-2017)</text>
|
|
58
|
+
<rect x="0" y="20" width="100" height="4" fill="url(#accentGrad)" rx="2"/>
|
|
59
|
+
<line x1="0" y1="22" x2="1160" y2="22" stroke="#ffffff" stroke-width="1" opacity="0.2" />
|
|
60
|
+
</g>
|
|
61
|
+
|
|
62
|
+
<!-- Layout Divider -->
|
|
63
|
+
<line x1="640" y1="180" x2="640" y2="630" stroke="#00e5ff" stroke-width="1" stroke-dasharray="4 4" opacity="0.3" />
|
|
64
|
+
|
|
65
|
+
<!-- Left Column: Technology Breakthroughs -->
|
|
66
|
+
<g transform="translate(60, 180)">
|
|
67
|
+
<text x="0" y="0" font-family="sans-serif" font-size="28" fill="#00e5ff" font-weight="bold">技术突破</text>
|
|
68
|
+
|
|
69
|
+
<!-- Timeline Line -->
|
|
70
|
+
<line x1="20" y1="50" x2="20" y2="480" stroke="#b3e5fc" stroke-width="2" opacity="0.3"/>
|
|
71
|
+
|
|
72
|
+
<!-- 1998 Item -->
|
|
73
|
+
<g transform="translate(0, 70)">
|
|
74
|
+
<circle cx="20" cy="0" r="6" fill="#1a237e" stroke="#00e5ff" stroke-width="2"/>
|
|
75
|
+
<text x="45" y="0" font-family="sans-serif" font-size="24" fill="#ffffff" font-weight="bold" alignment-baseline="middle">1998</text>
|
|
76
|
+
<text x="45" y="30" font-family="sans-serif" font-size="20" fill="#b3e5fc" alignment-baseline="middle">LeNet-5,CNN开创性工作</text>
|
|
77
|
+
</g>
|
|
78
|
+
|
|
79
|
+
<!-- 2012 Item -->
|
|
80
|
+
<g transform="translate(0, 190)">
|
|
81
|
+
<circle cx="20" cy="0" r="6" fill="#1a237e" stroke="#00e5ff" stroke-width="2"/>
|
|
82
|
+
<text x="45" y="0" font-family="sans-serif" font-size="24" fill="#ffffff" font-weight="bold" alignment-baseline="middle">2012</text>
|
|
83
|
+
<text x="45" y="30" font-family="sans-serif" font-size="20" fill="#b3e5fc" alignment-baseline="middle">AlexNet赢得ImageNet竞赛</text>
|
|
84
|
+
</g>
|
|
85
|
+
|
|
86
|
+
<!-- 2014 Item -->
|
|
87
|
+
<g transform="translate(0, 310)">
|
|
88
|
+
<circle cx="20" cy="0" r="6" fill="#1a237e" stroke="#00e5ff" stroke-width="2"/>
|
|
89
|
+
<text x="45" y="0" font-family="sans-serif" font-size="24" fill="#ffffff" font-weight="bold" alignment-baseline="middle">2014</text>
|
|
90
|
+
<text x="45" y="30" font-family="sans-serif" font-size="20" fill="#b3e5fc" alignment-baseline="middle">GAN生成对抗网络</text>
|
|
91
|
+
</g>
|
|
92
|
+
|
|
93
|
+
<!-- 2015 Item -->
|
|
94
|
+
<g transform="translate(0, 430)">
|
|
95
|
+
<circle cx="20" cy="0" r="6" fill="#1a237e" stroke="#00e5ff" stroke-width="2"/>
|
|
96
|
+
<text x="45" y="0" font-family="sans-serif" font-size="24" fill="#ffffff" font-weight="bold" alignment-baseline="middle">2015</text>
|
|
97
|
+
<text x="45" y="30" font-family="sans-serif" font-size="20" fill="#b3e5fc" alignment-baseline="middle">ResNet残差网络</text>
|
|
98
|
+
</g>
|
|
99
|
+
</g>
|
|
100
|
+
|
|
101
|
+
<!-- Right Column: Key Figures -->
|
|
102
|
+
<g transform="translate(688, 180)">
|
|
103
|
+
<text x="0" y="0" font-family="sans-serif" font-size="28" fill="#00e5ff" font-weight="bold">关键人物</text>
|
|
104
|
+
<text x="0" y="35" font-family="sans-serif" font-size="16" fill="#b3e5fc" opacity="0.7">Turing Award Winners ("Godfathers of AI")</text>
|
|
105
|
+
|
|
106
|
+
<!-- Card Container Styles -->
|
|
107
|
+
<defs>
|
|
108
|
+
<path id="cardShape" d="M0 10 L10 0 L380 0 L380 90 L370 100 L0 100 Z" />
|
|
109
|
+
</defs>
|
|
110
|
+
|
|
111
|
+
<!-- Person 1: Hinton -->
|
|
112
|
+
<g transform="translate(0, 70)">
|
|
113
|
+
<use href="#cardShape" fill="rgba(255,255,255,0.05)" stroke="rgba(0,229,255,0.3)" stroke-width="1"/>
|
|
114
|
+
<!-- Decorative corner -->
|
|
115
|
+
<path d="M0 20 L0 10 L10 0 L20 0" fill="none" stroke="#00e5ff" stroke-width="2"/>
|
|
116
|
+
<!-- Content -->
|
|
117
|
+
<text x="30" y="40" font-family="sans-serif" font-size="24" fill="#ffffff" font-weight="bold">Geoffrey Hinton</text>
|
|
118
|
+
<text x="30" y="70" font-family="sans-serif" font-size="16" fill="#b3e5fc">Backpropagation, Deep Belief Nets</text>
|
|
119
|
+
<!-- Tech Icon -->
|
|
120
|
+
<circle cx="340" cy="50" r="20" stroke="#00e5ff" stroke-width="1" fill="none" opacity="0.5"/>
|
|
121
|
+
<circle cx="340" cy="50" r="8" fill="#00e5ff" opacity="0.8"/>
|
|
122
|
+
</g>
|
|
123
|
+
|
|
124
|
+
<!-- Person 2: LeCun -->
|
|
125
|
+
<g transform="translate(0, 190)">
|
|
126
|
+
<use href="#cardShape" fill="rgba(255,255,255,0.05)" stroke="rgba(0,229,255,0.3)" stroke-width="1"/>
|
|
127
|
+
<path d="M0 20 L0 10 L10 0 L20 0" fill="none" stroke="#00e5ff" stroke-width="2"/>
|
|
128
|
+
<text x="30" y="40" font-family="sans-serif" font-size="24" fill="#ffffff" font-weight="bold">Yann LeCun</text>
|
|
129
|
+
<text x="30" y="70" font-family="sans-serif" font-size="16" fill="#b3e5fc">CNN (Convolutional Neural Networks)</text>
|
|
130
|
+
<circle cx="340" cy="50" r="20" stroke="#00e5ff" stroke-width="1" fill="none" opacity="0.5"/>
|
|
131
|
+
<rect x="332" y="42" width="16" height="16" fill="#00e5ff" opacity="0.8"/>
|
|
132
|
+
</g>
|
|
133
|
+
|
|
134
|
+
<!-- Person 3: Bengio -->
|
|
135
|
+
<g transform="translate(0, 310)">
|
|
136
|
+
<use href="#cardShape" fill="rgba(255,255,255,0.05)" stroke="rgba(0,229,255,0.3)" stroke-width="1"/>
|
|
137
|
+
<path d="M0 20 L0 10 L10 0 L20 0" fill="none" stroke="#00e5ff" stroke-width="2"/>
|
|
138
|
+
<text x="30" y="40" font-family="sans-serif" font-size="24" fill="#ffffff" font-weight="bold">Yoshua Bengio</text>
|
|
139
|
+
<text x="30" y="70" font-family="sans-serif" font-size="16" fill="#b3e5fc">Sequence Modeling, GANs</text>
|
|
140
|
+
<circle cx="340" cy="50" r="20" stroke="#00e5ff" stroke-width="1" fill="none" opacity="0.5"/>
|
|
141
|
+
<polygon points="340,38 352,58 328,58" fill="#00e5ff" opacity="0.8"/>
|
|
142
|
+
</g>
|
|
143
|
+
</g>
|
|
144
|
+
|
|
145
|
+
<!-- Footer Decor -->
|
|
146
|
+
<g transform="translate(0, 672)">
|
|
147
|
+
<rect x="0" y="0" width="1280" height="48" fill="#0d1b60" opacity="0.8"/>
|
|
148
|
+
<text x="60" y="30" font-family="sans-serif" font-size="12" fill="#b3e5fc" opacity="0.5">NEURAL NETWORKS RENAISSANCE | AI HISTORY SERIES</text>
|
|
149
|
+
|
|
150
|
+
<!-- Decorative dots footer -->
|
|
151
|
+
<g fill="#00e5ff" opacity="0.5">
|
|
152
|
+
<circle cx="1206" cy="24" r="2"/>
|
|
153
|
+
<circle cx="1216" cy="24" r="2"/>
|
|
154
|
+
<circle cx="1226" cy="24" r="2"/>
|
|
155
|
+
</g>
|
|
156
|
+
</g>
|
|
157
|
+
</svg>
|
|
158
|
+
</body>
|
|
159
|
+
</html>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""使用 LibreOffice 无头模式将 PPT/PPTX 直接转换为 PDF。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _detect_libreoffice() -> str:
|
|
15
|
+
"""寻找 LibreOffice 可执行文件路径。"""
|
|
16
|
+
system = platform.system()
|
|
17
|
+
candidates: list[str] = []
|
|
18
|
+
|
|
19
|
+
if system == 'Darwin':
|
|
20
|
+
candidates.append('/Applications/LibreOffice.app/Contents/MacOS/soffice')
|
|
21
|
+
elif system == 'Windows':
|
|
22
|
+
program_dirs = [
|
|
23
|
+
os.environ.get('PROGRAMFILES', ''),
|
|
24
|
+
os.environ.get('PROGRAMFILES(X86)', '')
|
|
25
|
+
]
|
|
26
|
+
for base in program_dirs:
|
|
27
|
+
if base:
|
|
28
|
+
candidates.append(str(Path(base) / 'LibreOffice' / 'program' / 'soffice.exe'))
|
|
29
|
+
|
|
30
|
+
# 通用命令放在末尾,优先尝试绝对路径
|
|
31
|
+
candidates.extend(['soffice', 'libreoffice'])
|
|
32
|
+
|
|
33
|
+
for candidate in candidates:
|
|
34
|
+
candidate_path = Path(candidate)
|
|
35
|
+
if candidate_path.exists():
|
|
36
|
+
return str(candidate_path)
|
|
37
|
+
|
|
38
|
+
resolved = shutil.which(candidate)
|
|
39
|
+
if resolved:
|
|
40
|
+
return resolved
|
|
41
|
+
|
|
42
|
+
return ''
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pptx_to_pdf(input_file: str | Path, output_file: str | Path) -> Path:
|
|
46
|
+
pptx_path = Path(input_file).expanduser().resolve()
|
|
47
|
+
if not pptx_path.exists():
|
|
48
|
+
raise FileNotFoundError(f'PPT/PPTX 文件不存在: {pptx_path}')
|
|
49
|
+
if pptx_path.is_dir():
|
|
50
|
+
raise ValueError(f'输入路径不是文件: {pptx_path}')
|
|
51
|
+
|
|
52
|
+
output_path = Path(output_file).expanduser().resolve()
|
|
53
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
libreoffice_cmd = _detect_libreoffice()
|
|
56
|
+
if not libreoffice_cmd:
|
|
57
|
+
raise RuntimeError('未找到 LibreOffice,请先安装(例如 apt install libreoffice 或从官网安装包)。')
|
|
58
|
+
|
|
59
|
+
with tempfile.TemporaryDirectory(prefix='pptx_to_pdf_') as temp_dir:
|
|
60
|
+
cmd = [
|
|
61
|
+
libreoffice_cmd,
|
|
62
|
+
'--headless',
|
|
63
|
+
'--invisible',
|
|
64
|
+
'--convert-to', 'pdf',
|
|
65
|
+
'--outdir', temp_dir,
|
|
66
|
+
str(pptx_path)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
70
|
+
if result.returncode != 0:
|
|
71
|
+
message = result.stderr.strip() or result.stdout.strip() or 'LibreOffice 返回非零退出码'
|
|
72
|
+
raise RuntimeError(f'LibreOffice 转换失败: {message}')
|
|
73
|
+
|
|
74
|
+
generated = Path(temp_dir) / f'{pptx_path.stem}.pdf'
|
|
75
|
+
if not generated.exists():
|
|
76
|
+
pdf_candidates = list(Path(temp_dir).glob('*.pdf'))
|
|
77
|
+
if len(pdf_candidates) == 1:
|
|
78
|
+
generated = pdf_candidates[0]
|
|
79
|
+
else:
|
|
80
|
+
raise RuntimeError('未找到 LibreOffice 生成的 PDF 文件')
|
|
81
|
+
|
|
82
|
+
if output_path.exists():
|
|
83
|
+
output_path.unlink()
|
|
84
|
+
|
|
85
|
+
shutil.move(str(generated), str(output_path))
|
|
86
|
+
return output_path
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_args() -> argparse.Namespace:
|
|
90
|
+
parser = argparse.ArgumentParser(description='将 PPT/PPTX 转换为 PDF(调用 LibreOffice 无头模式)。')
|
|
91
|
+
parser.add_argument('--input', required=True, help='PPT/PPTX 文件路径')
|
|
92
|
+
parser.add_argument('--output', required=True, help='输出 PDF 文件路径')
|
|
93
|
+
return parser.parse_args()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> None:
|
|
97
|
+
args = _parse_args()
|
|
98
|
+
try:
|
|
99
|
+
result = pptx_to_pdf(args.input, args.output)
|
|
100
|
+
print(f'已生成 PDF: {result}')
|
|
101
|
+
except Exception as error:
|
|
102
|
+
print(f'转换失败: {error}', file=sys.stderr)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == '__main__':
|
|
107
|
+
main()
|
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
将HTML文件中的SVG转换为PPTX
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pptx import Presentation
|
|
9
|
+
from pptx.util import Pt, Emu
|
|
10
|
+
from pptx.dml.color import RGBColor
|
|
11
|
+
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
|
|
12
|
+
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
|
13
|
+
from pptx.oxml.xmlchemy import OxmlElement
|
|
14
|
+
from pptx.oxml.ns import qn
|
|
15
|
+
|
|
16
|
+
PX_TO_EMU = 9525
|
|
17
|
+
|
|
18
|
+
def px(v):
|
|
19
|
+
return int(float(v) * PX_TO_EMU)
|
|
20
|
+
|
|
21
|
+
def hex_to_rgb(hex_color):
|
|
22
|
+
hex_color = hex_color.lstrip('#')
|
|
23
|
+
if len(hex_color) == 3:
|
|
24
|
+
hex_color = ''.join([c*2 for c in hex_color])
|
|
25
|
+
return RGBColor(int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16))
|
|
26
|
+
|
|
27
|
+
def _catmull_rom_spline(points, segments_per_curve=20):
|
|
28
|
+
"""Catmull-Rom 样条插值,生成平滑曲线点"""
|
|
29
|
+
if len(points) < 2:
|
|
30
|
+
return points
|
|
31
|
+
extended = [points[0]] + list(points) + [points[-1]]
|
|
32
|
+
result = []
|
|
33
|
+
for i in range(1, len(extended) - 2):
|
|
34
|
+
p0, p1, p2, p3 = extended[i-1], extended[i], extended[i+1], extended[i+2]
|
|
35
|
+
for t_idx in range(segments_per_curve):
|
|
36
|
+
t = t_idx / segments_per_curve
|
|
37
|
+
t2 = t * t
|
|
38
|
+
t3 = t2 * t
|
|
39
|
+
x = 0.5 * ((2 * p1[0]) + (-p0[0] + p2[0]) * t +
|
|
40
|
+
(2*p0[0] - 5*p1[0] + 4*p2[0] - p3[0]) * t2 +
|
|
41
|
+
(-p0[0] + 3*p1[0] - 3*p2[0] + p3[0]) * t3)
|
|
42
|
+
y = 0.5 * ((2 * p1[1]) + (-p0[1] + p2[1]) * t +
|
|
43
|
+
(2*p0[1] - 5*p1[1] + 4*p2[1] - p3[1]) * t2 +
|
|
44
|
+
(-p0[1] + 3*p1[1] - 3*p2[1] + p3[1]) * t3)
|
|
45
|
+
result.append((x, y))
|
|
46
|
+
result.append(points[-1])
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
def set_slide_background_gradient(slide, color1, color2):
|
|
50
|
+
"""设置幻灯片背景渐变"""
|
|
51
|
+
cSld = slide._element.find(qn('p:cSld'))
|
|
52
|
+
if cSld is None:
|
|
53
|
+
return
|
|
54
|
+
bg = cSld.find(qn('p:bg'))
|
|
55
|
+
if bg is None:
|
|
56
|
+
bg = OxmlElement('p:bg')
|
|
57
|
+
cSld.insert(0, bg)
|
|
58
|
+
else:
|
|
59
|
+
bg.clear()
|
|
60
|
+
bgPr = OxmlElement('p:bgPr')
|
|
61
|
+
gradFill = OxmlElement('a:gradFill')
|
|
62
|
+
gradFill.set('rotWithShape', '1')
|
|
63
|
+
gsLst = OxmlElement('a:gsLst')
|
|
64
|
+
|
|
65
|
+
def add_stop(pos, hex_c):
|
|
66
|
+
gs = OxmlElement('a:gs')
|
|
67
|
+
gs.set('pos', str(pos))
|
|
68
|
+
srgbClr = OxmlElement('a:srgbClr')
|
|
69
|
+
srgbClr.set('val', hex_c.lstrip('#'))
|
|
70
|
+
gs.append(srgbClr)
|
|
71
|
+
gsLst.append(gs)
|
|
72
|
+
|
|
73
|
+
add_stop(0, color1)
|
|
74
|
+
add_stop(100000, color2)
|
|
75
|
+
|
|
76
|
+
lin = OxmlElement('a:lin')
|
|
77
|
+
lin.set('ang', '2700000')
|
|
78
|
+
lin.set('scaled', '0')
|
|
79
|
+
gradFill.append(gsLst)
|
|
80
|
+
gradFill.append(lin)
|
|
81
|
+
bgPr.append(gradFill)
|
|
82
|
+
effectLst = OxmlElement('a:effectLst')
|
|
83
|
+
bgPr.append(effectLst)
|
|
84
|
+
bg.append(bgPr)
|
|
85
|
+
|
|
86
|
+
def set_transparency(shape, alpha_float, is_line=True):
|
|
87
|
+
"""设置透明度"""
|
|
88
|
+
fill = shape.line.fill if is_line else shape.fill
|
|
89
|
+
if fill.type == 1:
|
|
90
|
+
fore_color = fill.fore_color
|
|
91
|
+
if hasattr(fore_color, '_xFill'):
|
|
92
|
+
elm = fore_color._xFill
|
|
93
|
+
if hasattr(elm, "srgbClr"):
|
|
94
|
+
clr = elm.srgbClr
|
|
95
|
+
for a in clr.findall(qn("a:alpha")):
|
|
96
|
+
clr.remove(a)
|
|
97
|
+
alpha = OxmlElement('a:alpha')
|
|
98
|
+
alpha.set('val', str(int(alpha_float * 100000)))
|
|
99
|
+
clr.append(alpha)
|
|
100
|
+
|
|
101
|
+
def set_shape_transparency(shape, alpha_float):
|
|
102
|
+
"""设置形状填充透明度"""
|
|
103
|
+
spPr = shape._sp.spPr
|
|
104
|
+
solidFill = spPr.find(qn('a:solidFill'))
|
|
105
|
+
if solidFill is not None:
|
|
106
|
+
srgbClr = solidFill.find(qn('a:srgbClr'))
|
|
107
|
+
if srgbClr is not None:
|
|
108
|
+
for a in srgbClr.findall(qn("a:alpha")):
|
|
109
|
+
srgbClr.remove(a)
|
|
110
|
+
alpha = OxmlElement('a:alpha')
|
|
111
|
+
alpha.set('val', str(int(alpha_float * 100000)))
|
|
112
|
+
srgbClr.append(alpha)
|
|
113
|
+
|
|
114
|
+
def set_shape_gradient_fill(shape, color1, alpha1, color2, alpha2, angle=0):
|
|
115
|
+
"""设置形状渐变填充"""
|
|
116
|
+
spPr = shape._sp.spPr
|
|
117
|
+
for tag in ['a:solidFill', 'a:gradFill', 'a:noFill']:
|
|
118
|
+
existing = spPr.find(qn(tag))
|
|
119
|
+
if existing is not None:
|
|
120
|
+
spPr.remove(existing)
|
|
121
|
+
|
|
122
|
+
gradFill = OxmlElement('a:gradFill')
|
|
123
|
+
gradFill.set('rotWithShape', '1')
|
|
124
|
+
gsLst = OxmlElement('a:gsLst')
|
|
125
|
+
|
|
126
|
+
gs1 = OxmlElement('a:gs')
|
|
127
|
+
gs1.set('pos', '0')
|
|
128
|
+
srgbClr1 = OxmlElement('a:srgbClr')
|
|
129
|
+
srgbClr1.set('val', color1.lstrip('#'))
|
|
130
|
+
if alpha1 < 1.0:
|
|
131
|
+
alpha_elem1 = OxmlElement('a:alpha')
|
|
132
|
+
alpha_elem1.set('val', str(int(alpha1 * 100000)))
|
|
133
|
+
srgbClr1.append(alpha_elem1)
|
|
134
|
+
gs1.append(srgbClr1)
|
|
135
|
+
gsLst.append(gs1)
|
|
136
|
+
|
|
137
|
+
gs2 = OxmlElement('a:gs')
|
|
138
|
+
gs2.set('pos', '100000')
|
|
139
|
+
srgbClr2 = OxmlElement('a:srgbClr')
|
|
140
|
+
srgbClr2.set('val', color2.lstrip('#'))
|
|
141
|
+
if alpha2 < 1.0:
|
|
142
|
+
alpha_elem2 = OxmlElement('a:alpha')
|
|
143
|
+
alpha_elem2.set('val', str(int(alpha2 * 100000)))
|
|
144
|
+
srgbClr2.append(alpha_elem2)
|
|
145
|
+
gs2.append(srgbClr2)
|
|
146
|
+
gsLst.append(gs2)
|
|
147
|
+
|
|
148
|
+
lin = OxmlElement('a:lin')
|
|
149
|
+
lin.set('ang', str(int(angle * 60000)))
|
|
150
|
+
lin.set('scaled', '1')
|
|
151
|
+
|
|
152
|
+
gradFill.append(gsLst)
|
|
153
|
+
gradFill.append(lin)
|
|
154
|
+
spPr.append(gradFill)
|
|
155
|
+
|
|
156
|
+
def set_line_transparency(connector, alpha_float):
|
|
157
|
+
"""设置线条透明度"""
|
|
158
|
+
spPr = connector._element.find(qn('p:spPr'))
|
|
159
|
+
if spPr is None:
|
|
160
|
+
return
|
|
161
|
+
ln = spPr.find(qn('a:ln'))
|
|
162
|
+
if ln is None:
|
|
163
|
+
return
|
|
164
|
+
solidFill = ln.find(qn('a:solidFill'))
|
|
165
|
+
if solidFill is not None:
|
|
166
|
+
srgbClr = solidFill.find(qn('a:srgbClr'))
|
|
167
|
+
if srgbClr is not None:
|
|
168
|
+
for a in srgbClr.findall(qn("a:alpha")):
|
|
169
|
+
srgbClr.remove(a)
|
|
170
|
+
alpha = OxmlElement('a:alpha')
|
|
171
|
+
alpha.set('val', str(int(alpha_float * 100000)))
|
|
172
|
+
srgbClr.append(alpha)
|
|
173
|
+
|
|
174
|
+
def set_line_stroke_transparency(shape, alpha_float):
|
|
175
|
+
"""设置形状边框透明度"""
|
|
176
|
+
spPr = shape._sp.spPr
|
|
177
|
+
ln = spPr.find(qn('a:ln'))
|
|
178
|
+
if ln is None:
|
|
179
|
+
ln = OxmlElement('a:ln')
|
|
180
|
+
spPr.append(ln)
|
|
181
|
+
solidFill = ln.find(qn('a:solidFill'))
|
|
182
|
+
if solidFill is not None:
|
|
183
|
+
srgbClr = solidFill.find(qn('a:srgbClr'))
|
|
184
|
+
if srgbClr is not None:
|
|
185
|
+
for a in srgbClr.findall(qn("a:alpha")):
|
|
186
|
+
srgbClr.remove(a)
|
|
187
|
+
alpha = OxmlElement('a:alpha')
|
|
188
|
+
alpha.set('val', str(int(alpha_float * 100000)))
|
|
189
|
+
srgbClr.append(alpha)
|
|
190
|
+
|
|
191
|
+
def apply_gradient_to_text(run, color1, color2):
|
|
192
|
+
"""应用渐变到文本"""
|
|
193
|
+
rPr = run._r.get_or_add_rPr()
|
|
194
|
+
for tag in ['a:solidFill', 'a:gradFill', 'a:noFill']:
|
|
195
|
+
existing = rPr.find(qn(tag))
|
|
196
|
+
if existing is not None:
|
|
197
|
+
rPr.remove(existing)
|
|
198
|
+
|
|
199
|
+
gradFill = OxmlElement('a:gradFill')
|
|
200
|
+
gradFill.set('rotWithShape', '1')
|
|
201
|
+
|
|
202
|
+
gsLst = OxmlElement('a:gsLst')
|
|
203
|
+
|
|
204
|
+
gs1 = OxmlElement('a:gs')
|
|
205
|
+
gs1.set('pos', '0')
|
|
206
|
+
srgb1 = OxmlElement('a:srgbClr')
|
|
207
|
+
srgb1.set('val', color1.lstrip('#'))
|
|
208
|
+
gs1.append(srgb1)
|
|
209
|
+
gsLst.append(gs1)
|
|
210
|
+
|
|
211
|
+
gs2 = OxmlElement('a:gs')
|
|
212
|
+
gs2.set('pos', '100000')
|
|
213
|
+
srgb2 = OxmlElement('a:srgbClr')
|
|
214
|
+
srgb2.set('val', color2.lstrip('#'))
|
|
215
|
+
gs2.append(srgb2)
|
|
216
|
+
gsLst.append(gs2)
|
|
217
|
+
|
|
218
|
+
lin = OxmlElement('a:lin')
|
|
219
|
+
lin.set('ang', '0')
|
|
220
|
+
lin.set('scaled', '1')
|
|
221
|
+
|
|
222
|
+
gradFill.append(gsLst)
|
|
223
|
+
gradFill.append(lin)
|
|
224
|
+
rPr.insert(0, gradFill)
|
|
225
|
+
|
|
226
|
+
def set_text_color_xml(run, hex_color):
|
|
227
|
+
"""通过XML直接设置文字颜色,确保不被覆盖"""
|
|
228
|
+
rPr = run._r.get_or_add_rPr()
|
|
229
|
+
for tag in ['a:solidFill', 'a:gradFill', 'a:noFill']:
|
|
230
|
+
existing = rPr.find(qn(tag))
|
|
231
|
+
if existing is not None:
|
|
232
|
+
rPr.remove(existing)
|
|
233
|
+
solidFill = OxmlElement('a:solidFill')
|
|
234
|
+
srgbClr = OxmlElement('a:srgbClr')
|
|
235
|
+
srgbClr.set('val', hex_color.lstrip('#'))
|
|
236
|
+
solidFill.append(srgbClr)
|
|
237
|
+
rPr.insert(0, solidFill)
|
|
238
|
+
|
|
239
|
+
def add_text_box(slide, text, x, y, width, height, font_size, color, bold=False, anchor='middle', valign='middle', gradient=None):
|
|
240
|
+
"""添加文本框"""
|
|
241
|
+
if anchor == 'middle':
|
|
242
|
+
left = px(x - width/2)
|
|
243
|
+
elif anchor == 'end':
|
|
244
|
+
left = px(x - width)
|
|
245
|
+
else:
|
|
246
|
+
left = px(x)
|
|
247
|
+
|
|
248
|
+
top = px(y - height/2) if valign == 'middle' else px(y - height)
|
|
249
|
+
|
|
250
|
+
tb = slide.shapes.add_textbox(left, top, px(width), px(height))
|
|
251
|
+
tf = tb.text_frame
|
|
252
|
+
tf.word_wrap = False
|
|
253
|
+
|
|
254
|
+
if valign == 'middle':
|
|
255
|
+
tf.anchor = MSO_ANCHOR.MIDDLE
|
|
256
|
+
|
|
257
|
+
p = tf.paragraphs[0]
|
|
258
|
+
if anchor == 'middle':
|
|
259
|
+
p.alignment = PP_ALIGN.CENTER
|
|
260
|
+
elif anchor == 'end':
|
|
261
|
+
p.alignment = PP_ALIGN.RIGHT
|
|
262
|
+
else:
|
|
263
|
+
p.alignment = PP_ALIGN.LEFT
|
|
264
|
+
|
|
265
|
+
run = p.add_run()
|
|
266
|
+
run.text = text
|
|
267
|
+
run.font.size = Pt(font_size * 0.75)
|
|
268
|
+
run.font.bold = bold
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
run.font.name = 'Microsoft YaHei'
|
|
272
|
+
rPr = run._r.get_or_add_rPr()
|
|
273
|
+
ea = OxmlElement("a:ea")
|
|
274
|
+
ea.set("typeface", "Microsoft YaHei")
|
|
275
|
+
rPr.insert(0, ea)
|
|
276
|
+
except:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
if gradient:
|
|
280
|
+
apply_gradient_to_text(run, gradient[0], gradient[1])
|
|
281
|
+
else:
|
|
282
|
+
set_text_color_xml(run, color)
|
|
283
|
+
|
|
284
|
+
return tb
|
|
285
|
+
|
|
286
|
+
def add_rectangle(slide, x, y, width, height, fill_color, opacity=1.0, rx=0, stroke_color=None, stroke_width=0, stroke_opacity=None):
|
|
287
|
+
"""添加矩形"""
|
|
288
|
+
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE if rx > 0 else MSO_SHAPE.RECTANGLE,
|
|
289
|
+
px(x), px(y), px(width), px(height))
|
|
290
|
+
if rx > 0:
|
|
291
|
+
shape.adjustments[0] = min(rx / min(width, height), 0.5)
|
|
292
|
+
|
|
293
|
+
shape.fill.solid()
|
|
294
|
+
shape.fill.fore_color.rgb = hex_to_rgb(fill_color)
|
|
295
|
+
if opacity < 1.0:
|
|
296
|
+
set_shape_transparency(shape, opacity)
|
|
297
|
+
|
|
298
|
+
if stroke_color and stroke_width > 0:
|
|
299
|
+
shape.line.color.rgb = hex_to_rgb(stroke_color)
|
|
300
|
+
shape.line.width = Pt(stroke_width)
|
|
301
|
+
# 设置边框透明度
|
|
302
|
+
if stroke_opacity is not None and stroke_opacity < 1.0:
|
|
303
|
+
set_line_stroke_transparency(shape, stroke_opacity)
|
|
304
|
+
else:
|
|
305
|
+
shape.line.fill.background()
|
|
306
|
+
|
|
307
|
+
return shape
|
|
308
|
+
|
|
309
|
+
def add_gradient_rectangle(slide, x, y, width, height, color1, alpha1, color2, alpha2, angle=0, rx=0):
|
|
310
|
+
"""添加渐变矩形"""
|
|
311
|
+
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE if rx > 0 else MSO_SHAPE.RECTANGLE,
|
|
312
|
+
px(x), px(y), px(width), px(height))
|
|
313
|
+
if rx > 0:
|
|
314
|
+
shape.adjustments[0] = min(rx / min(width, height), 0.5)
|
|
315
|
+
|
|
316
|
+
set_shape_gradient_fill(shape, color1, alpha1, color2, alpha2, angle)
|
|
317
|
+
shape.line.fill.background()
|
|
318
|
+
return shape
|
|
319
|
+
|
|
320
|
+
def add_circle(slide, cx, cy, r, fill_color=None, stroke_color=None, stroke_width=1, opacity=1.0, stroke_opacity=None):
|
|
321
|
+
"""添加圆形"""
|
|
322
|
+
shape = slide.shapes.add_shape(MSO_SHAPE.OVAL, px(cx-r), px(cy-r), px(r*2), px(r*2))
|
|
323
|
+
|
|
324
|
+
if fill_color:
|
|
325
|
+
shape.fill.solid()
|
|
326
|
+
shape.fill.fore_color.rgb = hex_to_rgb(fill_color)
|
|
327
|
+
if opacity < 1.0:
|
|
328
|
+
set_shape_transparency(shape, opacity)
|
|
329
|
+
else:
|
|
330
|
+
shape.fill.background()
|
|
331
|
+
|
|
332
|
+
if stroke_color:
|
|
333
|
+
shape.line.color.rgb = hex_to_rgb(stroke_color)
|
|
334
|
+
shape.line.width = Pt(stroke_width)
|
|
335
|
+
# 设置边框透明度
|
|
336
|
+
if stroke_opacity is not None and stroke_opacity < 1.0:
|
|
337
|
+
set_line_stroke_transparency(shape, stroke_opacity)
|
|
338
|
+
else:
|
|
339
|
+
shape.line.fill.background()
|
|
340
|
+
|
|
341
|
+
return shape
|
|
342
|
+
|
|
343
|
+
def add_ellipse(slide, cx, cy, rx, ry, fill_color=None, stroke_color=None, stroke_width=1, opacity=1.0, rotation=0, stroke_opacity=None):
|
|
344
|
+
"""添加椭圆"""
|
|
345
|
+
shape = slide.shapes.add_shape(MSO_SHAPE.OVAL, px(cx-rx), px(cy-ry), px(rx*2), px(ry*2))
|
|
346
|
+
|
|
347
|
+
if fill_color:
|
|
348
|
+
shape.fill.solid()
|
|
349
|
+
shape.fill.fore_color.rgb = hex_to_rgb(fill_color)
|
|
350
|
+
if opacity < 1.0:
|
|
351
|
+
set_shape_transparency(shape, opacity)
|
|
352
|
+
else:
|
|
353
|
+
shape.fill.background()
|
|
354
|
+
|
|
355
|
+
if stroke_color:
|
|
356
|
+
shape.line.color.rgb = hex_to_rgb(stroke_color)
|
|
357
|
+
shape.line.width = Pt(stroke_width)
|
|
358
|
+
# 设置边框透明度
|
|
359
|
+
if stroke_opacity is not None and stroke_opacity < 1.0:
|
|
360
|
+
set_line_stroke_transparency(shape, stroke_opacity)
|
|
361
|
+
else:
|
|
362
|
+
shape.line.fill.background()
|
|
363
|
+
|
|
364
|
+
if rotation != 0:
|
|
365
|
+
shape.rotation = rotation
|
|
366
|
+
|
|
367
|
+
return shape
|
|
368
|
+
|
|
369
|
+
def add_line(slide, x1, y1, x2, y2, color, width=1, opacity=1.0, dash=False):
|
|
370
|
+
"""添加线条"""
|
|
371
|
+
connector = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, px(x1), px(y1), px(x2), px(y2))
|
|
372
|
+
connector.line.color.rgb = hex_to_rgb(color)
|
|
373
|
+
connector.line.width = Pt(width)
|
|
374
|
+
if dash:
|
|
375
|
+
connector.line.dash_style = 2
|
|
376
|
+
if opacity < 1.0:
|
|
377
|
+
set_line_transparency(connector, opacity)
|
|
378
|
+
return connector
|
|
379
|
+
|
|
380
|
+
def add_triangle(slide, cx, cy, size, fill_color, opacity=1.0, rotation=0, stroke_color=None, stroke_width=0, stroke_opacity=None):
|
|
381
|
+
"""添加三角形"""
|
|
382
|
+
shape = slide.shapes.add_shape(MSO_SHAPE.ISOSCELES_TRIANGLE,
|
|
383
|
+
px(cx - size/2), px(cy - size/2), px(size), px(size))
|
|
384
|
+
shape.fill.solid()
|
|
385
|
+
shape.fill.fore_color.rgb = hex_to_rgb(fill_color)
|
|
386
|
+
if opacity < 1.0:
|
|
387
|
+
set_shape_transparency(shape, opacity)
|
|
388
|
+
if rotation != 0:
|
|
389
|
+
shape.rotation = rotation
|
|
390
|
+
if stroke_color and stroke_width > 0:
|
|
391
|
+
shape.line.color.rgb = hex_to_rgb(stroke_color)
|
|
392
|
+
shape.line.width = Pt(stroke_width)
|
|
393
|
+
# 设置边框透明度
|
|
394
|
+
if stroke_opacity is not None and stroke_opacity < 1.0:
|
|
395
|
+
set_line_stroke_transparency(shape, stroke_opacity)
|
|
396
|
+
else:
|
|
397
|
+
shape.line.fill.background()
|
|
398
|
+
return shape
|
|
399
|
+
|
|
400
|
+
def add_spline(slide, points, color, width=2, opacity=1.0, smooth=True):
|
|
401
|
+
"""添加样条曲线,points为控制点列表[(x,y),...],smooth=True时使用Catmull-Rom插值"""
|
|
402
|
+
if len(points) < 2:
|
|
403
|
+
return None
|
|
404
|
+
if smooth and len(points) >= 3:
|
|
405
|
+
smooth_points = _catmull_rom_spline(points, segments_per_curve=20)
|
|
406
|
+
else:
|
|
407
|
+
smooth_points = points
|
|
408
|
+
|
|
409
|
+
builder = slide.shapes.build_freeform(px(smooth_points[0][0]), px(smooth_points[0][1]))
|
|
410
|
+
for i in range(1, len(smooth_points)):
|
|
411
|
+
builder.add_line_segments([(px(smooth_points[i][0]), px(smooth_points[i][1]))], close=False)
|
|
412
|
+
|
|
413
|
+
shape = builder.convert_to_shape()
|
|
414
|
+
shape.fill.background()
|
|
415
|
+
shape.line.color.rgb = hex_to_rgb(color)
|
|
416
|
+
shape.line.width = Pt(width)
|
|
417
|
+
if opacity < 1.0:
|
|
418
|
+
set_line_transparency(shape, opacity)
|
|
419
|
+
return shape
|
|
420
|
+
|
|
421
|
+
def add_freeform_path(slide, points, fill_color=None, stroke_color=None, stroke_width=1, opacity=1.0, closed=False, stroke_opacity=None):
|
|
422
|
+
"""添加自由形状路径"""
|
|
423
|
+
if len(points) < 2:
|
|
424
|
+
return None
|
|
425
|
+
builder = slide.shapes.build_freeform(px(points[0][0]), px(points[0][1]))
|
|
426
|
+
for i in range(1, len(points)):
|
|
427
|
+
builder.add_line_segments([(px(points[i][0]), px(points[i][1]))], close=False)
|
|
428
|
+
if closed:
|
|
429
|
+
builder.add_line_segments([(px(points[0][0]), px(points[0][1]))], close=True)
|
|
430
|
+
|
|
431
|
+
shape = builder.convert_to_shape()
|
|
432
|
+
if fill_color:
|
|
433
|
+
shape.fill.solid()
|
|
434
|
+
shape.fill.fore_color.rgb = hex_to_rgb(fill_color)
|
|
435
|
+
if opacity < 1.0:
|
|
436
|
+
set_shape_transparency(shape, opacity)
|
|
437
|
+
else:
|
|
438
|
+
shape.fill.background()
|
|
439
|
+
|
|
440
|
+
if stroke_color:
|
|
441
|
+
shape.line.color.rgb = hex_to_rgb(stroke_color)
|
|
442
|
+
shape.line.width = Pt(stroke_width)
|
|
443
|
+
# 设置边框透明度
|
|
444
|
+
actual_stroke_opacity = stroke_opacity if stroke_opacity is not None else opacity
|
|
445
|
+
if actual_stroke_opacity < 1.0:
|
|
446
|
+
set_line_transparency(shape, actual_stroke_opacity)
|
|
447
|
+
else:
|
|
448
|
+
shape.line.fill.background()
|
|
449
|
+
|
|
450
|
+
return shape
|
|
451
|
+
|
|
452
|
+
def add_polyline(slide, points, color, width=1, opacity=1.0, dash=False):
|
|
453
|
+
"""添加折线"""
|
|
454
|
+
if len(points) < 2:
|
|
455
|
+
return None
|
|
456
|
+
builder = slide.shapes.build_freeform(px(points[0][0]), px(points[0][1]))
|
|
457
|
+
for i in range(1, len(points)):
|
|
458
|
+
builder.add_line_segments([(px(points[i][0]), px(points[i][1]))], close=False)
|
|
459
|
+
|
|
460
|
+
shape = builder.convert_to_shape()
|
|
461
|
+
shape.fill.background()
|
|
462
|
+
shape.line.color.rgb = hex_to_rgb(color)
|
|
463
|
+
shape.line.width = Pt(width)
|
|
464
|
+
if dash:
|
|
465
|
+
shape.line.dash_style = 2
|
|
466
|
+
if opacity < 1.0:
|
|
467
|
+
set_line_transparency(shape, opacity)
|
|
468
|
+
return shape
|
|
469
|
+
|
|
470
|
+
def add_image(slide, image_path, x, y, width, height):
|
|
471
|
+
"""添加图片(填充方式)"""
|
|
472
|
+
shape = slide.shapes.add_picture(image_path, px(x), px(y), px(width), px(height))
|
|
473
|
+
return shape
|
|
474
|
+
|
|
475
|
+
def add_circular_image(slide, image_path, cx, cy, r):
|
|
476
|
+
"""添加圆形裁剪图片"""
|
|
477
|
+
from PIL import Image, ImageDraw
|
|
478
|
+
import io
|
|
479
|
+
|
|
480
|
+
img = Image.open(image_path)
|
|
481
|
+
size = min(img.width, img.height)
|
|
482
|
+
left = (img.width - size) // 2
|
|
483
|
+
top = (img.height - size) // 2
|
|
484
|
+
img_cropped = img.crop((left, top, left + size, top + size))
|
|
485
|
+
|
|
486
|
+
mask = Image.new('L', (size, size), 0)
|
|
487
|
+
draw = ImageDraw.Draw(mask)
|
|
488
|
+
draw.ellipse((0, 0, size, size), fill=255)
|
|
489
|
+
|
|
490
|
+
result = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
|
491
|
+
img_rgba = img_cropped.convert('RGBA')
|
|
492
|
+
result.paste(img_rgba, mask=mask)
|
|
493
|
+
|
|
494
|
+
buffer = io.BytesIO()
|
|
495
|
+
result.save(buffer, format='PNG')
|
|
496
|
+
buffer.seek(0)
|
|
497
|
+
|
|
498
|
+
diameter = r * 2
|
|
499
|
+
shape = slide.shapes.add_picture(buffer, px(cx - r), px(cy - r), px(diameter), px(diameter))
|
|
500
|
+
return shape
|
|
501
|
+
|
|
502
|
+
def create_slide(prs):
|
|
503
|
+
"""深度学习的崛起(1990-2017)"""
|
|
504
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
505
|
+
|
|
506
|
+
# 背景渐变 (#1a237e -> #0d1b60)
|
|
507
|
+
set_slide_background_gradient(slide, "1a237e", "0d1b60")
|
|
508
|
+
|
|
509
|
+
# 图片占位符位置插入图片 (左侧时间线区域右侧)
|
|
510
|
+
image_path = os.path.join(os.path.dirname(__file__), "demo.jpg")
|
|
511
|
+
add_image(slide, image_path, 420, 340, 180, 120)
|
|
512
|
+
|
|
513
|
+
# 圆形图片占位符 (右上角头像)
|
|
514
|
+
add_circular_image(slide, image_path, 1150, 120, 50)
|
|
515
|
+
|
|
516
|
+
# 右侧装饰性神经网络背景
|
|
517
|
+
add_line(slide, 800, 200, 900, 300, "#ffffff", 1, 0.1)
|
|
518
|
+
add_line(slide, 900, 300, 850, 450, "#ffffff", 1, 0.1)
|
|
519
|
+
add_line(slide, 850, 450, 750, 400, "#ffffff", 1, 0.1)
|
|
520
|
+
add_line(slide, 750, 400, 800, 200, "#ffffff", 1, 0.1)
|
|
521
|
+
add_line(slide, 800, 200, 650, 250, "#ffffff", 1, 0.1)
|
|
522
|
+
add_line(slide, 650, 250, 750, 400, "#ffffff", 1, 0.1)
|
|
523
|
+
add_circle(slide, 800, 200, 4, "#ffffff", opacity=0.1)
|
|
524
|
+
add_circle(slide, 900, 300, 4, "#ffffff", opacity=0.1)
|
|
525
|
+
add_circle(slide, 850, 450, 4, "#ffffff", opacity=0.1)
|
|
526
|
+
add_circle(slide, 750, 400, 4, "#ffffff", opacity=0.1)
|
|
527
|
+
add_circle(slide, 650, 250, 4, "#ffffff", opacity=0.1)
|
|
528
|
+
|
|
529
|
+
# 标题部分
|
|
530
|
+
add_text_box(slide, "深度学习的崛起(1990-2017)", 60, 80, 800, 60, 48, "#ffffff", bold=True, anchor='start', valign='top')
|
|
531
|
+
add_gradient_rectangle(slide, 60, 100, 100, 4, "#00e5ff", 1.0, "#2979ff", 1.0, 0, rx=2)
|
|
532
|
+
add_line(slide, 60, 102, 1220, 102, "#ffffff", 1, 0.2)
|
|
533
|
+
|
|
534
|
+
# 中间分隔虚线
|
|
535
|
+
add_line(slide, 640, 180, 640, 630, "#00e5ff", 1, 0.3, dash=True)
|
|
536
|
+
|
|
537
|
+
# 左侧:技术突破
|
|
538
|
+
add_text_box(slide, "技术突破", 60, 180, 200, 40, 28, "#00e5ff", bold=True, anchor='start', valign='top')
|
|
539
|
+
|
|
540
|
+
# 时间线主线
|
|
541
|
+
add_line(slide, 80, 230, 80, 660, "#b3e5fc", 2, 0.3)
|
|
542
|
+
|
|
543
|
+
# 1998
|
|
544
|
+
add_circle(slide, 80, 250, 6, "#1a237e", stroke_color="#00e5ff", stroke_width=2)
|
|
545
|
+
add_text_box(slide, "1998", 105, 240, 100, 30, 24, "#ffffff", bold=True, anchor='start', valign='top')
|
|
546
|
+
add_text_box(slide, "LeNet-5,CNN开创性工作", 105, 270, 350, 30, 20, "#b3e5fc", anchor='start', valign='top')
|
|
547
|
+
|
|
548
|
+
# 2012
|
|
549
|
+
add_circle(slide, 80, 350, 6, "#1a237e", stroke_color="#00e5ff", stroke_width=2)
|
|
550
|
+
add_text_box(slide, "2012", 105, 340, 100, 30, 24, "#ffffff", bold=True, anchor='start', valign='top')
|
|
551
|
+
add_text_box(slide, "AlexNet赢得ImageNet竞赛", 105, 370, 350, 30, 20, "#b3e5fc", anchor='start', valign='top')
|
|
552
|
+
|
|
553
|
+
# 2014
|
|
554
|
+
add_circle(slide, 80, 450, 6, "#1a237e", stroke_color="#00e5ff", stroke_width=2)
|
|
555
|
+
add_text_box(slide, "2014", 105, 440, 100, 30, 24, "#ffffff", bold=True, anchor='start', valign='top')
|
|
556
|
+
add_text_box(slide, "GAN生成对抗网络", 105, 470, 350, 30, 20, "#b3e5fc", anchor='start', valign='top')
|
|
557
|
+
|
|
558
|
+
# 2015
|
|
559
|
+
add_circle(slide, 80, 550, 6, "#1a237e", stroke_color="#00e5ff", stroke_width=2)
|
|
560
|
+
add_text_box(slide, "2015", 105, 540, 100, 30, 24, "#ffffff", bold=True, anchor='start', valign='top')
|
|
561
|
+
add_text_box(slide, "ResNet残差网络", 105, 570, 350, 30, 20, "#b3e5fc", anchor='start', valign='top')
|
|
562
|
+
|
|
563
|
+
# 右侧:关键人物
|
|
564
|
+
add_text_box(slide, "关键人物", 688, 180, 200, 40, 28, "#00e5ff", bold=True, anchor='start', valign='top')
|
|
565
|
+
add_text_box(slide, "Turing Award Winners (\"Godfathers of AI\")", 688, 215, 400, 25, 16, "#b3e5fc", anchor='start', valign='top')
|
|
566
|
+
|
|
567
|
+
# 人物卡片1: Hinton (切角矩形: 左上和右下切角10px)
|
|
568
|
+
add_freeform_path(slide, [(688, 260), (698, 250), (1068, 250), (1068, 340), (1058, 350), (688, 350)],
|
|
569
|
+
fill_color="#ffffff", stroke_color="#00e5ff", stroke_width=1, opacity=0.05, closed=True)
|
|
570
|
+
add_freeform_path(slide, [(688, 270), (688, 260), (698, 250), (708, 250)], stroke_color="#00e5ff", stroke_width=2)
|
|
571
|
+
add_text_box(slide, "Geoffrey Hinton", 718, 290, 250, 30, 24, "#ffffff", bold=True, anchor='start', valign='top')
|
|
572
|
+
add_text_box(slide, "Backpropagation, Deep Belief Nets", 718, 320, 300, 25, 16, "#b3e5fc", anchor='start', valign='top')
|
|
573
|
+
add_circle(slide, 1028, 300, 20, stroke_color="#00e5ff", stroke_width=1, opacity=0.5)
|
|
574
|
+
add_circle(slide, 1028, 300, 8, "#00e5ff", opacity=0.8)
|
|
575
|
+
|
|
576
|
+
# 人物卡片2: LeCun
|
|
577
|
+
add_freeform_path(slide, [(688, 380), (698, 370), (1068, 370), (1068, 460), (1058, 470), (688, 470)],
|
|
578
|
+
fill_color="#ffffff", stroke_color="#00e5ff", stroke_width=1, opacity=0.05, closed=True)
|
|
579
|
+
add_freeform_path(slide, [(688, 390), (688, 380), (698, 370), (708, 370)], stroke_color="#00e5ff", stroke_width=2)
|
|
580
|
+
add_text_box(slide, "Yann LeCun", 718, 410, 250, 30, 24, "#ffffff", bold=True, anchor='start', valign='top')
|
|
581
|
+
add_text_box(slide, "CNN (Convolutional Neural Networks)", 718, 440, 300, 25, 16, "#b3e5fc", anchor='start', valign='top')
|
|
582
|
+
add_circle(slide, 1028, 420, 20, stroke_color="#00e5ff", stroke_width=1, opacity=0.5)
|
|
583
|
+
add_rectangle(slide, 1020, 412, 16, 16, "#00e5ff", opacity=0.8)
|
|
584
|
+
|
|
585
|
+
# 人物卡片3: Bengio
|
|
586
|
+
add_freeform_path(slide, [(688, 500), (698, 490), (1068, 490), (1068, 580), (1058, 590), (688, 590)],
|
|
587
|
+
fill_color="#ffffff", stroke_color="#00e5ff", stroke_width=1, opacity=0.05, closed=True)
|
|
588
|
+
add_freeform_path(slide, [(688, 510), (688, 500), (698, 490), (708, 490)], stroke_color="#00e5ff", stroke_width=2)
|
|
589
|
+
add_text_box(slide, "Yoshua Bengio", 718, 530, 250, 30, 24, "#ffffff", bold=True, anchor='start', valign='top')
|
|
590
|
+
add_text_box(slide, "Sequence Modeling, GANs", 718, 560, 300, 25, 16, "#b3e5fc", anchor='start', valign='top')
|
|
591
|
+
add_circle(slide, 1028, 540, 20, stroke_color="#00e5ff", stroke_width=1, opacity=0.5)
|
|
592
|
+
add_triangle(slide, 1028, 548, 20, "#00e5ff", opacity=0.8, rotation=180)
|
|
593
|
+
|
|
594
|
+
# 底部装饰
|
|
595
|
+
add_rectangle(slide, 0, 672, 1280, 48, "#0d1b60", opacity=0.8)
|
|
596
|
+
add_text_box(slide, "NEURAL NETWORKS RENAISSANCE | AI HISTORY SERIES", 60, 702, 500, 20, 12, "#b3e5fc", anchor='start', valign='top')
|
|
597
|
+
add_circle(slide, 1206, 696, 2, "#00e5ff", opacity=0.5)
|
|
598
|
+
add_circle(slide, 1216, 696, 2, "#00e5ff", opacity=0.5)
|
|
599
|
+
add_circle(slide, 1226, 696, 2, "#00e5ff", opacity=0.5)
|
|
600
|
+
|
|
601
|
+
def main():
|
|
602
|
+
prs = Presentation()
|
|
603
|
+
prs.slide_width = px(1280)
|
|
604
|
+
prs.slide_height = px(720)
|
|
605
|
+
|
|
606
|
+
create_slide(prs)
|
|
607
|
+
|
|
608
|
+
output_path = os.path.join(os.path.dirname(__file__), "深度学习的崛起.pptx")
|
|
609
|
+
prs.save(output_path)
|
|
610
|
+
print(f"成功生成: {output_path}")
|
|
611
|
+
|
|
612
|
+
if __name__ == "__main__":
|
|
613
|
+
main()
|
package/index.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
6
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
7
|
+
const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
8
|
+
|
|
9
|
+
const ASSETS_ROOT = path.join(__dirname, 'assets');
|
|
10
|
+
|
|
11
|
+
const resolveAsset = (relativePath) => {
|
|
12
|
+
const target = path.resolve(ASSETS_ROOT, relativePath);
|
|
13
|
+
const rel = path.relative(ASSETS_ROOT, target);
|
|
14
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
15
|
+
throw new Error('非法资源路径');
|
|
16
|
+
}
|
|
17
|
+
if (!fs.existsSync(target)) {
|
|
18
|
+
throw new Error(`未找到资源: ${relativePath}`);
|
|
19
|
+
}
|
|
20
|
+
return target;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const loadText = (relativePath) => fs.readFileSync(resolveAsset(relativePath), 'utf8');
|
|
24
|
+
|
|
25
|
+
const SNIPPETS = {
|
|
26
|
+
html_to_png: {
|
|
27
|
+
title: 'HTML 转 PNG',
|
|
28
|
+
description: 'Playwright 渲染 HTML 后截图,自动按主体尺寸设置 viewport,适合静态页面或组件 demo。',
|
|
29
|
+
codeFile: 'html_to_png.py',
|
|
30
|
+
notes: [
|
|
31
|
+
'默认等待 1500ms 以确保样式/异步资源加载完成,可通过脚本中的 wait 参数调节。',
|
|
32
|
+
'使用 device_scale_factor=4 输出高清图,如需更快速度可降低倍率。',
|
|
33
|
+
'截图时优先取 body 内的顶层单节点,否则回退到整个 body,保证尺寸合理。'
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
pdf_to_png: {
|
|
37
|
+
title: 'PDF 转 PNG',
|
|
38
|
+
description: '调用 pdf2image 将 PDF 分页转成位图,并按序号命名输出。',
|
|
39
|
+
codeFile: 'pdf_to_png.py',
|
|
40
|
+
notes: [
|
|
41
|
+
'支持自定义 dpi 与输出格式(png/jpg 等),默认 300dpi 用于打印级清晰度。',
|
|
42
|
+
'输出目录会自动创建,文件名形如 page_0001.png,便于批量处理。',
|
|
43
|
+
'如需大批量转换,可将 convert_from_path 的线程参数打开以提升性能。'
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
pptx_to_pdf: {
|
|
47
|
+
title: 'PPTX 转 PDF',
|
|
48
|
+
description: '使用 LibreOffice 无头模式批量转换,兼容 Windows/macOS/Linux。',
|
|
49
|
+
codeFile: 'pptx_to_pdf.py',
|
|
50
|
+
notes: [
|
|
51
|
+
'脚本会自动探测 soffice 路径,如未安装需先安装 LibreOffice。',
|
|
52
|
+
'转换在临时目录完成后再移动到目标路径,避免半成品文件覆盖。',
|
|
53
|
+
'输出路径父目录会自动创建,可直接传绝对路径。'
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
svg_to_pptx: {
|
|
57
|
+
title: 'SVG 转 PPTX',
|
|
58
|
+
description: '把 SVG 转化成在 PPTX 中创建可操作的元素。',
|
|
59
|
+
codeFile: 'svg_to_pptx.py',
|
|
60
|
+
extraFiles: [
|
|
61
|
+
{ label: '示例 HTML(ppt_slide.html)', file: 'ppt_slide.html', language: 'html' }
|
|
62
|
+
],
|
|
63
|
+
notes: [
|
|
64
|
+
'【文本颜色丢失】python-pptx 设置字体名称时会添加 a:latin/a:ea 等元素,如果颜色填充放在 rPr 末尾会被 PowerPoint 忽略。解决:用 rPr.insert(0, solidFill/gradFill) 将颜色填充插入到 rPr 开头,确保优先级最高。',
|
|
65
|
+
'【渐变文本失效】设置渐变前必须先清理 a:solidFill、a:gradFill、a:noFill 三种标签,否则会冲突。同时添加 rotWithShape="1" 和 scaled="1" 属性确保渐变方向正确。',
|
|
66
|
+
'【背景渐变被覆盖】直接操作 slide._element 时,若 p:bg 已存在需先 clear() 再重建,否则新旧元素混杂导致渲染异常。用 cSld.insert(0, bg) 确保背景节点在正确位置。',
|
|
67
|
+
'【透明度不生效】设置透明度前需检查 fill.type == 1(纯色填充),然后定位到 srgbClr 节点追加 a:alpha 子元素,添加前先移除已有的 a:alpha 避免重复。',
|
|
68
|
+
'【中文字体回退】仅设置 run.font.name 对中文无效,需额外通过 XML 插入 a:ea 元素并指定 typeface="Microsoft YaHei",且 a:ea 要插入到 rPr 开头防止被后续元素覆盖。',
|
|
69
|
+
'【形状渐变填充】设置形状渐变时,必须先移除 spPr 下已有的 a:solidFill/a:gradFill/a:noFill,否则渐变不生效。渐变位置 pos 使用 0-100000 范围(百分比*1000),角度 ang 使用度数*60000。',
|
|
70
|
+
'【渐变透明度】每个渐变停止点的透明度需在对应 srgbClr 下追加 a:alpha 元素,val 值为透明度*100000(如 0.5 透明度对应 50000)。',
|
|
71
|
+
'【曲线绘制】使用 build_freeform 绘制曲线时,起点坐标在构造器中指定,后续点通过 add_line_segments 添加。若需平滑曲线,可用 Catmull-Rom 样条插值生成中间点。'
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const GUIDES = {
|
|
77
|
+
make_ppt: {
|
|
78
|
+
title: '制作PPT',
|
|
79
|
+
content: `制作PPT的完整步骤如下(可根据当前实际状态,跳过部分已经完成的环节):
|
|
80
|
+
|
|
81
|
+
1. **规划PPT内容**,存储到markdown文件中:
|
|
82
|
+
- 每页Slide的规划需要包含:内容、布局方式、插图(如果需要)
|
|
83
|
+
- 如果用户未明确声明,则**默认比例为宽1280px、高720px,默认背景为深色、科技感、弱渐变、简约风格**
|
|
84
|
+
- 不同页面应尽可能使用不同的布局方式,避免过于单调,可适当使用图表来呈现信息,例如折线图、柱状图、饼状图等,但背景颜色保持一致
|
|
85
|
+
- 如果需要插入图片,则按照以下方式:
|
|
86
|
+
- 收集全部图片素材,优先使用用户提供的;若未提供,则使用网络搜索合适的图片并保存到本地;如果仍未找到合适的,则调用图像生成工具生成图片
|
|
87
|
+
- 对图片素材进行重命名来编号,通过名称标识每个图片用于哪个slide及其宽高比例,例如\`slide01_1_1920X1080.png\`表示该图片用于第一张幻灯片,并且是该幻灯片中的第一张图,宽高为1920*1080
|
|
88
|
+
|
|
89
|
+
2. **逐页生成SVG代码**:
|
|
90
|
+
- 对于插图,统一使用\`rect\`或\`circle\`元素进行占位(必须符合图片宽高比例),之后转化成pptx时,在python脚本中换成真实的本地图片
|
|
91
|
+
- 如果当前系统配置了专门的\`svg\`工具或MCP,则优先使用工具生成高质量的svg代码;若未配置,则直接自行生成svg代码即可
|
|
92
|
+
- 每一页的svg代码都保存在独立的html文件中
|
|
93
|
+
|
|
94
|
+
3. **把SVG转成PPTX文件**:
|
|
95
|
+
- 参考snippet工具中提供的\`svg_to_pptx\`代码示例,编写临时的python脚本,把svg转化成可编辑的pptx文件,不要遗漏任何元素(例如线条、装饰等)
|
|
96
|
+
- 转化的方法是把svg代码转化成使用python创建pptx中可操作元素的代码,示例中的创建元素的工具函数均可以使用
|
|
97
|
+
- 同时对于每个slide务必创建单独的create_slide函数,例如\`create_slide1\`、\`create_slide2\`等
|
|
98
|
+
- **不要使用\`BeautifulSoup\`这种通用的解析元素的方式,而是逐个元素从svg代码转化为python创建元素的代码**
|
|
99
|
+
- 转换过程中,需要把图片占位元素替换成前面准备好的图片素材
|
|
100
|
+
|
|
101
|
+
4. **验证转换结果**:
|
|
102
|
+
- 转换完成后,需要把svg和pptx转化为图片,然后**逐页**对图片进行对比分析
|
|
103
|
+
- 判断两者**背景、容器、线条、文字、装饰的颜色、透明度和渐变效果**是否基本一致
|
|
104
|
+
- 如果差异较大,则需要进一步调整转化脚本
|
|
105
|
+
|
|
106
|
+
5. **保留工作文件**:
|
|
107
|
+
- 完成后**需要保留**规划文档、html页面文件和python转化脚本,方便进一步根据反馈进行修改`
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const buildSnippet = (title) => {
|
|
112
|
+
const entry = SNIPPETS[title];
|
|
113
|
+
if (!entry) {
|
|
114
|
+
const supported = Object.keys(SNIPPETS).join(', ');
|
|
115
|
+
throw new Error(`title 仅支持: ${supported}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lines = [];
|
|
119
|
+
lines.push(`【${entry.title}】参考脚本`);
|
|
120
|
+
if (entry.description) lines.push(entry.description);
|
|
121
|
+
|
|
122
|
+
if (entry.notes?.length) {
|
|
123
|
+
lines.push('要点:');
|
|
124
|
+
entry.notes.forEach((note) => lines.push(`- ${note}`));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (entry.extraFiles?.length) {
|
|
128
|
+
entry.extraFiles.forEach((extra) => {
|
|
129
|
+
const content = loadText(extra.file).trimEnd();
|
|
130
|
+
lines.push(extra.label || '配套文件');
|
|
131
|
+
const lang = extra.language || path.extname(extra.file).replace('.', '') || '';
|
|
132
|
+
lines.push(`\`\`\`${lang}`);
|
|
133
|
+
lines.push(content);
|
|
134
|
+
lines.push('```');
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const code = loadText(entry.codeFile).trimEnd();
|
|
139
|
+
lines.push('示例代码:');
|
|
140
|
+
lines.push('```python');
|
|
141
|
+
lines.push(code);
|
|
142
|
+
lines.push('```');
|
|
143
|
+
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const buildGuide = (title) => {
|
|
148
|
+
const entry = GUIDES[title];
|
|
149
|
+
if (!entry) {
|
|
150
|
+
const supported = Object.keys(GUIDES).join(', ');
|
|
151
|
+
throw new Error(`title 仅支持: ${supported}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const lines = [];
|
|
155
|
+
lines.push(`【${entry.title}】技能指导`);
|
|
156
|
+
lines.push(entry.content);
|
|
157
|
+
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const listTools = () => ({
|
|
162
|
+
tools: [
|
|
163
|
+
{
|
|
164
|
+
name: 'snippet',
|
|
165
|
+
description: '返回常见文件转换的 Python 参考脚本及要点说明',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
title: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: '示例类型,可选: html_to_png | pdf_to_png | pptx_to_pdf | svg_to_pptx',
|
|
172
|
+
enum: Object.keys(SNIPPETS)
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
required: ['title']
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'guide',
|
|
180
|
+
description: '返回技能指导说明',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
title: {
|
|
185
|
+
type: 'string',
|
|
186
|
+
description: '技能类型,可选: make_ppt',
|
|
187
|
+
enum: Object.keys(GUIDES)
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
required: ['title']
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const server = new Server({ name: 'skills', version: '2.2.0' }, { capabilities: { tools: {} } });
|
|
197
|
+
|
|
198
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => listTools());
|
|
199
|
+
|
|
200
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
201
|
+
const { name, arguments: args } = request.params;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
if (name === 'snippet') {
|
|
205
|
+
const title = args?.title;
|
|
206
|
+
const payload = buildSnippet(title);
|
|
207
|
+
return { content: [{ type: 'text', text: payload }] };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (name === 'guide') {
|
|
211
|
+
const title = args?.title;
|
|
212
|
+
const payload = buildGuide(title);
|
|
213
|
+
return { content: [{ type: 'text', text: payload }] };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { content: [{ type: 'text', text: `未知工具: ${name}` }], isError: true };
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const message = error?.message || '未知错误';
|
|
219
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const main = async () => {
|
|
224
|
+
const transport = new StdioServerTransport();
|
|
225
|
+
await server.connect(transport);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
main().catch((error) => {
|
|
229
|
+
console.error(error);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ww_nero/skills",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "MCP server that returns Python reference snippets and skill guides",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skills": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"assets"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.22.0"
|
|
15
|
+
}
|
|
16
|
+
}
|