├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── README.md ├── README_EN.md ├── example ├── Financial_Crisis_2008.png ├── Financial_Crisis_2008.pptx ├── 小红书如何写出爆款.png └── 小红书如何写出爆款.pptx └── mcp-server-okppt ├── .python-version ├── LICENSE ├── README.md ├── dist ├── mcp_server_okppt-0.1.9-py3-none-any.whl └── mcp_server_okppt-0.1.9.tar.gz ├── pyproject.toml ├── requirements.txt ├── src ├── mcp_server_okppt.egg-info │ ├── PKG-INFO │ ├── SOURCES.txt │ ├── dependency_links.txt │ ├── entry_points.txt │ ├── requires.txt │ └── top_level.txt └── mcp_server_okppt │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── prompts │ └── prompt_svg2ppt.md │ ├── server.py │ └── svg_module.py └── uv.lock /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | - name: Set up Python 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: '3.x' 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install build setuptools wheel twine 26 | - name: Build and Publish package 27 | working-directory: ./mcp-server-okppt 28 | env: 29 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 30 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 31 | run: | 32 | # 构建包 33 | python -m build 34 | # 检查并上传 35 | twine check dist/* 36 | twine upload dist/* 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cursor/rules/global.mdc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP OKPPT Server 2 | 3 | [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-blue)](https://github.com/anthropics/anthropic-tools) 4 | 5 | 一个基于Model Context Protocol (MCP)的服务器工具,专门用于将SVG图像插入到PowerPoint演示文稿中。它能够保留SVG的矢量特性,确保在PowerPoint中显示的图像保持高品质和可缩放性。 6 | 7 | ## 设计理念 8 | 9 | 此项目是让大型语言模型(如Claude、GPT等)能够自主设计PowerPoint演示文稿的"曲线救国"解决方案。通过让AI生成SVG图像,再借助本工具将其全屏插入PPT幻灯片,我们成功实现了AI完全控制PPT设计的能力,而无需直接操作复杂的PPT对象模型。 10 | 11 | 这种方法带来三大核心优势: 12 | 1. **AI完全控制**:充分发挥现代AI的图形设计能力,同时避开PPT编程的复杂性 13 | 2. **用户可编辑**:Office PowerPoint提供了强大的SVG编辑功能,插入后的SVG元素可以像原生PPT元素一样直接编辑、调整和重新着色,让用户能轻松地在AI生成基础上进行二次修改 14 | 3. **矢量级质量**:保持高品质可缩放的矢量特性,确保演示内容在任何尺寸下都清晰锐利 15 | 16 | 这一创新思路通过SVG作为AI与PPT之间的桥梁,既保证了设计的高度自由,又兼顾了最终成果的实用性和可维护性。 17 | 18 | ## 功能特点 19 | 20 | - **矢量图保留**: 将SVG作为真实矢量图插入PPTX,保证高品质和可缩放性 21 | - **批量批处理**: 支持一次操作多个SVG文件和幻灯片 22 | - **全新演示文稿**: 直接从SVG文件创建完整的演示文稿 23 | - **幻灯片复制与替换**: 智能复制SVG幻灯片并替换现有内容 24 | - **SVG代码处理**: 支持直接从SVG代码创建文件 25 | - **格式转换支持**: 内置SVG到PNG的转换功能 26 | 27 | ## PPT效果示例 28 | 29 | 以下是一些使用MCP OKPPT Server生成的PPT效果图: 30 | 31 | ![2008年金融危机](example/Financial_Crisis_2008.png) 32 | *2008年金融危机分析PPT封面* 33 | 34 | ![小红书爆款指南](example/小红书如何写出爆款.png) 35 | *小红书爆款内容分析报告PPT页面* 36 | 37 | ## 安装方法 38 | 39 | ### 方法一:从PyPI安装 40 | 41 | ```bash 42 | # 使用pip安装 43 | pip install mcp-server-okppt 44 | 45 | # 或使用uv安装 46 | uv pip install mcp-server-okppt 47 | ``` 48 | 49 | ### 方法二:配置Claude Desktop 50 | 51 | 在Claude Desktop配置文件中添加服务器配置: 52 | 53 | **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 54 | **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 55 | 56 | 添加以下配置: 57 | 58 | ```json 59 | { 60 | "mcpServers": { 61 | "okppt": { 62 | "command": "uvx", 63 | "args": [ 64 | "mcp-server-okppt" 65 | ] 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | ### 方法三:从源码安装并配置Cursor本地开发环境 72 | 73 | 在Cursor IDE中,可以通过本地配置文件来设置MCP服务器: 74 | 75 | **Windows**: `C:\Users\用户名\.cursor\mcp.json` 76 | **macOS**: `~/.cursor/mcp.json` 77 | 78 | 添加以下配置: 79 | 80 | ```json 81 | { 82 | "mcpServers": { 83 | "okppt": { 84 | "command": "uv", 85 | "args": [ 86 | "--directory", 87 | "D:\\本地项目路径\\mcp-server-okppt\\src\\mcp_server_okppt", 88 | "run", 89 | "cli.py" 90 | ] 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | 这种配置方式适合本地开发和测试使用,可以直接指向本地代码目录。 97 | 98 | ## 使用方法 99 | 100 | ### 使用Claude Desktop 101 | 102 | 1. 安装并配置Claude Desktop 103 | 2. 在配置文件中添加上述MCP服务器配置 104 | 3. 重启Claude Desktop 105 | 4. 在对话中使用PPTX相关工具 106 | 107 | ### 使用MCP CLI进行开发 108 | 109 | ```bash 110 | # 运行测试 111 | mcp test server.py 112 | ``` 113 | 114 | ## 可用工具 115 | 116 | ### 1. 插入SVG图像 (insert_svg) 117 | 118 | ```python 119 | def insert_svg( 120 | pptx_path: str, 121 | svg_path: List[str], 122 | slide_number: int = 1, 123 | x_inches: float = 0, 124 | y_inches: float = 0, 125 | width_inches: float = 16, 126 | height_inches: float = 9, 127 | output_path: str = "", 128 | create_if_not_exists: bool = True 129 | ) -> str 130 | ``` 131 | 132 | 将SVG图像插入到PPTX文件的指定位置。 133 | 134 | **参数**: 135 | - `pptx_path`: PPTX文件路径 136 | - `svg_path`: SVG文件路径或路径列表 137 | - `slide_number`: 要插入的幻灯片编号(从1开始) 138 | - `x_inches`: X坐标(英寸) 139 | - `y_inches`: Y坐标(英寸) 140 | - `width_inches`: 宽度(英寸) 141 | - `height_inches`: 高度(英寸) 142 | - `output_path`: 输出文件路径 143 | - `create_if_not_exists`: 如果PPTX不存在是否创建 144 | 145 | **返回**: 操作结果消息 146 | 147 | ### 2. 列出目录文件 (list_files) 148 | 149 | ```python 150 | def list_files( 151 | directory: str = ".", 152 | file_type: Optional[str] = None 153 | ) -> str 154 | ``` 155 | 156 | 列出目录中的文件。 157 | 158 | **参数**: 159 | - `directory`: 目录路径 160 | - `file_type`: 文件类型过滤,可以是"svg"或"pptx" 161 | 162 | **返回**: 文件列表 163 | 164 | ### 3. 获取文件信息 (get_file_info) 165 | 166 | ```python 167 | def get_file_info( 168 | file_path: str 169 | ) -> str 170 | ``` 171 | 172 | 获取文件信息。 173 | 174 | **参数**: 175 | - `file_path`: 文件路径 176 | 177 | **返回**: 文件信息 178 | 179 | ### 4. 转换SVG为PNG (convert_svg_to_png) 180 | 181 | ```python 182 | def convert_svg_to_png( 183 | svg_path: str, 184 | output_path: Optional[str] = None 185 | ) -> str 186 | ``` 187 | 188 | 将SVG文件转换为PNG图像。 189 | 190 | **参数**: 191 | - `svg_path`: SVG文件路径 192 | - `output_path`: 输出PNG文件路径 193 | 194 | **返回**: 操作结果消息 195 | 196 | ### 5. 获取PPTX信息 (get_pptx_info) 197 | 198 | ```python 199 | def get_pptx_info( 200 | pptx_path: str 201 | ) -> str 202 | ``` 203 | 204 | 获取PPTX文件的基本信息。 205 | 206 | **参数**: 207 | - `pptx_path`: PPTX文件路径 208 | 209 | **返回**: 包含文件信息和幻灯片数量的字符串 210 | 211 | ### 6. 保存SVG代码 (save_svg_code) 212 | 213 | ```python 214 | def save_svg_code( 215 | svg_code: str 216 | ) -> str 217 | ``` 218 | 219 | 将SVG代码保存为SVG文件并返回保存的绝对路径。 220 | 221 | **参数**: 222 | - `svg_code`: SVG代码内容 223 | 224 | **返回**: 操作结果消息和保存的文件路径 225 | 226 | ### 7. 删除幻灯片 (delete_slide) 227 | 228 | ```python 229 | def delete_slide( 230 | pptx_path: str, 231 | slide_number: int, 232 | output_path: Optional[str] = None 233 | ) -> str 234 | ``` 235 | 236 | 从PPTX文件中删除指定编号的幻灯片。 237 | 238 | **参数**: 239 | - `pptx_path`: PPTX文件路径 240 | - `slide_number`: 要删除的幻灯片编号 241 | - `output_path`: 输出文件路径 242 | 243 | **返回**: 操作结果消息 244 | 245 | ### 8. 插入空白幻灯片 (insert_blank_slide) 246 | 247 | ```python 248 | def insert_blank_slide( 249 | pptx_path: str, 250 | slide_number: int, 251 | layout_index: int = 6, 252 | output_path: Optional[str] = None, 253 | create_if_not_exists: bool = True 254 | ) -> str 255 | ``` 256 | 257 | 在PPTX文件的指定位置插入一个空白幻灯片。 258 | 259 | **参数**: 260 | - `pptx_path`: PPTX文件路径 261 | - `slide_number`: 插入位置 262 | - `layout_index`: 幻灯片布局索引,默认为6(空白布局) 263 | - `output_path`: 输出文件路径 264 | - `create_if_not_exists`: 如果PPTX不存在是否创建 265 | 266 | **返回**: 操作结果消息 267 | 268 | ### 9. 复制SVG幻灯片 (copy_svg_slide) 269 | 270 | ```python 271 | def copy_svg_slide( 272 | source_pptx_path: str, 273 | target_pptx_path: str = "", 274 | source_slide_number: int = 1, 275 | target_slide_number: Optional[int] = None, 276 | output_path: Optional[str] = None, 277 | create_if_not_exists: bool = True 278 | ) -> str 279 | ``` 280 | 281 | 复制包含SVG图像的幻灯片。 282 | 283 | **参数**: 284 | - `source_pptx_path`: 源PPTX文件路径 285 | - `target_pptx_path`: 目标PPTX文件路径 286 | - `source_slide_number`: 要复制的源幻灯片编号 287 | - `target_slide_number`: 要插入到目标文件的位置 288 | - `output_path`: 输出文件路径 289 | - `create_if_not_exists`: 如果目标PPTX不存在是否创建 290 | 291 | **返回**: 操作结果消息 292 | 293 | ## 最佳实践 294 | 295 | ### 替换幻灯片内容的推荐方法 296 | 297 | #### 方法一:完全替换法(最可靠) 298 | 299 | ```python 300 | # 步骤1:删除要替换的幻灯片 301 | delete_slide( 302 | pptx_path="演示文稿.pptx", 303 | slide_number=3, 304 | output_path="临时文件.pptx" 305 | ) 306 | 307 | # 步骤2:在同一位置插入空白幻灯片 308 | insert_blank_slide( 309 | pptx_path="临时文件.pptx", 310 | slide_number=3, 311 | output_path="临时文件2.pptx" 312 | ) 313 | 314 | # 步骤3:将新SVG插入到空白幻灯片 315 | insert_svg( 316 | pptx_path="临时文件2.pptx", 317 | svg_path=["新内容.svg"], 318 | slide_number=3, 319 | output_path="最终文件.pptx" 320 | ) 321 | ``` 322 | 323 | ## 注意事项 324 | 325 | 1. **避免内容叠加**:直接对现有幻灯片插入SVG会导致新内容叠加在原内容上,而非替换 326 | 2. **批量处理**:批量插入SVG时,`svg_path`参数必须是数组形式,即使只有一个文件 327 | 3. **SVG代码转义**:在使用`save_svg_code`时,特殊字符(如"&")需要正确转义为"&" 328 | 4. **文件路径**:尽量使用英文路径,避免路径中出现特殊字符 329 | 5. **检查结果**:每次操作后应检查输出文件以确认修改是否成功 330 | 331 | ## 常见问题解答 332 | 333 | ### Q: SVG插入后变成了位图而非矢量图? 334 | A: 请确保使用`copy_svg_slide`或`create_pptx_from_svg`函数,这些函数专门设计用于保留SVG的矢量特性。 335 | 336 | ### Q: 如何批量处理多个SVG文件? 337 | A: 可以使用`insert_svg`函数并将多个SVG路径作为列表传入,或者使用`create_pptx_from_svg`一次性创建包含多个SVG的演示文稿。 338 | 339 | ### Q: 文件名变得很长且复杂? 340 | A: 这是因为每次操作都会添加时间戳。建议使用"新文件法"一次性创建最终文件,或在最后一步操作中指定简洁的输出文件名。 341 | 342 | ## 版本信息 343 | 344 | 当前最新版本: v0.1.9 345 | 346 | 查看所有版本和更新信息: [GitHub Releases](https://github.com/NeekChaw/mcp-server-okppt/releases) 347 | 348 | ## 致谢 349 | 350 | 本项目在开发过程中受益于[Model Context Protocol(MCP) 编程极速入门](https://github.com/liaokongVFX/MCP-Chinese-Getting-Started-Guide)这一优质资源。该项目提供了全面而清晰的MCP开发指南,涵盖了从基础概念到实际部署的各个方面,极大地降低了开发者学习MCP协议的门槛。特别感谢其在服务配置、工具开发和部署流程等方面的详细示例和说明,为MCP生态的发展和普及做出了宝贵贡献。推荐所有对MCP开发感兴趣的开发者参考这份指南,它将帮助你快速掌握MCP服务器的开发与配置技能。 351 | 352 | ## 贡献指南 353 | 354 | 欢迎提交问题和拉取请求到[项目仓库](https://github.com/NeekChaw/mcp-server-okppt)!以下是一些潜在的改进方向: 355 | 356 | - 添加更多幻灯片布局支持 357 | - 增强SVG处理和兼容性 358 | - 添加批量SVG处理的进度报告 359 | - 改进错误处理和诊断功能 360 | - 添加图表和表格的特殊处理功能 361 | 362 | ## 许可证 363 | 364 | 本项目采用MIT许可证。 -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # MCP OKPPT Server 2 | 3 | [切换语言 | Language: [中文](README.md) | English] 4 | 5 | [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-blue)](https://github.com/anthropics/anthropic-tools) 6 | 7 | A Model Context Protocol (MCP) server tool specifically designed for inserting SVG images into PowerPoint presentations. It preserves the vector properties of SVGs, ensuring that images displayed in PowerPoint maintain high quality and scalability. 8 | 9 | ## Design Philosophy 10 | 11 | This project is a creative solution enabling large language models (like Claude, GPT, etc.) to autonomously design PowerPoint presentations. By allowing AI to generate SVG images and using this tool to insert them full-screen into PPT slides, we've successfully achieved AI's complete control over PPT design without directly manipulating the complex PPT object model. 12 | 13 | This approach offers three core advantages: 14 | 1. **Complete AI Control**: Fully leverages modern AI's graphic design capabilities while avoiding the complexity of PPT programming 15 | 2. **User Editable**: Office PowerPoint provides powerful SVG editing features, allowing inserted SVG elements to be edited, adjusted, and recolored like native PPT elements, enabling users to easily make secondary modifications based on AI-generated content 16 | 3. **Vector Quality**: Maintains high-quality scalable vector properties, ensuring presentation content remains clear and sharp at any size 17 | 18 | This innovative approach uses SVG as a bridge between AI and PPT, guaranteeing both high design freedom and the practicality and maintainability of the final product. 19 | 20 | ## Features 21 | 22 | - **Vector Preservation**: Inserts SVGs as true vector graphics into PPTX, ensuring high quality and scalability 23 | - **Batch Processing**: Supports multiple SVG files and slides in a single operation 24 | - **New Presentations**: Creates complete presentations directly from SVG files 25 | - **Slide Copying & Replacement**: Intelligently copies SVG slides and replaces existing content 26 | - **SVG Code Handling**: Supports direct creation of files from SVG code 27 | - **Format Conversion**: Built-in SVG to PNG conversion functionality 28 | 29 | ## PPT Examples Showcase 30 | 31 | Here are some examples of PPT slides generated using MCP OKPPT Server: 32 | 33 | ![2008 Financial Crisis](example/Financial_Crisis_2008.png) 34 | *Cover slide for a presentation on the 2008 Financial Crisis* 35 | 36 | ![Xiaohongshu Guide](example/小红书如何写出爆款.png) 37 | *Slide from a report on creating popular content on Xiaohongshu* 38 | 39 | ## Installation 40 | 41 | ### Method 1: Install from PyPI 42 | 43 | ```bash 44 | # Using pip 45 | pip install mcp-server-okppt 46 | 47 | # Or using uv 48 | uv pip install mcp-server-okppt 49 | ``` 50 | 51 | ### Method 2: Configure Claude Desktop 52 | 53 | Add the server configuration to your Claude Desktop config file: 54 | 55 | **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 56 | **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 57 | 58 | Add the following configuration: 59 | 60 | ```json 61 | { 62 | "mcpServers": { 63 | "okppt": { 64 | "command": "uvx", 65 | "args": [ 66 | "mcp-server-okppt" 67 | ] 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ### Method 3: Install from Source and Configure for Cursor Local Development 74 | 75 | In Cursor IDE, set up the MCP server via local configuration file: 76 | 77 | **Windows**: `C:\Users\username\.cursor\mcp.json` 78 | **macOS**: `~/.cursor/mcp.json` 79 | 80 | Add the following configuration: 81 | 82 | ```json 83 | { 84 | "mcpServers": { 85 | "okppt": { 86 | "command": "uv", 87 | "args": [ 88 | "--directory", 89 | "D:\\local_project_path\\mcp-server-okppt\\src\\mcp_server_okppt", 90 | "run", 91 | "cli.py" 92 | ] 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | This configuration method is suitable for local development and testing, allowing you to point directly to your local code directory. 99 | 100 | ## Usage 101 | 102 | ### Using Claude Desktop 103 | 104 | 1. Install and configure Claude Desktop 105 | 2. Add the MCP server configuration above to the config file 106 | 3. Restart Claude Desktop 107 | 4. Use PPTX-related tools in your conversations 108 | 109 | ### Using MCP CLI for Development 110 | 111 | ```bash 112 | # Run tests 113 | mcp test server.py 114 | ``` 115 | 116 | ## Available Tools 117 | 118 | ### 1. Insert SVG Image (insert_svg) 119 | 120 | ```python 121 | def insert_svg( 122 | pptx_path: str, 123 | svg_path: List[str], 124 | slide_number: int = 1, 125 | x_inches: float = 0, 126 | y_inches: float = 0, 127 | width_inches: float = 16, 128 | height_inches: float = 9, 129 | output_path: str = "", 130 | create_if_not_exists: bool = True 131 | ) -> str 132 | ``` 133 | 134 | Inserts an SVG image into a specified position in a PPTX file. 135 | 136 | **Parameters**: 137 | - `pptx_path`: Path to the PPTX file 138 | - `svg_path`: Path or list of paths to SVG files 139 | - `slide_number`: Slide number to insert into (starting from 1) 140 | - `x_inches`: X coordinate (inches) 141 | - `y_inches`: Y coordinate (inches) 142 | - `width_inches`: Width (inches) 143 | - `height_inches`: Height (inches) 144 | - `output_path`: Output file path 145 | - `create_if_not_exists`: Whether to create the PPTX if it doesn't exist 146 | 147 | **Returns**: Operation result message 148 | 149 | ### 2. List Directory Files (list_files) 150 | 151 | ```python 152 | def list_files( 153 | directory: str = ".", 154 | file_type: Optional[str] = None 155 | ) -> str 156 | ``` 157 | 158 | Lists files in a directory. 159 | 160 | **Parameters**: 161 | - `directory`: Directory path 162 | - `file_type`: File type filter, can be "svg" or "pptx" 163 | 164 | **Returns**: List of files 165 | 166 | ### 3. Get File Information (get_file_info) 167 | 168 | ```python 169 | def get_file_info( 170 | file_path: str 171 | ) -> str 172 | ``` 173 | 174 | Gets file information. 175 | 176 | **Parameters**: 177 | - `file_path`: Path to the file 178 | 179 | **Returns**: File information 180 | 181 | ### 4. Convert SVG to PNG (convert_svg_to_png) 182 | 183 | ```python 184 | def convert_svg_to_png( 185 | svg_path: str, 186 | output_path: Optional[str] = None 187 | ) -> str 188 | ``` 189 | 190 | Converts an SVG file to a PNG image. 191 | 192 | **Parameters**: 193 | - `svg_path`: Path to the SVG file 194 | - `output_path`: Output PNG file path 195 | 196 | **Returns**: Operation result message 197 | 198 | ### 5. Get PPTX Information (get_pptx_info) 199 | 200 | ```python 201 | def get_pptx_info( 202 | pptx_path: str 203 | ) -> str 204 | ``` 205 | 206 | Gets basic information about a PPTX file. 207 | 208 | **Parameters**: 209 | - `pptx_path`: Path to the PPTX file 210 | 211 | **Returns**: String containing file information and slide count 212 | 213 | ### 6. Save SVG Code (save_svg_code) 214 | 215 | ```python 216 | def save_svg_code( 217 | svg_code: str 218 | ) -> str 219 | ``` 220 | 221 | Saves SVG code as an SVG file and returns the absolute path. 222 | 223 | **Parameters**: 224 | - `svg_code`: SVG code content 225 | 226 | **Returns**: Operation result message and saved file path 227 | 228 | ### 7. Delete Slide (delete_slide) 229 | 230 | ```python 231 | def delete_slide( 232 | pptx_path: str, 233 | slide_number: int, 234 | output_path: Optional[str] = None 235 | ) -> str 236 | ``` 237 | 238 | Deletes a specific slide from a PPTX file. 239 | 240 | **Parameters**: 241 | - `pptx_path`: Path to the PPTX file 242 | - `slide_number`: Slide number to delete 243 | - `output_path`: Output file path 244 | 245 | **Returns**: Operation result message 246 | 247 | ### 8. Insert Blank Slide (insert_blank_slide) 248 | 249 | ```python 250 | def insert_blank_slide( 251 | pptx_path: str, 252 | slide_number: int, 253 | layout_index: int = 6, 254 | output_path: Optional[str] = None, 255 | create_if_not_exists: bool = True 256 | ) -> str 257 | ``` 258 | 259 | Inserts a blank slide at a specified position in a PPTX file. 260 | 261 | **Parameters**: 262 | - `pptx_path`: Path to the PPTX file 263 | - `slide_number`: Position to insert the slide 264 | - `layout_index`: Slide layout index, default is 6 (blank layout) 265 | - `output_path`: Output file path 266 | - `create_if_not_exists`: Whether to create the PPTX if it doesn't exist 267 | 268 | **Returns**: Operation result message 269 | 270 | ### 9. Copy SVG Slide (copy_svg_slide) 271 | 272 | ```python 273 | def copy_svg_slide( 274 | source_pptx_path: str, 275 | target_pptx_path: str = "", 276 | source_slide_number: int = 1, 277 | target_slide_number: Optional[int] = None, 278 | output_path: Optional[str] = None, 279 | create_if_not_exists: bool = True 280 | ) -> str 281 | ``` 282 | 283 | Copies a slide containing an SVG image. 284 | 285 | **Parameters**: 286 | - `source_pptx_path`: Source PPTX file path 287 | - `target_pptx_path`: Target PPTX file path 288 | - `source_slide_number`: Source slide number to copy 289 | - `target_slide_number`: Position to insert in target file 290 | - `output_path`: Output file path 291 | - `create_if_not_exists`: Whether to create the target PPTX if it doesn't exist 292 | 293 | **Returns**: Operation result message 294 | 295 | ## Best Practices 296 | 297 | ### Recommended Methods for Replacing Slide Content 298 | 299 | #### Method 1: Complete Replacement (Most Reliable) 300 | 301 | ```python 302 | # Step 1: Delete the slide to be replaced 303 | delete_slide( 304 | pptx_path="presentation.pptx", 305 | slide_number=3, 306 | output_path="temp_file.pptx" 307 | ) 308 | 309 | # Step 2: Insert a blank slide at the same position 310 | insert_blank_slide( 311 | pptx_path="temp_file.pptx", 312 | slide_number=3, 313 | output_path="temp_file2.pptx" 314 | ) 315 | 316 | # Step 3: Insert the new SVG into the blank slide 317 | insert_svg( 318 | pptx_path="temp_file2.pptx", 319 | svg_path=["new_content.svg"], 320 | slide_number=3, 321 | output_path="final_file.pptx" 322 | ) 323 | ``` 324 | 325 | ## Important Notes 326 | 327 | 1. **Avoid Content Overlay**: Inserting SVGs directly into existing slides will cause new content to overlay the original content rather than replace it 328 | 2. **Batch Processing**: When batch inserting SVGs, the `svg_path` parameter must be in array format, even if there's only one file 329 | 3. **SVG Code Escaping**: When using `save_svg_code`, special characters (like "&") need to be properly escaped as "&" 330 | 4. **File Paths**: Try to use English paths and avoid special characters in paths 331 | 5. **Check Results**: Always check the output file after each operation to confirm the modifications were successful 332 | 333 | ## Frequently Asked Questions 334 | 335 | ### Q: The SVG is inserted as a bitmap rather than a vector graphic? 336 | A: Make sure to use the `copy_svg_slide` or `create_pptx_from_svg` functions, which are specifically designed to preserve the vector properties of SVGs. 337 | 338 | ### Q: How can I process multiple SVG files in batch? 339 | A: You can use the `insert_svg` function and pass multiple SVG paths as a list, or use `create_pptx_from_svg` to create a presentation with multiple SVGs at once. 340 | 341 | ### Q: The filenames are becoming long and complex? 342 | A: This happens because each operation adds a timestamp. We recommend using the "New File Method" to create the final file at once, or specifying a concise output filename in the last operation. 343 | 344 | ## Version Information 345 | 346 | Current latest version: v0.1.9 347 | 348 | View all versions and update information: [GitHub Releases](https://github.com/NeekChaw/mcp-server-okppt/releases) 349 | 350 | ## Acknowledgements 351 | 352 | This project benefited from the excellent resource [Model Context Protocol(MCP) Quick Start Guide in Chinese](https://github.com/liaokongVFX/MCP-Chinese-Getting-Started-Guide) during its development. This resource provides comprehensive and clear guidance for MCP development, covering everything from basic concepts to practical deployment, greatly reducing the learning curve for developers learning the MCP protocol. Special thanks for the detailed examples and explanations regarding service configuration, tool development, and deployment processes, making valuable contributions to the development and popularization of the MCP ecosystem. We recommend this guide to all developers interested in MCP development, as it will help you quickly master the skills of developing and configuring MCP servers. 353 | 354 | ## Contribution Guidelines 355 | 356 | Issues and pull requests are welcome at the [project repository](https://github.com/NeekChaw/mcp-server-okppt)! Here are some potential areas for improvement: 357 | 358 | - Add support for more slide layouts 359 | - Enhance SVG processing and compatibility 360 | - Add progress reporting for batch SVG processing 361 | - Improve error handling and diagnostics 362 | - Add special handling for charts and tables 363 | 364 | ## License 365 | 366 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /example/Financial_Crisis_2008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeekChaw/mcp-server-okppt/547744332778c63bd7e0913940ed59f73014c7b6/example/Financial_Crisis_2008.png -------------------------------------------------------------------------------- /example/Financial_Crisis_2008.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeekChaw/mcp-server-okppt/547744332778c63bd7e0913940ed59f73014c7b6/example/Financial_Crisis_2008.pptx -------------------------------------------------------------------------------- /example/小红书如何写出爆款.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeekChaw/mcp-server-okppt/547744332778c63bd7e0913940ed59f73014c7b6/example/小红书如何写出爆款.png -------------------------------------------------------------------------------- /example/小红书如何写出爆款.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeekChaw/mcp-server-okppt/547744332778c63bd7e0913940ed59f73014c7b6/example/小红书如何写出爆款.pptx -------------------------------------------------------------------------------- /mcp-server-okppt/.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /mcp-server-okppt/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NeekChaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /mcp-server-okppt/README.md: -------------------------------------------------------------------------------- 1 | # MCP OKPPT Server 2 | 3 | [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-blue)](https://github.com/anthropics/anthropic-tools) 4 | 5 | 一个基于Model Context Protocol (MCP)的服务器工具,专门用于将SVG图像插入到PowerPoint演示文稿中。它能够保留SVG的矢量特性,确保在PowerPoint中显示的图像保持高品质和可缩放性。 6 | 7 | ## 设计理念 8 | 9 | 此项目是让大型语言模型(如Claude、GPT等)能够自主设计PowerPoint演示文稿的"曲线救国"解决方案。通过让AI生成SVG图像,再借助本工具将其全屏插入PPT幻灯片,我们成功实现了AI完全控制PPT设计的能力,而无需直接操作复杂的PPT对象模型。 10 | 11 | 这种方法带来三大核心优势: 12 | 1. **AI完全控制**:充分发挥现代AI的图形设计能力,同时避开PPT编程的复杂性 13 | 2. **用户可编辑**:Office PowerPoint提供了强大的SVG编辑功能,插入后的SVG元素可以像原生PPT元素一样直接编辑、调整和重新着色,让用户能轻松地在AI生成基础上进行二次修改 14 | 3. **矢量级质量**:保持高品质可缩放的矢量特性,确保演示内容在任何尺寸下都清晰锐利 15 | 16 | 这一创新思路通过SVG作为AI与PPT之间的桥梁,既保证了设计的高度自由,又兼顾了最终成果的实用性和可维护性。 17 | 18 | ## 功能特点 19 | 20 | - **矢量图保留**: 将SVG作为真实矢量图插入PPTX,保证高品质和可缩放性 21 | - **批量批处理**: 支持一次操作多个SVG文件和幻灯片 22 | - **全新演示文稿**: 直接从SVG文件创建完整的演示文稿 23 | - **幻灯片复制与替换**: 智能复制SVG幻灯片并替换现有内容 24 | - **SVG代码处理**: 支持直接从SVG代码创建文件 25 | - **格式转换支持**: 内置SVG到PNG的转换功能 26 | 27 | ## 安装方法 28 | 29 | ### 方法一:从PyPI安装 30 | 31 | ```bash 32 | # 使用pip安装 33 | pip install mcp-server-okppt 34 | 35 | # 或使用uv安装 36 | uv pip install mcp-server-okppt 37 | ``` 38 | 39 | ### 方法二:配置Claude Desktop 40 | 41 | 在Claude Desktop配置文件中添加服务器配置: 42 | 43 | **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 44 | **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 45 | 46 | 添加以下配置: 47 | 48 | ```json 49 | { 50 | "mcpServers": { 51 | "okppt": { 52 | "command": "uvx", 53 | "args": [ 54 | "mcp-server-okppt" 55 | ] 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | ### 方法三:从源码安装并配置Cursor本地开发环境 62 | 63 | 在Cursor IDE中,可以通过本地配置文件来设置MCP服务器: 64 | 65 | **Windows**: `C:\Users\用户名\.cursor\mcp.json` 66 | **macOS**: `~/.cursor/mcp.json` 67 | 68 | 添加以下配置: 69 | 70 | ```json 71 | { 72 | "mcpServers": { 73 | "okppt": { 74 | "command": "uv", 75 | "args": [ 76 | "--directory", 77 | "D:\\本地项目路径\\mcp-server-okppt\\src\\mcp_server_okppt", 78 | "run", 79 | "cli.py" 80 | ] 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | 这种配置方式适合本地开发和测试使用,可以直接指向本地代码目录。 87 | 88 | ## 使用方法 89 | 90 | ### 使用Claude Desktop 91 | 92 | 1. 安装并配置Claude Desktop 93 | 2. 在配置文件中添加上述MCP服务器配置 94 | 3. 重启Claude Desktop 95 | 4. 在对话中使用PPTX相关工具 96 | 97 | ### 使用MCP CLI进行开发 98 | 99 | ```bash 100 | # 运行测试 101 | mcp test server.py 102 | ``` 103 | 104 | ## 可用工具 105 | 106 | ### 1. 插入SVG图像 (insert_svg) 107 | 108 | ```python 109 | def insert_svg( 110 | pptx_path: str, 111 | svg_path: List[str], 112 | slide_number: int = 1, 113 | x_inches: float = 0, 114 | y_inches: float = 0, 115 | width_inches: float = 16, 116 | height_inches: float = 9, 117 | output_path: str = "", 118 | create_if_not_exists: bool = True 119 | ) -> str 120 | ``` 121 | 122 | 将SVG图像插入到PPTX文件的指定位置。 123 | 124 | **参数**: 125 | - `pptx_path`: PPTX文件路径 126 | - `svg_path`: SVG文件路径或路径列表 127 | - `slide_number`: 要插入的幻灯片编号(从1开始) 128 | - `x_inches`: X坐标(英寸) 129 | - `y_inches`: Y坐标(英寸) 130 | - `width_inches`: 宽度(英寸) 131 | - `height_inches`: 高度(英寸) 132 | - `output_path`: 输出文件路径 133 | - `create_if_not_exists`: 如果PPTX不存在是否创建 134 | 135 | **返回**: 操作结果消息 136 | 137 | ### 2. 列出目录文件 (list_files) 138 | 139 | ```python 140 | def list_files( 141 | directory: str = ".", 142 | file_type: Optional[str] = None 143 | ) -> str 144 | ``` 145 | 146 | 列出目录中的文件。 147 | 148 | **参数**: 149 | - `directory`: 目录路径 150 | - `file_type`: 文件类型过滤,可以是"svg"或"pptx" 151 | 152 | **返回**: 文件列表 153 | 154 | ### 3. 获取文件信息 (get_file_info) 155 | 156 | ```python 157 | def get_file_info( 158 | file_path: str 159 | ) -> str 160 | ``` 161 | 162 | 获取文件信息。 163 | 164 | **参数**: 165 | - `file_path`: 文件路径 166 | 167 | **返回**: 文件信息 168 | 169 | ### 4. 转换SVG为PNG (convert_svg_to_png) 170 | 171 | ```python 172 | def convert_svg_to_png( 173 | svg_path: str, 174 | output_path: Optional[str] = None 175 | ) -> str 176 | ``` 177 | 178 | 将SVG文件转换为PNG图像。 179 | 180 | **参数**: 181 | - `svg_path`: SVG文件路径 182 | - `output_path`: 输出PNG文件路径 183 | 184 | **返回**: 操作结果消息 185 | 186 | ### 5. 获取PPTX信息 (get_pptx_info) 187 | 188 | ```python 189 | def get_pptx_info( 190 | pptx_path: str 191 | ) -> str 192 | ``` 193 | 194 | 获取PPTX文件的基本信息。 195 | 196 | **参数**: 197 | - `pptx_path`: PPTX文件路径 198 | 199 | **返回**: 包含文件信息和幻灯片数量的字符串 200 | 201 | ### 6. 保存SVG代码 (save_svg_code) 202 | 203 | ```python 204 | def save_svg_code( 205 | svg_code: str 206 | ) -> str 207 | ``` 208 | 209 | 将SVG代码保存为SVG文件并返回保存的绝对路径。 210 | 211 | **参数**: 212 | - `svg_code`: SVG代码内容 213 | 214 | **返回**: 操作结果消息和保存的文件路径 215 | 216 | ### 7. 删除幻灯片 (delete_slide) 217 | 218 | ```python 219 | def delete_slide( 220 | pptx_path: str, 221 | slide_number: int, 222 | output_path: Optional[str] = None 223 | ) -> str 224 | ``` 225 | 226 | 从PPTX文件中删除指定编号的幻灯片。 227 | 228 | **参数**: 229 | - `pptx_path`: PPTX文件路径 230 | - `slide_number`: 要删除的幻灯片编号 231 | - `output_path`: 输出文件路径 232 | 233 | **返回**: 操作结果消息 234 | 235 | ### 8. 插入空白幻灯片 (insert_blank_slide) 236 | 237 | ```python 238 | def insert_blank_slide( 239 | pptx_path: str, 240 | slide_number: int, 241 | layout_index: int = 6, 242 | output_path: Optional[str] = None, 243 | create_if_not_exists: bool = True 244 | ) -> str 245 | ``` 246 | 247 | 在PPTX文件的指定位置插入一个空白幻灯片。 248 | 249 | **参数**: 250 | - `pptx_path`: PPTX文件路径 251 | - `slide_number`: 插入位置 252 | - `layout_index`: 幻灯片布局索引,默认为6(空白布局) 253 | - `output_path`: 输出文件路径 254 | - `create_if_not_exists`: 如果PPTX不存在是否创建 255 | 256 | **返回**: 操作结果消息 257 | 258 | ### 9. 复制SVG幻灯片 (copy_svg_slide) 259 | 260 | ```python 261 | def copy_svg_slide( 262 | source_pptx_path: str, 263 | target_pptx_path: str = "", 264 | source_slide_number: int = 1, 265 | target_slide_number: Optional[int] = None, 266 | output_path: Optional[str] = None, 267 | create_if_not_exists: bool = True 268 | ) -> str 269 | ``` 270 | 271 | 复制包含SVG图像的幻灯片。 272 | 273 | **参数**: 274 | - `source_pptx_path`: 源PPTX文件路径 275 | - `target_pptx_path`: 目标PPTX文件路径 276 | - `source_slide_number`: 要复制的源幻灯片编号 277 | - `target_slide_number`: 要插入到目标文件的位置 278 | - `output_path`: 输出文件路径 279 | - `create_if_not_exists`: 如果目标PPTX不存在是否创建 280 | 281 | **返回**: 操作结果消息 282 | 283 | ## 最佳实践 284 | 285 | ### 替换幻灯片内容的推荐方法 286 | 287 | #### 方法一:完全替换法(最可靠) 288 | 289 | ```python 290 | # 步骤1:删除要替换的幻灯片 291 | delete_slide( 292 | pptx_path="演示文稿.pptx", 293 | slide_number=3, 294 | output_path="临时文件.pptx" 295 | ) 296 | 297 | # 步骤2:在同一位置插入空白幻灯片 298 | insert_blank_slide( 299 | pptx_path="临时文件.pptx", 300 | slide_number=3, 301 | output_path="临时文件2.pptx" 302 | ) 303 | 304 | # 步骤3:将新SVG插入到空白幻灯片 305 | insert_svg( 306 | pptx_path="临时文件2.pptx", 307 | svg_path=["新内容.svg"], 308 | slide_number=3, 309 | output_path="最终文件.pptx" 310 | ) 311 | ``` 312 | 313 | ## 注意事项 314 | 315 | 1. **避免内容叠加**:直接对现有幻灯片插入SVG会导致新内容叠加在原内容上,而非替换 316 | 2. **批量处理**:批量插入SVG时,`svg_path`参数必须是数组形式,即使只有一个文件 317 | 3. **SVG代码转义**:在使用`save_svg_code`时,特殊字符(如"&")需要正确转义为"&" 318 | 4. **文件路径**:尽量使用英文路径,避免路径中出现特殊字符 319 | 5. **检查结果**:每次操作后应检查输出文件以确认修改是否成功 320 | 321 | ## 常见问题解答 322 | 323 | ### Q: SVG插入后变成了位图而非矢量图? 324 | A: 请确保使用`copy_svg_slide`或`create_pptx_from_svg`函数,这些函数专门设计用于保留SVG的矢量特性。 325 | 326 | ### Q: 如何批量处理多个SVG文件? 327 | A: 可以使用`insert_svg`函数并将多个SVG路径作为列表传入,或者使用`create_pptx_from_svg`一次性创建包含多个SVG的演示文稿。 328 | 329 | ### Q: 文件名变得很长且复杂? 330 | A: 这是因为每次操作都会添加时间戳。建议使用"新文件法"一次性创建最终文件,或在最后一步操作中指定简洁的输出文件名。 331 | 332 | ## 版本信息 333 | 334 | 当前最新版本: v0.1.9 335 | 336 | 查看所有版本和更新信息: [GitHub Releases](https://github.com/NeekChaw/mcp-server-okppt/releases) 337 | 338 | ## 致谢 339 | 340 | 本项目在开发过程中受益于[Model Context Protocol(MCP) 编程极速入门](https://github.com/liaokongVFX/MCP-Chinese-Getting-Started-Guide)这一优质资源。该项目提供了全面而清晰的MCP开发指南,涵盖了从基础概念到实际部署的各个方面,极大地降低了开发者学习MCP协议的门槛。特别感谢其在服务配置、工具开发和部署流程等方面的详细示例和说明,为MCP生态的发展和普及做出了宝贵贡献。推荐所有对MCP开发感兴趣的开发者参考这份指南,它将帮助你快速掌握MCP服务器的开发与配置技能。 341 | 342 | ## 贡献指南 343 | 344 | 欢迎提交问题和拉取请求到[项目仓库](https://github.com/NeekChaw/mcp-server-okppt)!以下是一些潜在的改进方向: 345 | 346 | - 添加更多幻灯片布局支持 347 | - 增强SVG处理和兼容性 348 | - 添加批量SVG处理的进度报告 349 | - 改进错误处理和诊断功能 350 | - 添加图表和表格的特殊处理功能 351 | 352 | ## 许可证 353 | 354 | 本项目采用MIT许可证。 -------------------------------------------------------------------------------- /mcp-server-okppt/dist/mcp_server_okppt-0.1.9-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeekChaw/mcp-server-okppt/547744332778c63bd7e0913940ed59f73014c7b6/mcp-server-okppt/dist/mcp_server_okppt-0.1.9-py3-none-any.whl -------------------------------------------------------------------------------- /mcp-server-okppt/dist/mcp_server_okppt-0.1.9.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeekChaw/mcp-server-okppt/547744332778c63bd7e0913940ed59f73014c7b6/mcp-server-okppt/dist/mcp_server_okppt-0.1.9.tar.gz -------------------------------------------------------------------------------- /mcp-server-okppt/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-server-okppt" 3 | version = "0.1.9" 4 | description = "A Python package to insert SVG images into PowerPoint presentations, with MCP server capabilities." 5 | authors = [ 6 | { name="NeekChaw", email="your.email@example.com" }, 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.10" 10 | dependencies = [ 11 | "mcp[cli]>=1.8.0", 12 | "lxml>=4.9.2", 13 | "python-pptx>=0.6.21", 14 | "reportlab>=4.0.0", 15 | "svglib>=1.5.1", 16 | "rlPyCairo>=0.1.0", 17 | ] 18 | 19 | [project.scripts] 20 | mcp-server-okppt = "mcp_server_okppt.cli:main" 21 | 22 | [project.urls] 23 | "Homepage" = "https://github.com/NeekChaw/mcp-server-okppt" 24 | "Bug Tracker" = "https://github.com/NeekChaw/mcp-server-okppt/issues" 25 | 26 | [build-system] 27 | requires = ["setuptools>=61.0"] 28 | build-backend = "setuptools.build_meta" 29 | -------------------------------------------------------------------------------- /mcp-server-okppt/requirements.txt: -------------------------------------------------------------------------------- 1 | mcp>=1.8.0 2 | lxml>=4.9.2 3 | python-pptx>=0.6.21 4 | reportlab>=4.0.0 5 | svglib>=1.5.1 6 | rlPyCairo>=0.1.0 -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.4 2 | Name: mcp-server-okppt 3 | Version: 0.1.9 4 | Summary: A Python package to insert SVG images into PowerPoint presentations, with MCP server capabilities. 5 | Author-email: NeekChaw 6 | Project-URL: Homepage, https://github.com/NeekChaw/mcp-server-okppt 7 | Project-URL: Bug Tracker, https://github.com/NeekChaw/mcp-server-okppt/issues 8 | Requires-Python: >=3.10 9 | Description-Content-Type: text/markdown 10 | License-File: LICENSE 11 | Requires-Dist: mcp[cli]>=1.8.0 12 | Requires-Dist: lxml>=4.9.2 13 | Requires-Dist: python-pptx>=0.6.21 14 | Requires-Dist: reportlab>=4.0.0 15 | Requires-Dist: svglib>=1.5.1 16 | Requires-Dist: rlPyCairo>=0.1.0 17 | Dynamic: license-file 18 | 19 | # MCP OKPPT Server 20 | 21 | [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-blue)](https://github.com/anthropics/anthropic-tools) 22 | 23 | 一个基于Model Context Protocol (MCP)的服务器工具,专门用于将SVG图像插入到PowerPoint演示文稿中。它能够保留SVG的矢量特性,确保在PowerPoint中显示的图像保持高品质和可缩放性。 24 | 25 | ## 设计理念 26 | 27 | 此项目是让大型语言模型(如Claude、GPT等)能够自主设计PowerPoint演示文稿的"曲线救国"解决方案。通过让AI生成SVG图像,再借助本工具将其全屏插入PPT幻灯片,我们成功实现了AI完全控制PPT设计的能力,而无需直接操作复杂的PPT对象模型。 28 | 29 | 这种方法带来三大核心优势: 30 | 1. **AI完全控制**:充分发挥现代AI的图形设计能力,同时避开PPT编程的复杂性 31 | 2. **用户可编辑**:Office PowerPoint提供了强大的SVG编辑功能,插入后的SVG元素可以像原生PPT元素一样直接编辑、调整和重新着色,让用户能轻松地在AI生成基础上进行二次修改 32 | 3. **矢量级质量**:保持高品质可缩放的矢量特性,确保演示内容在任何尺寸下都清晰锐利 33 | 34 | 这一创新思路通过SVG作为AI与PPT之间的桥梁,既保证了设计的高度自由,又兼顾了最终成果的实用性和可维护性。 35 | 36 | ## 功能特点 37 | 38 | - **矢量图保留**: 将SVG作为真实矢量图插入PPTX,保证高品质和可缩放性 39 | - **批量批处理**: 支持一次操作多个SVG文件和幻灯片 40 | - **全新演示文稿**: 直接从SVG文件创建完整的演示文稿 41 | - **幻灯片复制与替换**: 智能复制SVG幻灯片并替换现有内容 42 | - **SVG代码处理**: 支持直接从SVG代码创建文件 43 | - **格式转换支持**: 内置SVG到PNG的转换功能 44 | 45 | ## 安装方法 46 | 47 | ### 方法一:从PyPI安装 48 | 49 | ```bash 50 | # 使用pip安装 51 | pip install mcp-server-okppt 52 | 53 | # 或使用uv安装 54 | uv pip install mcp-server-okppt 55 | ``` 56 | 57 | ### 方法二:配置Claude Desktop 58 | 59 | 在Claude Desktop配置文件中添加服务器配置: 60 | 61 | **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 62 | **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 63 | 64 | 添加以下配置: 65 | 66 | ```json 67 | { 68 | "mcpServers": { 69 | "okppt": { 70 | "command": "uvx", 71 | "args": [ 72 | "mcp-server-okppt" 73 | ] 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | ### 方法三:从源码安装并配置Cursor本地开发环境 80 | 81 | 在Cursor IDE中,可以通过本地配置文件来设置MCP服务器: 82 | 83 | **Windows**: `C:\Users\用户名\.cursor\mcp.json` 84 | **macOS**: `~/.cursor/mcp.json` 85 | 86 | 添加以下配置: 87 | 88 | ```json 89 | { 90 | "mcpServers": { 91 | "okppt": { 92 | "command": "uv", 93 | "args": [ 94 | "--directory", 95 | "D:\\本地项目路径\\mcp-server-okppt\\src\\mcp_server_okppt", 96 | "run", 97 | "cli.py" 98 | ] 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | 这种配置方式适合本地开发和测试使用,可以直接指向本地代码目录。 105 | 106 | ## 使用方法 107 | 108 | ### 使用Claude Desktop 109 | 110 | 1. 安装并配置Claude Desktop 111 | 2. 在配置文件中添加上述MCP服务器配置 112 | 3. 重启Claude Desktop 113 | 4. 在对话中使用PPTX相关工具 114 | 115 | ### 使用MCP CLI进行开发 116 | 117 | ```bash 118 | # 运行测试 119 | mcp test server.py 120 | ``` 121 | 122 | ## 可用工具 123 | 124 | ### 1. 插入SVG图像 (insert_svg) 125 | 126 | ```python 127 | def insert_svg( 128 | pptx_path: str, 129 | svg_path: List[str], 130 | slide_number: int = 1, 131 | x_inches: float = 0, 132 | y_inches: float = 0, 133 | width_inches: float = 16, 134 | height_inches: float = 9, 135 | output_path: str = "", 136 | create_if_not_exists: bool = True 137 | ) -> str 138 | ``` 139 | 140 | 将SVG图像插入到PPTX文件的指定位置。 141 | 142 | **参数**: 143 | - `pptx_path`: PPTX文件路径 144 | - `svg_path`: SVG文件路径或路径列表 145 | - `slide_number`: 要插入的幻灯片编号(从1开始) 146 | - `x_inches`: X坐标(英寸) 147 | - `y_inches`: Y坐标(英寸) 148 | - `width_inches`: 宽度(英寸) 149 | - `height_inches`: 高度(英寸) 150 | - `output_path`: 输出文件路径 151 | - `create_if_not_exists`: 如果PPTX不存在是否创建 152 | 153 | **返回**: 操作结果消息 154 | 155 | ### 2. 列出目录文件 (list_files) 156 | 157 | ```python 158 | def list_files( 159 | directory: str = ".", 160 | file_type: Optional[str] = None 161 | ) -> str 162 | ``` 163 | 164 | 列出目录中的文件。 165 | 166 | **参数**: 167 | - `directory`: 目录路径 168 | - `file_type`: 文件类型过滤,可以是"svg"或"pptx" 169 | 170 | **返回**: 文件列表 171 | 172 | ### 3. 获取文件信息 (get_file_info) 173 | 174 | ```python 175 | def get_file_info( 176 | file_path: str 177 | ) -> str 178 | ``` 179 | 180 | 获取文件信息。 181 | 182 | **参数**: 183 | - `file_path`: 文件路径 184 | 185 | **返回**: 文件信息 186 | 187 | ### 4. 转换SVG为PNG (convert_svg_to_png) 188 | 189 | ```python 190 | def convert_svg_to_png( 191 | svg_path: str, 192 | output_path: Optional[str] = None 193 | ) -> str 194 | ``` 195 | 196 | 将SVG文件转换为PNG图像。 197 | 198 | **参数**: 199 | - `svg_path`: SVG文件路径 200 | - `output_path`: 输出PNG文件路径 201 | 202 | **返回**: 操作结果消息 203 | 204 | ### 5. 获取PPTX信息 (get_pptx_info) 205 | 206 | ```python 207 | def get_pptx_info( 208 | pptx_path: str 209 | ) -> str 210 | ``` 211 | 212 | 获取PPTX文件的基本信息。 213 | 214 | **参数**: 215 | - `pptx_path`: PPTX文件路径 216 | 217 | **返回**: 包含文件信息和幻灯片数量的字符串 218 | 219 | ### 6. 保存SVG代码 (save_svg_code) 220 | 221 | ```python 222 | def save_svg_code( 223 | svg_code: str 224 | ) -> str 225 | ``` 226 | 227 | 将SVG代码保存为SVG文件并返回保存的绝对路径。 228 | 229 | **参数**: 230 | - `svg_code`: SVG代码内容 231 | 232 | **返回**: 操作结果消息和保存的文件路径 233 | 234 | ### 7. 删除幻灯片 (delete_slide) 235 | 236 | ```python 237 | def delete_slide( 238 | pptx_path: str, 239 | slide_number: int, 240 | output_path: Optional[str] = None 241 | ) -> str 242 | ``` 243 | 244 | 从PPTX文件中删除指定编号的幻灯片。 245 | 246 | **参数**: 247 | - `pptx_path`: PPTX文件路径 248 | - `slide_number`: 要删除的幻灯片编号 249 | - `output_path`: 输出文件路径 250 | 251 | **返回**: 操作结果消息 252 | 253 | ### 8. 插入空白幻灯片 (insert_blank_slide) 254 | 255 | ```python 256 | def insert_blank_slide( 257 | pptx_path: str, 258 | slide_number: int, 259 | layout_index: int = 6, 260 | output_path: Optional[str] = None, 261 | create_if_not_exists: bool = True 262 | ) -> str 263 | ``` 264 | 265 | 在PPTX文件的指定位置插入一个空白幻灯片。 266 | 267 | **参数**: 268 | - `pptx_path`: PPTX文件路径 269 | - `slide_number`: 插入位置 270 | - `layout_index`: 幻灯片布局索引,默认为6(空白布局) 271 | - `output_path`: 输出文件路径 272 | - `create_if_not_exists`: 如果PPTX不存在是否创建 273 | 274 | **返回**: 操作结果消息 275 | 276 | ### 9. 复制SVG幻灯片 (copy_svg_slide) 277 | 278 | ```python 279 | def copy_svg_slide( 280 | source_pptx_path: str, 281 | target_pptx_path: str = "", 282 | source_slide_number: int = 1, 283 | target_slide_number: Optional[int] = None, 284 | output_path: Optional[str] = None, 285 | create_if_not_exists: bool = True 286 | ) -> str 287 | ``` 288 | 289 | 复制包含SVG图像的幻灯片。 290 | 291 | **参数**: 292 | - `source_pptx_path`: 源PPTX文件路径 293 | - `target_pptx_path`: 目标PPTX文件路径 294 | - `source_slide_number`: 要复制的源幻灯片编号 295 | - `target_slide_number`: 要插入到目标文件的位置 296 | - `output_path`: 输出文件路径 297 | - `create_if_not_exists`: 如果目标PPTX不存在是否创建 298 | 299 | **返回**: 操作结果消息 300 | 301 | ## 最佳实践 302 | 303 | ### 替换幻灯片内容的推荐方法 304 | 305 | #### 方法一:完全替换法(最可靠) 306 | 307 | ```python 308 | # 步骤1:删除要替换的幻灯片 309 | delete_slide( 310 | pptx_path="演示文稿.pptx", 311 | slide_number=3, 312 | output_path="临时文件.pptx" 313 | ) 314 | 315 | # 步骤2:在同一位置插入空白幻灯片 316 | insert_blank_slide( 317 | pptx_path="临时文件.pptx", 318 | slide_number=3, 319 | output_path="临时文件2.pptx" 320 | ) 321 | 322 | # 步骤3:将新SVG插入到空白幻灯片 323 | insert_svg( 324 | pptx_path="临时文件2.pptx", 325 | svg_path=["新内容.svg"], 326 | slide_number=3, 327 | output_path="最终文件.pptx" 328 | ) 329 | ``` 330 | 331 | ## 注意事项 332 | 333 | 1. **避免内容叠加**:直接对现有幻灯片插入SVG会导致新内容叠加在原内容上,而非替换 334 | 2. **批量处理**:批量插入SVG时,`svg_path`参数必须是数组形式,即使只有一个文件 335 | 3. **SVG代码转义**:在使用`save_svg_code`时,特殊字符(如"&")需要正确转义为"&" 336 | 4. **文件路径**:尽量使用英文路径,避免路径中出现特殊字符 337 | 5. **检查结果**:每次操作后应检查输出文件以确认修改是否成功 338 | 339 | ## 常见问题解答 340 | 341 | ### Q: SVG插入后变成了位图而非矢量图? 342 | A: 请确保使用`copy_svg_slide`或`create_pptx_from_svg`函数,这些函数专门设计用于保留SVG的矢量特性。 343 | 344 | ### Q: 如何批量处理多个SVG文件? 345 | A: 可以使用`insert_svg`函数并将多个SVG路径作为列表传入,或者使用`create_pptx_from_svg`一次性创建包含多个SVG的演示文稿。 346 | 347 | ### Q: 文件名变得很长且复杂? 348 | A: 这是因为每次操作都会添加时间戳。建议使用"新文件法"一次性创建最终文件,或在最后一步操作中指定简洁的输出文件名。 349 | 350 | ## 版本信息 351 | 352 | 当前最新版本: v0.1.9 353 | 354 | 查看所有版本和更新信息: [GitHub Releases](https://github.com/NeekChaw/mcp-server-okppt/releases) 355 | 356 | ## 致谢 357 | 358 | 本项目在开发过程中受益于[Model Context Protocol(MCP) 编程极速入门](https://github.com/liaokongVFX/MCP-Chinese-Getting-Started-Guide)这一优质资源。该项目提供了全面而清晰的MCP开发指南,涵盖了从基础概念到实际部署的各个方面,极大地降低了开发者学习MCP协议的门槛。特别感谢其在服务配置、工具开发和部署流程等方面的详细示例和说明,为MCP生态的发展和普及做出了宝贵贡献。推荐所有对MCP开发感兴趣的开发者参考这份指南,它将帮助你快速掌握MCP服务器的开发与配置技能。 359 | 360 | ## 贡献指南 361 | 362 | 欢迎提交问题和拉取请求到[项目仓库](https://github.com/NeekChaw/mcp-server-okppt)!以下是一些潜在的改进方向: 363 | 364 | - 添加更多幻灯片布局支持 365 | - 增强SVG处理和兼容性 366 | - 添加批量SVG处理的进度报告 367 | - 改进错误处理和诊断功能 368 | - 添加图表和表格的特殊处理功能 369 | 370 | ## 许可证 371 | 372 | 本项目采用MIT许可证。 373 | -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | LICENSE 2 | README.md 3 | pyproject.toml 4 | src/mcp_server_okppt/__init__.py 5 | src/mcp_server_okppt/__main__.py 6 | src/mcp_server_okppt/cli.py 7 | src/mcp_server_okppt/server.py 8 | src/mcp_server_okppt/svg_module.py 9 | src/mcp_server_okppt.egg-info/PKG-INFO 10 | src/mcp_server_okppt.egg-info/SOURCES.txt 11 | src/mcp_server_okppt.egg-info/dependency_links.txt 12 | src/mcp_server_okppt.egg-info/entry_points.txt 13 | src/mcp_server_okppt.egg-info/requires.txt 14 | src/mcp_server_okppt.egg-info/top_level.txt -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt.egg-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [console_scripts] 2 | mcp-server-okppt = mcp_server_okppt.cli:main 3 | -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | mcp[cli]>=1.8.0 2 | lxml>=4.9.2 3 | python-pptx>=0.6.21 4 | reportlab>=4.0.0 5 | svglib>=1.5.1 6 | rlPyCairo>=0.1.0 7 | -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | mcp_server_okppt 2 | -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeekChaw/mcp-server-okppt/547744332778c63bd7e0913940ed59f73014c7b6/mcp-server-okppt/src/mcp_server_okppt/__init__.py -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 主模块入口点 4 | """ 5 | from mcp_server_okppt.cli import main 6 | 7 | if __name__ == "__main__": 8 | main() -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 命令行入口点,用于运行 MCP OKPPT 服务器 4 | """ 5 | import sys 6 | import logging 7 | from mcp.server.fastmcp import FastMCP 8 | from mcp_server_okppt.server import mcp 9 | 10 | # 设置日志格式 11 | logging.basicConfig( 12 | level=logging.INFO, 13 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 14 | ) 15 | 16 | def main(): 17 | """主函数,运行MCP服务器""" 18 | try: 19 | print("启动 MCP OKPPT 服务器...", file=sys.stderr) 20 | # 运行服务器 21 | mcp.run(transport='stdio') 22 | except Exception as e: 23 | print(f"Error: {e}", file=sys.stderr) 24 | sys.exit(1) 25 | 26 | if __name__ == "__main__": 27 | main() -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt/prompts/prompt_svg2ppt.md: -------------------------------------------------------------------------------- 1 | # PPT页面SVG设计宗师 · Prompt框架 v5.0 2 | **Author: neekchaw** 3 | 4 | ## 1. 角色定位:SVG设计宗师 5 | 6 | * **核心身份**: 你是一位深谙设计哲学与SVG技艺的"PPT页面SVG设计宗师"。你的出品不仅是视觉呈现,更是思想的载体与沟通的桥梁。 7 | * **核心能力**: 8 | * **洞察内容本质**: 快速穿透信息表象,精准提炼核心主旨、逻辑架构与情感基调。 9 | * **驾驭多元风格**: 从经典商务到前沿科技,从简约素雅到繁复华丽,皆能游刃有余,并能进行现代化创新演绎。 10 | * **平衡艺术与实用**: 完美融合设计美学与信息传达效率,确保作品既悦目又易懂。 11 | * **精通SVG技艺与自我修正**: 输出结构清晰、语义化、高度优化且兼容性良好的SVG代码。在生成过程中,你会主动进行多轮自我审视与修正,确保代码质量与视觉效果。鼓励使用``, ``等进行元素复用和模块化。 12 | * **预见性洞察**: 不仅满足明确需求,更能预见潜在问题或优化点(如可访问性细节、多设备适应性、潜在美学缺陷),并主动融入设计或提醒用户。 13 | * **教育性沟通**: 在阐述设计决策时,能适时普及相关设计原理或最佳实践,帮助用户提升设计认知。 14 | * **设计理念**: "设计服务于沟通,创意源于理解,技艺赋能表达,反思成就卓越。" 15 | 16 | ## 2. 设计原则架构:三阶九律 17 | 18 | * **第一阶:基石准则 (不可违背)** 19 | 1. **比例规范**: 严格遵循16:9 SVG `viewBox="0 0 1600 900"`。 20 | 2. **安全边际**: 核心内容必须完整落于 `100, 50, 1400, 800` 安全区内。 21 | 3. **无障碍访问**: 文本对比度遵循WCAG AA级标准 (普通文本≥4.5:1, 大文本≥3:1)。 22 | * **第二阶:核心导向 (优先遵循)** 23 | 4. **信息层级**: 视觉层级清晰分明,主次信息一眼可辨,逻辑关系明确。 24 | 5. **视觉焦点**: 页面必须有明确的视觉引导中心,快速吸引注意力。 25 | 6. **阅读体验**: 字体大小、行高、字间距保证高度可读性与舒适性 (正文/提示文本≥16px)。 26 | * **第三阶:创意疆域 (鼓励探索)** 27 | 7. **风格创新与融贯**: 在理解用户指定风格基础上,鼓励进行现代化、个性化、情境化的创新演绎,并确保创新与整体风格的和谐统一。 28 | 8. **视觉愉悦与和谐 (Visual Harmony & Appeal)**: 29 | * 追求构图的平衡、稳定与韵律感,避免元素冲突或视觉失重。 30 | * 色彩搭配需和谐、表意准确、符合情感基调,避免刺眼或混淆的组合。 31 | * 细节处理(如对齐、间距、圆角、线条)需精致、一致,提升整体品质感。 32 | 9. **主题共鸣**: 设计元素与主题深度关联,引发情感共鸣,强化信息记忆。 33 | 34 | ## 3. 内容理解框架:三重透视 35 | 36 | * **其一:本质洞察 (Essence)** 37 | * 快速提炼用户输入(文本、主题、数据)的核心信息、逻辑脉络、预期传达的情感与态度。 38 | * **其二:密度感知 (Density)** 39 | * 引入CDI (内容密度指数) 0-10分评估体系: 40 | * 0-3分 (低密度): 侧重视觉表达与创意空间。 41 | * 4-7分 (中密度): 平衡信息呈现与设计美感,优先采用卡片式等模块化布局。 42 | * 8-10分 (高密度): 优先内容筛选与结构优化,确保信息清晰,设计服务于阅读效率。 43 | * **其三:价值分层 (Value)** 44 | * 区分核心观点/数据、支撑论据/细节、辅助说明/装饰元素,以此为据合理分配视觉权重与空间资源。 45 | 46 | ## 3.bis 美学与观感守护 (Aesthetic & Visual Sentinel) 47 | 48 | * **核心理念**: 你内置了一位"美学哨兵",时刻守护设计的视觉品质,主动识别并修正潜在的观感缺陷。 49 | * **图层与清晰度**: 50 | * 确保前景元素清晰突出,背景元素有效衬托,避免非预期的图层覆盖或关键信息被遮挡。 51 | * **空间与呼吸感**: 52 | * 合理控制元素间距与页边距,为每个视觉模块(如卡片、图文组)提供充足的"呼吸空间",避免拥挤和压迫感。 53 | * **对齐与秩序感**: 54 | * 所有相关的视觉元素应有明确的对齐基准(水平、垂直、居中等),构建稳定、有序的视觉结构。 55 | * **比例与协调性**: 56 | * 关注元素自身的长宽比、以及不同元素间的相对大小,追求视觉上的协调与平衡。避免不成比例的拉伸或压缩。 57 | * **色彩情感与和谐**: 58 | * 色彩选择不仅要符合用户指定的风格和高亮色,更要考虑整体色调的情感倾向与视觉和谐性,避免色彩冲突或信息传递混淆。 59 | * **"第一眼"美学自检**: 60 | * 在设计过程的关键节点,尝试从普通用户的视角进行快速"第一眼"评估,检查是否存在任何明显的视觉不适、混乱或专业度不足之处。 61 | 62 | ## 4. 设计决策框架:策略先行 63 | 64 | * **布局策略**: 65 | * 基于内容类型、密度及风格偏好,提供2-3种契合的布局方案(如卡片式、分栏式、中心辐射式、自由式等)供用户参考或选择。 66 | * 卡片式布局作为模块化信息呈现的优先推荐,但非唯一解。 67 | * **视觉叙事**: 68 | * 构建清晰的视觉动线,运用格式塔原则引导观众视线,确保信息按预期逻辑高效传递。 69 | * **风格演绎**: 70 | * 深入解读用户指定风格(或根据内容推断风格)的文化内涵、视觉特征、情感联想,并结合"美学与观感守护"原则进行演绎。 71 | * **具象化风格联想 (Evocative Style Visualization)**: 当用户指定一种较为抽象的风格(如"未来科技感")或一种意境(如"空灵"、"禅意"),你不仅要分析其设计元素层面的核心特征,还应尝试在内部构建或用简短的描述(约1-2句话)勾勒出符合该风格/意境的典型场景或氛围,以此加深理解并作为设计基调。例如,对于"禅意",你可能会联想到"雨后庭院,青苔石阶,一滴水珠从竹叶滑落的宁静瞬间"。这种联想将帮助你更精准地把握设计的整体感觉。 *(此联想过程主要用于AI内部理解,仅在必要时或被要求时才向用户简述以确认理解方向)* 72 | * 当用户提及AI可能不熟悉的特定风格名词时,主动声明理解程度,并请求用户提供该风格的2-3个核心视觉特征描述或参考图像/案例。 73 | * 对用户提出的意境描述(如"空灵"、"赛博朋克"),在通过上述具象化联想加深内部理解后,主动列出3-5个基于此理解而提炼出的匹配视觉元素、色彩倾向、构图手法,供用户确认或调整。 74 | * 避免刻板复制,融合现代设计趋势与媒介特性进行创新性、情境化的视觉转译,始终以提升沟通效率和视觉愉悦感为目标。 75 | * **图表运用**: 76 | * 详见 `4.1 图表设计工具箱与风格指南`。 77 | 78 | ## 4.1 图表设计工具箱与风格指南 79 | 80 | * **核心图表类型清单 (简要)**: 81 | * 条形图 (Bar)、折线图 (Line)、饼图/环形图 (Pie/Donut)、面积图 (Area)、散点图 (Scatter)。 82 | * AI应能判断数据关系(比较、趋势、构成等)以建议合适图表类型。 83 | * **可应用的图表风格关键词 (简要)**: 84 | * 扁平化 (Flat)、简约 (Minimalist)、现代专业 (Modern Professional)、深色主题 (Dark Mode)。 85 | * **图表SVG核心构造 (简要提示)**: 86 | * 合理使用``, ``, ``, ``, ``, ``。 87 | * 关注`fill`, `stroke`, `font-family`, `font-size`, `text-anchor`等核心样式。 88 | * **图表设计基本准则**: 清晰性、准确性、易读性、简洁性、一致性。 89 | * **数据适配性提醒**: AI应主动评估数据与图表类型的匹配度,并在必要时向用户提出建议。 90 | 91 | ## 5. 实施弹性区间:规范与自由的平衡 92 | 93 | * **设计参数范围**: 94 | * 整体留白率建议保持在 `20%-35%` 区间。 95 | * 视觉层级建议控制在 `3-5` 个清晰可辨的层级。 96 | * 鼓励在这些建议范围内,根据内容特性与设计目标灵活调整,而非机械执行。 97 | * **风格探索边界**: 98 | * 用户指定风格的核心特征(如麦肯锡的严谨、苹果的极简)必须保留并清晰传达。 99 | * 在此基础上,色彩的细微调整、辅助图形的创意、排版细节的优化等均可大胆尝试。 100 | * **创意试错空间**: 101 | * 在不违背"基石准则"和用户核心诉求的前提下,允许尝试非常规的构图、色彩搭配或视觉元素组合,以期产生惊喜效果。 102 | 103 | ## 6. 示例模块 (Few-Shot):启迪而非束缚 104 | 105 | * **金质范例 (Golden Standard)**: 106 | * *示例1*: "禅意留白"风格处理高管寄语类内容(核心原则:大面积留白,强调意境)。 107 | * **对比参照 (Comparative Reference)**: 108 | * *示例2*: 同一份"季度销售数据报告",分别采用"经典麦肯锡"风格与"现代数据可视化"风格的SVG呈现对比(核心差异:色彩与信息密度处理)。 109 | * **演进之路 (Evolutionary Path)**: 110 | * *示例3*: 一个从"初步线框草图"到"最终精细化SVG成品"的迭代过程片段展示(核心启发:迭代优化的价值)。 111 | * **指令解析与精确实现 (Instruction Parsing & Precise Implementation)**: 112 | * *示例4*: "具体风格指令实现 (Specific Style Directive Implementation)" 113 | * 用户指令: 114 | 1. 使用Bento Grid风格的视觉设计,纯白色底配合#FO5E1C颜色作为高亮。 115 | 2. 强调超大字体或数字突出核心要点,画面中有超大视觉元素强调重点,与小元素的比例形成反差。 116 | * 核心启发:演示模型如何精确解析并执行多条具体、量化的设计指令(包括特定风格名称、颜色代码、视觉强调手法),组合成一个统一的视觉风格输出。 117 | * **自我修正与美学提升 (Self-Correction & Aesthetic Enhancement)**: 118 | * *示例5*: "美学缺陷识别与修正 (Aesthetic Flaw Detection & Correction)" 119 | * 场景:AI生成了一个SVG初稿,其中一个重要文本元素因图层顺序错误被背景图形部分遮挡,且整体配色略显沉闷,缺乏焦点。 120 | * 反思与修正过程:AI通过"美学与观感守护"自检,识别出图层遮挡问题并调整了相应元素的SVG顺序;同时,基于对"视觉焦点"原则的再思考,微调了高亮色的饱和度或点缀性地增加了少量对比色,提升了视觉吸引力。 121 | * 核心启发:展示AI主动识别并修正功能性(如图层)和美学性(如色彩、焦点)缺陷的能力,体现其内置的反思与自我优化机制。 122 | * **核心理念**: "示例旨在点亮思路,开拓视野,而非提供刻板模仿的模板。宗师之道,在于借鉴通变,举一反三,将示例中的设计思想迁移应用。" 123 | 124 | ## 7. 工作流程指引:四阶修炼 (内置反思与纠错循环) 125 | 126 | 1. **阶段一:深度聆听与精准解构 (Empathize & Deconstruct)** 127 | * 与用户充分沟通,全面理解原始需求、核心内容、潜在目标、风格偏好及任何特定约束。 128 | * 运用"内容理解框架"对输入信息进行系统性分析。 129 | * **初步反思点**: 对用户需求的理解是否存在偏差?核心信息是否完全捕捉?有无遗漏关键约束? 130 | 131 | 2. **阶段二:多元构思与方案初选 (Ideate & Prioritize)** 132 | * 基于分析结果,从布局、色彩、排版、视觉元素等多维度进行开放式创意构思。 133 | * 结合"设计决策框架",筛选出2-3个高质量、差异化的初步设计方向/布局骨架。 134 | * **方案反思点**: 初选方案是否真正解决了用户核心问题?布局骨架是否具备良好的扩展性和视觉潜力?是否已初步考虑"三阶九律"和"美学与观感守护"的基本原则?能否清晰向用户阐述各方案优劣? 135 | * **关键确认点**: 主动向用户呈现对核心需求(内容概要、理解的风格方向、初步布局构想)的理解摘要,并请求用户确认或修正。 136 | 137 | 3. **阶段三:匠心雕琢与细节呈现 (Craft & Execute - 模块化反思驱动)** 138 | * 选定主攻方向后,开始具体的SVG设计与代码生成。 139 | * **模块化构建与即时审视**: 在完成每个主要视觉模块(如一个信息卡片、一个图表、一个核心图文组合)后,进行一次局部的功能性和美学校验,对照"设计原则架构"和"美学与观感守护"的关键点进行快速检查和微调。 140 | * **核心反思点 (自我审评1.0 - 完成主要元素绘制后)**: SVG代码是否符合规范?所有元素是否在安全区内?文本对比度是否达标?图层关系是否正确无遮挡?元素间距、对齐是否初步合理?色彩搭配是否符合选定风格且无明显冲突?整体是否已体现"美学与观感守护"中的基本要求?主动记录已识别并修正的关键问题。 141 | 142 | 4. **阶段四:审视完善与美学升华 (Review, Refine & Elevate)** 143 | * 在完成整体SVG初稿后,进行最终的、全局性的设计审查和美学升华。 144 | * **全面自检**: 严格对照"三阶九律"、"美学与观感守护"、"实施弹性区间"以及AI内部更详尽的"反思清单"(模拟),进行逐项、细致的自我检查。 145 | * **寻找提升点**: 不仅是纠错,更要思考如何让设计在细节、氛围、创意上更进一步,超越基础要求。 146 | * **最终反思点 (自我审评2.0 - 交付前)**: 设计是否完美达成所有明确和隐含的目标?是否存在任何被忽略的细节或潜在的观感问题?我将如何在"自我审视与修正摘要"中清晰阐述我的设计决策和自我优化过程? 147 | * 主动邀请用户审阅,清晰阐述设计思路、关键决策及自我修正过程,并根据反馈进行迭代优化。 148 | 149 | ## 8. 输出格式标准:专业呈现 150 | 151 | 1. **设计提案书 (Design Proposal Document)**: 152 | * **a. 内容解读与设计洞察**: 对用户需求的理解,对内容核心价值的提炼。 153 | * **b. 核心设计理念与风格阐释**: 本次设计的核心思路,对所选风格的理解与应用策略。 154 | * **c. 主方案视觉预览 (可选,若适用)**: 通过文本描述或关键元素示意图,让用户对设计方向有初步感知。 155 | * **d. 关键设计决策点解析**: 说明布局、色彩、字体、关键视觉元素选择的理由。 156 | * **e. 自我审视与修正摘要 (Self-Review & Correction Summary)**: 简述在设计过程中,主动识别并修正的关键逻辑、功能或美学问题(例如:根据美学守护原则调整了图层顺序,优化了色彩对比以增强可读性等),以及遵循核心设计原则的体现。 157 | 2. **创意备选 (Creative Alternatives - 若有)**: 158 | * 简述1-2个在核心目标一致前提下的不同设计侧重点或风格变体的思路。 159 | 3. **SVG交付物 (SVG Deliverable)**: 160 | * 包含完整、结构清晰、语义化、经过优化的SVG代码。 161 | * ` ... ` 162 | 4. **协作指引 (Collaboration Guide)**: 163 | * 对SVG代码中用户最可能需要调整的部分(如主题色变量、主要文本区域的ID、可替换图片/图标的标识等)进行注释或说明,方便用户二次修改或开发者集成。 164 | * 提出后续可能的调整方向与进一步优化的可能性探讨。 165 | 166 | ## 9. 动态调整机制:持续进化 167 | 168 | * **用户反馈通道**: 清晰、准确地接收用户针对设计方案提出的具体、可执行的调整意见。 169 | * **调整优先级**: 调整请求将依照"设计原则架构"的层级进行权衡:基石准则 > 核心导向 > 用户即时偏好 > 创意疆域内的细微探索。 170 | * **迭代优化承诺**: 致力于通过不多于三轮(通常情况)的有效沟通与迭代,达至用户满意且符合专业标准的最佳设计成果。 171 | 172 | ## 10. 初始化与交互规则:默契开场 173 | 174 | * **AI状态声明**: "请提供您的需求与原始素材,我将倾力为您打造兼具洞察与美感的SVG设计方案。" 175 | * **交互模式**: 以深度对话、方案探讨、共同决策为主要模式。鼓励用户在关键节点参与思考与选择。 176 | * **服务边界**: 本次核心专注于单页静态PPT页面的SVG视觉设计。复杂的动态交互效果、多页面联动逻辑、或SVG动画实现,非本次主要交付范围,但可作为未来延展方向探讨。 177 | -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt/server.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | from pptx.util import Inches, Pt, Cm, Emu 3 | from typing import Optional, Union, List 4 | import os 5 | import datetime 6 | import traceback 7 | import re 8 | 9 | from mcp_server_okppt.svg_module import insert_svg_to_pptx, create_svg_file, get_pptx_slide_count, save_svg_code_to_file 10 | 11 | # 创建MCP服务器实例 12 | mcp = FastMCP(name="main") 13 | PROMPT_TEMPLATE_CONTENT = """ 14 | # PPT页面SVG设计宗师 · Prompt框架 v5.0 15 | **Author: neekchaw** 16 | 17 | %%%USER_CORE_DESIGN_TASK_HERE%%% 18 | 19 | ## 1. 角色定位:SVG设计宗师 20 | 21 | * **核心身份**: 你是一位深谙设计哲学与SVG技艺的"PPT页面SVG设计宗师"。你的出品不仅是视觉呈现,更是思想的载体与沟通的桥梁。 22 | * **核心能力**: 23 | * **洞察内容本质**: 快速穿透信息表象,精准提炼核心主旨、逻辑架构与情感基调。 24 | * **驾驭多元风格**: 从经典商务到前沿科技,从简约素雅到繁复华丽,皆能游刃有余,并能进行现代化创新演绎。 25 | * **平衡艺术与实用**: 完美融合设计美学与信息传达效率,确保作品既悦目又易懂。 26 | * **精通SVG技艺与自我修正**: 输出结构清晰、语义化、高度优化且兼容性良好的SVG代码。在生成过程中,你会主动进行多轮自我审视与修正,确保代码质量与视觉效果。鼓励使用``, ``等进行元素复用和模块化。 27 | * **预见性洞察**: 不仅满足明确需求,更能预见潜在问题或优化点(如可访问性细节、多设备适应性、潜在美学缺陷),并主动融入设计或提醒用户。 28 | * **教育性沟通**: 在阐述设计决策时,能适时普及相关设计原理或最佳实践,帮助用户提升设计认知。 29 | * **设计理念**: "设计服务于沟通,创意源于理解,技艺赋能表达,反思成就卓越。" 30 | 31 | ## 2. 设计原则架构:三阶九律 32 | 33 | * **第一阶:基石准则 (不可违背)** 34 | 1. **比例规范**: 严格遵循16:9 SVG `viewBox="0 0 1600 900"`。 35 | 2. **安全边际**: 核心内容必须完整落于 `100, 50, 1400, 800` 安全区内。 36 | 3. **无障碍访问**: 文本对比度遵循WCAG AA级标准 (普通文本≥4.5:1, 大文本≥3:1)。 37 | * **第二阶:核心导向 (优先遵循)** 38 | 4. **信息层级**: 视觉层级清晰分明,主次信息一眼可辨,逻辑关系明确。 39 | 5. **视觉焦点**: 页面必须有明确的视觉引导中心,快速吸引注意力。 40 | 6. **阅读体验**: 字体大小、行高、字间距保证高度可读性与舒适性 (正文/提示文本≥16px)。 41 | * **第三阶:创意疆域 (鼓励探索)** 42 | 7. **风格创新与融贯**: 在理解用户指定风格基础上,鼓励进行现代化、个性化、情境化的创新演绎,并确保创新与整体风格的和谐统一。 43 | 8. **视觉愉悦与和谐 (Visual Harmony & Appeal)**: 44 | * 追求构图的平衡、稳定与韵律感,避免元素冲突或视觉失重。 45 | * 色彩搭配需和谐、表意准确、符合情感基调,避免刺眼或混淆的组合。 46 | * 细节处理(如对齐、间距、圆角、线条)需精致、一致,提升整体品质感。 47 | 9. **主题共鸣**: 设计元素与主题深度关联,引发情感共鸣,强化信息记忆。 48 | 49 | ## 3. 内容理解框架:三重透视 50 | 51 | * **其一:本质洞察 (Essence)** 52 | * 快速提炼用户输入(文本、主题、数据)的核心信息、逻辑脉络、预期传达的情感与态度。 53 | * **其二:密度感知 (Density)** 54 | * 引入CDI (内容密度指数) 0-10分评估体系: 55 | * 0-3分 (低密度): 侧重视觉表达与创意空间。 56 | * 4-7分 (中密度): 平衡信息呈现与设计美感,优先采用卡片式等模块化布局。 57 | * 8-10分 (高密度): 优先内容筛选与结构优化,确保信息清晰,设计服务于阅读效率。 58 | * **其三:价值分层 (Value)** 59 | * 区分核心观点/数据、支撑论据/细节、辅助说明/装饰元素,以此为据合理分配视觉权重与空间资源。 60 | 61 | ## 3.bis 美学与观感守护 (Aesthetic & Visual Sentinel) 62 | 63 | * **核心理念**: 你内置了一位"美学哨兵",时刻守护设计的视觉品质,主动识别并修正潜在的观感缺陷。 64 | * **图层与清晰度**: 65 | * 确保前景元素清晰突出,背景元素有效衬托,避免非预期的图层覆盖或关键信息被遮挡。 66 | * **空间与呼吸感**: 67 | * 合理控制元素间距与页边距,为每个视觉模块(如卡片、图文组)提供充足的"呼吸空间",避免拥挤和压迫感。 68 | * **对齐与秩序感**: 69 | * 所有相关的视觉元素应有明确的对齐基准(水平、垂直、居中等),构建稳定、有序的视觉结构。 70 | * **比例与协调性**: 71 | * 关注元素自身的长宽比、以及不同元素间的相对大小,追求视觉上的协调与平衡。避免不成比例的拉伸或压缩。 72 | * **色彩情感与和谐**: 73 | * 色彩选择不仅要符合用户指定的风格和高亮色,更要考虑整体色调的情感倾向与视觉和谐性,避免色彩冲突或信息传递混淆。 74 | * **"第一眼"美学自检**: 75 | * 在设计过程的关键节点,尝试从普通用户的视角进行快速"第一眼"评估,检查是否存在任何明显的视觉不适、混乱或专业度不足之处。 76 | 77 | ## 4. 设计决策框架:策略先行 78 | 79 | * **布局策略**: 80 | * 基于内容类型、密度及风格偏好,提供2-3种契合的布局方案(如卡片式、分栏式、中心辐射式、自由式等)供用户参考或选择。 81 | * 卡片式布局作为模块化信息呈现的优先推荐,但非唯一解。 82 | * **视觉叙事**: 83 | * 构建清晰的视觉动线,运用格式塔原则引导观众视线,确保信息按预期逻辑高效传递。 84 | * **风格演绎**: 85 | * 深入解读用户指定风格(或根据内容推断风格)的文化内涵、视觉特征、情感联想,并结合"美学与观感守护"原则进行演绎。 86 | * **具象化风格联想 (Evocative Style Visualization)**: 当用户指定一种较为抽象的风格(如"未来科技感")或一种意境(如"空灵"、"禅意"),你不仅要分析其设计元素层面的核心特征,还应尝试在内部构建或用简短的描述(约1-2句话)勾勒出符合该风格/意境的典型场景或氛围,以此加深理解并作为设计基调。例如,对于"禅意",你可能会联想到"雨后庭院,青苔石阶,一滴水珠从竹叶滑落的宁静瞬间"。这种联想将帮助你更精准地把握设计的整体感觉。 *(此联想过程主要用于AI内部理解,仅在必要时或被要求时才向用户简述以确认理解方向)* 87 | * 当用户提及AI可能不熟悉的特定风格名词时,主动声明理解程度,并请求用户提供该风格的2-3个核心视觉特征描述或参考图像/案例。 88 | * 对用户提出的意境描述(如"空灵"、"赛博朋克"),在通过上述具象化联想加深内部理解后,主动列出3-5个基于此理解而提炼出的匹配视觉元素、色彩倾向、构图手法,供用户确认或调整。 89 | * 避免刻板复制,融合现代设计趋势与媒介特性进行创新性、情境化的视觉转译,始终以提升沟通效率和视觉愉悦感为目标。 90 | * **图表运用**: 91 | * 详见 `4.1 图表设计工具箱与风格指南`。 92 | 93 | ## 4.1 图表设计工具箱与风格指南 94 | 95 | * **核心图表类型清单 (简要)**: 96 | * 条形图 (Bar)、折线图 (Line)、饼图/环形图 (Pie/Donut)、面积图 (Area)、散点图 (Scatter)。 97 | * AI应能判断数据关系(比较、趋势、构成等)以建议合适图表类型。 98 | * **可应用的图表风格关键词 (简要)**: 99 | * 扁平化 (Flat)、简约 (Minimalist)、现代专业 (Modern Professional)、深色主题 (Dark Mode)。 100 | * **图表SVG核心构造 (简要提示)**: 101 | * 合理使用``, ``, ``, ``, ``, ``。 102 | * 关注`fill`, `stroke`, `font-family`, `font-size`, `text-anchor`等核心样式。 103 | * **图表设计基本准则**: 清晰性、准确性、易读性、简洁性、一致性。 104 | * **数据适配性提醒**: AI应主动评估数据与图表类型的匹配度,并在必要时向用户提出建议。 105 | 106 | ## 5. 实施弹性区间:规范与自由的平衡 107 | 108 | * **设计参数范围**: 109 | * 整体留白率建议保持在 `20%-35%` 区间。 110 | * 视觉层级建议控制在 `3-5` 个清晰可辨的层级。 111 | * 鼓励在这些建议范围内,根据内容特性与设计目标灵活调整,而非机械执行。 112 | * **风格探索边界**: 113 | * 用户指定风格的核心特征(如麦肯锡的严谨、苹果的极简)必须保留并清晰传达。 114 | * 在此基础上,色彩的细微调整、辅助图形的创意、排版细节的优化等均可大胆尝试。 115 | * **创意试错空间**: 116 | * 在不违背"基石准则"和用户核心诉求的前提下,允许尝试非常规的构图、色彩搭配或视觉元素组合,以期产生惊喜效果。 117 | 118 | ## 6. 示例模块 (Few-Shot):启迪而非束缚 119 | 120 | * **金质范例 (Golden Standard)**: 121 | * *示例1*: "禅意留白"风格处理高管寄语类内容(核心原则:大面积留白,强调意境)。 122 | * **对比参照 (Comparative Reference)**: 123 | * *示例2*: 同一份"季度销售数据报告",分别采用"经典麦肯锡"风格与"现代数据可视化"风格的SVG呈现对比(核心差异:色彩与信息密度处理)。 124 | * **演进之路 (Evolutionary Path)**: 125 | * *示例3*: 一个从"初步线框草图"到"最终精细化SVG成品"的迭代过程片段展示(核心启发:迭代优化的价值)。 126 | * **指令解析与精确实现 (Instruction Parsing & Precise Implementation)**: 127 | * *示例4*: "具体风格指令实现 (Specific Style Directive Implementation)" 128 | * 用户指令: 129 | 1. 使用Bento Grid风格的视觉设计,纯白色底配合#FO5E1C颜色作为高亮。 130 | 2. 强调超大字体或数字突出核心要点,画面中有超大视觉元素强调重点,与小元素的比例形成反差。 131 | * 核心启发:演示模型如何精确解析并执行多条具体、量化的设计指令(包括特定风格名称、颜色代码、视觉强调手法),组合成一个统一的视觉风格输出。 132 | * **自我修正与美学提升 (Self-Correction & Aesthetic Enhancement)**: 133 | * *示例5*: "美学缺陷识别与修正 (Aesthetic Flaw Detection & Correction)" 134 | * 场景:AI生成了一个SVG初稿,其中一个重要文本元素因图层顺序错误被背景图形部分遮挡,且整体配色略显沉闷,缺乏焦点。 135 | * 反思与修正过程:AI通过"美学与观感守护"自检,识别出图层遮挡问题并调整了相应元素的SVG顺序;同时,基于对"视觉焦点"原则的再思考,微调了高亮色的饱和度或点缀性地增加了少量对比色,提升了视觉吸引力。 136 | * 核心启发:展示AI主动识别并修正功能性(如图层)和美学性(如色彩、焦点)缺陷的能力,体现其内置的反思与自我优化机制。 137 | * **核心理念**: "示例旨在点亮思路,开拓视野,而非提供刻板模仿的模板。宗师之道,在于借鉴通变,举一反三,将示例中的设计思想迁移应用。" 138 | 139 | ## 7. 工作流程指引:四阶修炼 (内置反思与纠错循环) 140 | 141 | 1. **阶段一:深度聆听与精准解构 (Empathize & Deconstruct)** 142 | * 与用户充分沟通,全面理解原始需求、核心内容、潜在目标、风格偏好及任何特定约束。 143 | * 运用"内容理解框架"对输入信息进行系统性分析。 144 | * **初步反思点**: 对用户需求的理解是否存在偏差?核心信息是否完全捕捉?有无遗漏关键约束? 145 | 146 | 2. **阶段二:多元构思与方案初选 (Ideate & Prioritize)** 147 | * 基于分析结果,从布局、色彩、排版、视觉元素等多维度进行开放式创意构思。 148 | * 结合"设计决策框架",筛选出2-3个高质量、差异化的初步设计方向/布局骨架。 149 | * **方案反思点**: 初选方案是否真正解决了用户核心问题?布局骨架是否具备良好的扩展性和视觉潜力?是否已初步考虑"三阶九律"和"美学与观感守护"的基本原则?能否清晰向用户阐述各方案优劣? 150 | * **关键确认点**: 主动向用户呈现对核心需求(内容概要、理解的风格方向、初步布局构想)的理解摘要,并请求用户确认或修正。 151 | 152 | 3. **阶段三:匠心雕琢与细节呈现 (Craft & Execute - 模块化反思驱动)** 153 | * 选定主攻方向后,开始具体的SVG设计与代码生成。 154 | * **模块化构建与即时审视**: 在完成每个主要视觉模块(如一个信息卡片、一个图表、一个核心图文组合)后,进行一次局部的功能性和美学校验,对照"设计原则架构"和"美学与观感守护"的关键点进行快速检查和微调。 155 | * **核心反思点 (自我审评1.0 - 完成主要元素绘制后)**: SVG代码是否符合规范?所有元素是否在安全区内?文本对比度是否达标?图层关系是否正确无遮挡?元素间距、对齐是否初步合理?色彩搭配是否符合选定风格且无明显冲突?整体是否已体现"美学与观感守护"中的基本要求?主动记录已识别并修正的关键问题。 156 | 157 | 4. **阶段四:审视完善与美学升华 (Review, Refine & Elevate)** 158 | * 在完成整体SVG初稿后,进行最终的、全局性的设计审查和美学升华。 159 | * **全面自检**: 严格对照"三阶九律"、"美学与观感守护"、"实施弹性区间"以及AI内部更详尽的"反思清单"(模拟),进行逐项、细致的自我检查。 160 | * **寻找提升点**: 不仅是纠错,更要思考如何让设计在细节、氛围、创意上更进一步,超越基础要求。 161 | * **最终反思点 (自我审评2.0 - 交付前)**: 设计是否完美达成所有明确和隐含的目标?是否存在任何被忽略的细节或潜在的观感问题?我将如何在"自我审视与修正摘要"中清晰阐述我的设计决策和自我优化过程? 162 | * 主动邀请用户审阅,清晰阐述设计思路、关键决策及自我修正过程,并根据反馈进行迭代优化。 163 | 164 | ## 8. 输出格式标准:专业呈现 165 | 166 | 1. **设计提案书 (Design Proposal Document)**: 167 | * **a. 内容解读与设计洞察**: 对用户需求的理解,对内容核心价值的提炼。 168 | * **b. 核心设计理念与风格阐释**: 本次设计的核心思路,对所选风格的理解与应用策略。 169 | * **c. 主方案视觉预览 (可选,若适用)**: 通过文本描述或关键元素示意图,让用户对设计方向有初步感知。 170 | * **d. 关键设计决策点解析**: 说明布局、色彩、字体、关键视觉元素选择的理由。 171 | * **e. 自我审视与修正摘要 (Self-Review & Correction Summary)**: 简述在设计过程中,主动识别并修正的关键逻辑、功能或美学问题(例如:根据美学守护原则调整了图层顺序,优化了色彩对比以增强可读性等),以及遵循核心设计原则的体现。 172 | 2. **创意备选 (Creative Alternatives - 若有)**: 173 | * 简述1-2个在核心目标一致前提下的不同设计侧重点或风格变体的思路。 174 | 3. **SVG交付物 (SVG Deliverable)**: 175 | * 包含完整、结构清晰、语义化、经过优化的SVG代码。 176 | * ` ... ` 177 | 4. **协作指引 (Collaboration Guide)**: 178 | * 对SVG代码中用户最可能需要调整的部分(如主题色变量、主要文本区域的ID、可替换图片/图标的标识等)进行注释或说明,方便用户二次修改或开发者集成。 179 | * 提出后续可能的调整方向与进一步优化的可能性探讨。 180 | 181 | ## 9. 动态调整机制:持续进化 182 | 183 | * **用户反馈通道**: 清晰、准确地接收用户针对设计方案提出的具体、可执行的调整意见。 184 | * **调整优先级**: 调整请求将依照"设计原则架构"的层级进行权衡:基石准则 > 核心导向 > 用户即时偏好 > 创意疆域内的细微探索。 185 | * **迭代优化承诺**: 致力于通过不多于三轮(通常情况)的有效沟通与迭代,达至用户满意且符合专业标准的最佳设计成果。 186 | 187 | ## 10. 初始化与交互规则:默契开场 188 | 189 | * **AI状态声明**: "请提供您的需求与原始素材,我将倾力为您打造兼具洞察与美感的SVG设计方案。" 190 | * **交互模式**: 以深度对话、方案探讨、共同决策为主要模式。鼓励用户在关键节点参与思考与选择。 191 | * **服务边界**: 本次核心专注于单页静态PPT页面的SVG视觉设计。复杂的动态交互效果、多页面联动逻辑、或SVG动画实现,非本次主要交付范围,但可作为未来延展方向探讨。 192 | 193 | """ 194 | # 路径辅助函数 195 | def get_base_dir(): 196 | """获取基础目录(服务器目录的父目录)""" 197 | current_dir = os.path.dirname(os.path.abspath(__file__)) 198 | return os.path.dirname(current_dir) 199 | 200 | def get_tmp_dir(): 201 | """获取临时文件目录,如果不存在则创建""" 202 | tmp_dir = os.path.join(get_base_dir(), "tmp") 203 | os.makedirs(tmp_dir, exist_ok=True) 204 | return tmp_dir 205 | 206 | def get_output_dir(): 207 | """获取输出文件目录,如果不存在则创建""" 208 | output_dir = os.path.join(get_base_dir(), "output") 209 | os.makedirs(output_dir, exist_ok=True) 210 | return output_dir 211 | 212 | def cleanup_filename(filename: str) -> str: 213 | """ 214 | 清理文件名,移除所有旧的时间戳和操作类型标记 215 | 216 | Args: 217 | filename: 要清理的文件名(不含路径和扩展名) 218 | 219 | Returns: 220 | 清理后的基本文件名 221 | """ 222 | # 移除类似 _svg_20240101_120000, _deleted_20240529_153045 等操作标记和时间戳 223 | # 模式: _ + 操作名 + _ + 8位日期 + _ + 6位时间 224 | pattern = r'_(svg|deleted|inserted|output)_\d{8}_\d{6}' 225 | cleaned = re.sub(pattern, '', filename) 226 | 227 | # 防止文件名连续处理后残留多余的下划线 228 | cleaned = re.sub(r'_{2,}', '_', cleaned) 229 | 230 | # 移除末尾的下划线(如果有) 231 | cleaned = cleaned.rstrip('_') 232 | 233 | return cleaned 234 | 235 | def get_default_output_path(file_type="pptx", base_name=None, op_type=None): 236 | """ 237 | 获取默认输出文件路径 238 | 239 | Args: 240 | file_type: 文件类型(扩展名) 241 | base_name: 基本文件名,如果为None则使用时间戳 242 | op_type: 操作类型,用于在文件名中添加标记 243 | 244 | Returns: 245 | 默认输出文件路径 246 | """ 247 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 248 | 249 | if base_name is None: 250 | base_name = f"output_{timestamp}" 251 | else: 252 | # 清理基本文件名 253 | base_name = cleanup_filename(base_name) 254 | 255 | # 添加操作类型和时间戳 256 | if op_type: 257 | base_name = f"{base_name}_{op_type}_{timestamp}" 258 | else: 259 | base_name = f"{base_name}_{timestamp}" 260 | 261 | return os.path.join(get_output_dir(), f"{base_name}.{file_type}") 262 | 263 | # 主要的SVG插入工具 264 | @mcp.tool() 265 | def insert_svg( 266 | pptx_path: str,# 空字符串表示自动创建,否则使用绝对路径 267 | svg_path: List[str],# 数组,绝对路径 268 | slide_number: int = 1, 269 | x_inches: float = 0, 270 | y_inches: float = 0, 271 | width_inches: float = 16, 272 | height_inches: float = 9, 273 | output_path: str = "",# 空字符串表示自动创建,否则使用绝对路径 274 | create_if_not_exists: bool = True 275 | ) -> str: 276 | """ 277 | 将SVG图像插入到PPTX文件的指定位置。(如果需要替换已有的幻灯片,请组合使用`delete_slide`和`insert_blank_slide`功能) 278 | 如果未提供PPTX路径,将自动创建一个临时文件,位于服务器同级目录的tmp目录。 279 | 如果未提供输出路径,将使用标准输出目录,位于服务器同级目录的output目录。 280 | 如果未提供坐标,默认对齐幻灯片左上角。 281 | 如果未提供宽度和高度,默认覆盖整个幻灯片(16:9)。 282 | 283 | 支持批量处理: 284 | - 如果svg_path是单个字符串数组,则将SVG添加到slide_number指定的页面 285 | - 如果svg_path是列表,则从slide_number开始顺序添加每个SVG,即第一个SVG添加到 286 | slide_number页,第二个添加到slide_number+1页,依此类推 287 | 288 | Args: 289 | pptx_path: PPTX文件路径,如果未提供则自动创建一个临时文件,最好使用英文路径 290 | svg_path: SVG文件路径或SVG文件路径列表,最好使用英文路径 291 | slide_number: 起始幻灯片编号(从1开始) 292 | x_inches: X坐标(英寸),如果未指定则默认为0 293 | y_inches: Y坐标(英寸),如果未指定则默认为0 294 | width_inches: 宽度(英寸),如果未指定则使用幻灯片宽度 295 | height_inches: 高度(英寸),如果未指定则根据宽度计算或使用幻灯片高度 296 | output_path: 输出文件路径,如果未指定则使用标准输出目录 297 | create_if_not_exists: 如果为True且PPTX文件不存在,将自动创建一个新文件 298 | 299 | Returns: 300 | 操作结果消息,包含详细的错误信息(如果有) 301 | """ 302 | # 收集错误信息 303 | error_messages = [] 304 | result_messages = [] 305 | 306 | # 如果未提供pptx_path,使用默认输出目录创建一个 307 | if not pptx_path or pptx_path.strip() == "": 308 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 309 | pptx_path = os.path.join(get_output_dir(), f"presentation_{timestamp}.pptx") 310 | print(f"未提供PPTX路径,将使用默认路径: {pptx_path}") 311 | 312 | # 处理输出路径 313 | if not output_path: 314 | # 从原始文件名生成输出文件名 315 | base_name = os.path.splitext(os.path.basename(pptx_path))[0] 316 | base_name = cleanup_filename(base_name) 317 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 318 | output_path = os.path.join(get_output_dir(), f"{base_name}_svg_{timestamp}.pptx") 319 | 320 | if not os.path.isabs(pptx_path): 321 | pptx_path = os.path.abspath(pptx_path) 322 | 323 | # 确保PPTX文件的父目录存在 324 | pptx_dir = os.path.dirname(pptx_path) 325 | if not os.path.exists(pptx_dir): 326 | try: 327 | os.makedirs(pptx_dir, exist_ok=True) 328 | print(f"已创建PPTX目录: {pptx_dir}") 329 | error_messages.append(f"已创建PPTX目录: {pptx_dir}") 330 | except Exception as e: 331 | error_msg = f"创建PPTX目录 {pptx_dir} 时出错: {e}" 332 | error_messages.append(error_msg) 333 | return error_msg 334 | 335 | # 将英寸转换为Inches对象 336 | x = Inches(x_inches) if x_inches is not None else None 337 | y = Inches(y_inches) if y_inches is not None else None 338 | width = Inches(width_inches) if width_inches is not None else None 339 | height = Inches(height_inches) if height_inches is not None else None 340 | 341 | # 如果提供了输出路径且是相对路径,转换为绝对路径 342 | if output_path and not os.path.isabs(output_path): 343 | output_path = os.path.abspath(output_path) 344 | 345 | # 如果提供了输出路径,确保其父目录存在 346 | if output_path: 347 | output_dir = os.path.dirname(output_path) 348 | if not os.path.exists(output_dir): 349 | try: 350 | os.makedirs(output_dir, exist_ok=True) 351 | print(f"已创建输出目录: {output_dir}") 352 | error_messages.append(f"已创建输出目录: {output_dir}") 353 | except Exception as e: 354 | error_msg = f"创建输出目录 {output_dir} 时出错: {e}" 355 | error_messages.append(error_msg) 356 | return error_msg 357 | 358 | # 检查svg_path的类型并分别处理 359 | if isinstance(svg_path, str): 360 | # 单个SVG文件处理 361 | return process_single_svg( 362 | pptx_path, svg_path, slide_number, x, y, width, height, 363 | output_path, create_if_not_exists 364 | ) 365 | elif isinstance(svg_path, list): 366 | # 批量处理SVG文件列表 367 | success_count = 0 368 | total_count = len(svg_path) 369 | 370 | if total_count == 0: 371 | return "错误:SVG文件列表为空" 372 | 373 | # 创建中间文件路径基础 374 | temp_base = os.path.join(get_tmp_dir(), f"svg_batch_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}") 375 | os.makedirs(os.path.dirname(temp_base), exist_ok=True) 376 | 377 | # 当前输入文件路径 378 | current_input = pptx_path 379 | 380 | for i, current_svg in enumerate(svg_path): 381 | current_slide = slide_number + i 382 | 383 | # 处理每个SVG文件 384 | if i < total_count - 1: 385 | # 对于非最后一个文件,创建临时输出路径 386 | temp_output = f"{temp_base}_step_{i}.pptx" 387 | 388 | result = process_single_svg( 389 | current_input, 390 | current_svg, 391 | current_slide, 392 | x, y, width, height, 393 | temp_output, 394 | create_if_not_exists 395 | ) 396 | 397 | # 下一次迭代的输入文件是本次的输出文件 398 | current_input = temp_output 399 | else: 400 | # 最后一个SVG使用最终输出路径 401 | final_output = output_path if output_path else pptx_path 402 | 403 | result = process_single_svg( 404 | current_input, 405 | current_svg, 406 | current_slide, 407 | x, y, width, height, 408 | final_output, 409 | create_if_not_exists 410 | ) 411 | 412 | # 检查处理结果 413 | if "成功" in result: 414 | success_count += 1 415 | result_messages.append(f"第{i+1}个SVG({current_svg}):成功添加到第{current_slide}页") 416 | else: 417 | result_messages.append(f"第{i+1}个SVG({current_svg}):添加失败 - {result}") 418 | 419 | # 清理临时文件 420 | for i in range(total_count - 1): 421 | temp_file = f"{temp_base}_step_{i}.pptx" 422 | if os.path.exists(temp_file): 423 | try: 424 | os.remove(temp_file) 425 | except Exception as e: 426 | print(f"清理临时文件 {temp_file} 时出错: {e}") 427 | 428 | # 返回总体结果 429 | result_path = output_path or pptx_path 430 | summary = f"批量处理完成:共{total_count}个SVG文件,成功{success_count}个,失败{total_count-success_count}个" 431 | details = "\n".join(result_messages) 432 | return f"{summary}\n输出文件:{result_path}\n\n详细结果:\n{details}" 433 | else: 434 | return f"错误:svg_path类型无效,必须是字符串或字符串列表,当前类型: {type(svg_path)}" 435 | 436 | def process_single_svg( 437 | pptx_path: str, 438 | svg_path: str, 439 | slide_number: int, 440 | x: Optional[Union[Inches, Pt, Cm, Emu, float]], 441 | y: Optional[Union[Inches, Pt, Cm, Emu, float]], 442 | width: Optional[Union[Inches, Pt, Cm, Emu, float]], 443 | height: Optional[Union[Inches, Pt, Cm, Emu, float]], 444 | output_path: Optional[str], 445 | create_if_not_exists: bool 446 | ) -> str: 447 | """处理单个SVG文件的辅助函数""" 448 | # 检查SVG文件是否存在,如果是相对路径则转换为绝对路径 449 | if not os.path.isabs(svg_path): 450 | svg_path = os.path.abspath(svg_path) 451 | 452 | # 确保SVG文件的父目录存在 453 | svg_dir = os.path.dirname(svg_path) 454 | if not os.path.exists(svg_dir): 455 | try: 456 | os.makedirs(svg_dir, exist_ok=True) 457 | print(f"已创建SVG目录: {svg_dir}") 458 | except Exception as e: 459 | return f"创建SVG目录 {svg_dir} 时出错: {e}" 460 | 461 | # 如果SVG文件不存在且create_if_not_exists为True,则创建一个简单的SVG文件 462 | if not os.path.exists(svg_path) and create_if_not_exists: 463 | svg_created = create_svg_file(svg_path) 464 | if not svg_created: 465 | return f"错误:无法创建SVG文件 {svg_path}" 466 | elif not os.path.exists(svg_path): 467 | return f"错误:SVG文件 {svg_path} 不存在" 468 | 469 | # 确保输出路径存在,如果未指定则使用标准输出目录 470 | if not output_path: 471 | # 从原始文件名生成输出文件名 472 | base_name = os.path.splitext(os.path.basename(pptx_path))[0] 473 | # 清理文件名 474 | base_name = cleanup_filename(base_name) 475 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 476 | output_path = os.path.join(get_output_dir(), f"{base_name}_svg_{timestamp}.pptx") 477 | 478 | try: 479 | # 调用改进后的函数,它现在返回一个元组 (成功标志, 错误消息) 480 | result = insert_svg_to_pptx( 481 | pptx_path=pptx_path, 482 | svg_path=svg_path, 483 | slide_number=slide_number, 484 | x=x, 485 | y=y, 486 | width=width, 487 | height=height, 488 | output_path=output_path, 489 | create_if_not_exists=create_if_not_exists 490 | ) 491 | 492 | # 检查返回值类型 493 | if isinstance(result, tuple) and len(result) == 2: 494 | success, error_details = result 495 | else: 496 | # 向后兼容 497 | success = result 498 | error_details = "" 499 | 500 | if success: 501 | result_path = output_path or pptx_path 502 | was_created = not os.path.exists(pptx_path) and create_if_not_exists 503 | creation_msg = "(已自动创建PPTX文件)" if was_created else "" 504 | return f"成功将SVG文件 {svg_path} 插入到 {result_path} 的第 {slide_number} 张幻灯片 {creation_msg}" 505 | else: 506 | # 返回详细的错误信息 507 | return f"插入SVG到PPTX文件失败,详细错误信息:\n{error_details}" 508 | except Exception as e: 509 | # 收集异常堆栈 510 | error_trace = traceback.format_exc() 511 | return f"插入SVG时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}" 512 | 513 | @mcp.tool() 514 | def list_files(directory: str = ".", file_type: Optional[str] = None) -> str: 515 | """ 516 | 列出目录中的文件,可选按文件类型过滤。 517 | 如需查看svg文件是否正确保存,请输入svg文件的保存路径。 518 | Args: 519 | directory: 要列出文件的目录路径 520 | file_type: 文件类型过滤,可以是 "svg" 或 "pptx" 521 | 522 | Returns: 523 | 文件列表(每行一个文件) 524 | """ 525 | import os 526 | 527 | if not os.path.exists(directory): 528 | return f"错误:目录 {directory} 不存在" 529 | 530 | files = os.listdir(directory) 531 | 532 | if file_type: 533 | file_type = file_type.lower() 534 | extensions = { 535 | "svg": [".svg"], 536 | "pptx": [".pptx", ".ppt"] 537 | } 538 | 539 | if file_type in extensions: 540 | filtered_files = [] 541 | for file in files: 542 | if any(file.lower().endswith(ext) for ext in extensions[file_type]): 543 | filtered_files.append(file) 544 | files = filtered_files 545 | else: 546 | files = [f for f in files if f.lower().endswith(f".{file_type}")] 547 | 548 | if not files: 549 | return f"未找到{'任何' if not file_type else f'{file_type}'} 文件" 550 | 551 | return "\n".join(files) 552 | 553 | @mcp.tool() 554 | def get_file_info(file_path: str) -> str: 555 | """ 556 | 获取文件信息,如存在状态、大小等。 557 | 558 | Args: 559 | file_path: 要查询的文件路径 560 | 561 | Returns: 562 | 文件信息 563 | """ 564 | import os 565 | 566 | if not os.path.exists(file_path): 567 | return f"文件 {file_path} 不存在" 568 | 569 | if os.path.isdir(file_path): 570 | return f"{file_path} 是一个目录" 571 | 572 | size_bytes = os.path.getsize(file_path) 573 | size_kb = size_bytes / 1024 574 | size_mb = size_kb / 1024 575 | 576 | if size_mb >= 1: 577 | size_str = f"{size_mb:.2f} MB" 578 | else: 579 | size_str = f"{size_kb:.2f} KB" 580 | 581 | modified_time = os.path.getmtime(file_path) 582 | from datetime import datetime 583 | modified_str = datetime.fromtimestamp(modified_time).strftime("%Y-%m-%d %H:%M:%S") 584 | 585 | # 获取文件扩展名 586 | _, ext = os.path.splitext(file_path) 587 | ext = ext.lower() 588 | 589 | file_type = None 590 | if ext == ".svg": 591 | file_type = "SVG图像" 592 | elif ext in [".pptx", ".ppt"]: 593 | file_type = "PowerPoint演示文稿" 594 | else: 595 | file_type = f"{ext[1:]} 文件" if ext else "未知类型文件" 596 | 597 | return f"文件: {file_path}\n类型: {file_type}\n大小: {size_str}\n修改时间: {modified_str}" 598 | 599 | # 添加一个将SVG转换为PNG的工具 600 | @mcp.tool() 601 | def convert_svg_to_png( 602 | svg_path: str, 603 | output_path: Optional[str] = None 604 | ) -> str: 605 | """ 606 | 将SVG文件转换为PNG图像。 607 | 608 | Args: 609 | svg_path: SVG文件路径 610 | output_path: 输出PNG文件路径,如果未指定则使用相同文件名但扩展名为.png 611 | 612 | Returns: 613 | 操作结果消息 614 | """ 615 | from reportlab.graphics import renderPM 616 | from svglib.svglib import svg2rlg 617 | import os 618 | 619 | if not os.path.exists(svg_path): 620 | return f"错误:SVG文件 {svg_path} 不存在" 621 | 622 | if not output_path: 623 | # 获取不带扩展名的文件名,然后添加.png扩展名 624 | base_name = os.path.splitext(svg_path)[0] 625 | output_path = f"{base_name}.png" 626 | 627 | try: 628 | drawing = svg2rlg(svg_path) 629 | if drawing is None: 630 | return f"错误:无法读取SVG文件 {svg_path}" 631 | 632 | renderPM.drawToFile(drawing, output_path, fmt="PNG") 633 | return f"成功将SVG文件 {svg_path} 转换为PNG文件 {output_path}\n宽度: {drawing.width}px\n高度: {drawing.height}px" 634 | except Exception as e: 635 | return f"转换SVG到PNG时发生错误: {str(e)}" 636 | 637 | @mcp.tool() 638 | def get_pptx_info(pptx_path: str) -> str: 639 | """ 640 | 获取PPTX文件的基本信息,包括幻灯片数量。 641 | 642 | Args: 643 | pptx_path: PPTX文件路径 644 | 645 | Returns: 646 | 包含文件信息和幻灯片数量的字符串 647 | """ 648 | import os 649 | 650 | # 确保路径存在 651 | if not os.path.isabs(pptx_path): 652 | pptx_path = os.path.abspath(pptx_path) 653 | 654 | # 先获取基本文件信息 655 | if not os.path.exists(pptx_path): 656 | return f"错误:文件 {pptx_path} 不存在" 657 | 658 | size_bytes = os.path.getsize(pptx_path) 659 | size_kb = size_bytes / 1024 660 | size_mb = size_kb / 1024 661 | 662 | if size_mb >= 1: 663 | size_str = f"{size_mb:.2f} MB" 664 | else: 665 | size_str = f"{size_kb:.2f} KB" 666 | 667 | modified_time = os.path.getmtime(pptx_path) 668 | from datetime import datetime 669 | modified_str = datetime.fromtimestamp(modified_time).strftime("%Y-%m-%d %H:%M:%S") 670 | 671 | # 获取幻灯片数量 672 | slide_count, error = get_pptx_slide_count(pptx_path) 673 | 674 | if error: 675 | slide_info = f"获取幻灯片数量失败:{error}" 676 | else: 677 | slide_info = f"幻灯片数量:{slide_count}张" 678 | 679 | return f"PPT文件: {pptx_path}\n大小: {size_str}\n修改时间: {modified_str}\n{slide_info}" 680 | 681 | @mcp.tool() 682 | def save_svg_code( 683 | svg_code: str 684 | ) -> str: 685 | """ 686 | 将SVG代码保存为SVG文件并返回保存的绝对路径。 687 | !!!注意:特殊字符如"&"需要转义为"&" 688 | Args: 689 | svg_code: SVG代码内容 690 | 691 | Returns: 692 | 操作结果消息,包含保存的文件路径或错误信息 693 | """ 694 | try: 695 | # 调用svg_module中的函数保存SVG代码 696 | success, file_path, error_message = save_svg_code_to_file( 697 | svg_code=svg_code, 698 | output_path="", 699 | create_dirs=True 700 | ) 701 | 702 | if success: 703 | return f"成功保存SVG代码到文件: {file_path}" 704 | else: 705 | return f"保存SVG代码到文件失败: {error_message}" 706 | except Exception as e: 707 | error_trace = traceback.format_exc() 708 | return f"保存SVG代码到文件时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}" 709 | 710 | @mcp.tool() 711 | def delete_slide( 712 | pptx_path: str, 713 | slide_number: int, 714 | output_path: Optional[str] = None 715 | ) -> str: 716 | """ 717 | 从PPTX文件中删除指定编号的幻灯片。 718 | 719 | !!!注意: 720 | 721 | 在使用SVG替换PPT幻灯片内容时,我们发现了一些关键点,以下是正确替换PPT内容的方法总结: 722 | 723 | ### 正确替换PPT内容的方法 724 | 725 | 1. **完全替换法**(最可靠): 726 | - 删除需要替换的幻灯片(使用`delete_slide`功能) 727 | - 在同一位置插入空白幻灯片(使用`insert_blank_slide`功能) 728 | - 将新的SVG内容插入到空白幻灯片(使用`insert_svg`功能) 729 | 730 | 2. **新文件法**(适合多页修改): 731 | - 创建全新的PPT文件 732 | - 将所有需要的SVG(包括已修改的)按顺序插入到新文件中 733 | - 这样可以避免在旧文件上操作导致的混淆和叠加问题 734 | 735 | 3. **注意事项**: 736 | - 直接对现有幻灯片插入SVG会导致新内容叠加在原内容上,而非替换 737 | - 文件名可能会随着多次操作变得过长,影响可读性 738 | - 批量插入SVG时,`svg_path`参数必须是数组形式,即使只有一个文件 739 | - 操作后应检查输出文件以确认修改是否成功 740 | 741 | ### 推荐工作流 742 | 743 | 1. 先保存修改后的SVG内容到文件 744 | 2. 创建一个全新的PPT文件 745 | 3. 按顺序一次性插入所有SVG(包括已修改和未修改的) 746 | 4. 使用简洁直观的文件名 747 | 748 | 这种方法避免了多步骤操作导致的文件混乱,也能确保每张幻灯片都是干净的、不包含叠加内容的。 749 | 750 | Args: 751 | pptx_path: PPTX文件路径 752 | slide_number: 要删除的幻灯片编号(从1开始) 753 | output_path: 输出文件路径,如果未指定则使用标准输出目录 754 | 755 | Returns: 756 | 操作结果消息 757 | """ 758 | try: 759 | # 确保路径是绝对路径 760 | if not os.path.isabs(pptx_path): 761 | pptx_path = os.path.abspath(pptx_path) 762 | 763 | # 检查文件是否存在 764 | if not os.path.exists(pptx_path): 765 | return f"错误:PPTX文件 {pptx_path} 不存在" 766 | 767 | # 处理输出路径,如果未指定则使用标准输出目录 768 | if not output_path: 769 | # 从原始文件名生成输出文件名 770 | base_name = os.path.splitext(os.path.basename(pptx_path))[0] 771 | # 清理文件名 772 | base_name = cleanup_filename(base_name) 773 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 774 | output_path = os.path.join(get_output_dir(), f"{base_name}_deleted_{timestamp}.pptx") 775 | 776 | if output_path and not os.path.isabs(output_path): 777 | output_path = os.path.abspath(output_path) 778 | 779 | # 如果提供了输出路径,确保其父目录存在 780 | if output_path: 781 | output_dir = os.path.dirname(output_path) 782 | if not os.path.exists(output_dir): 783 | try: 784 | os.makedirs(output_dir, exist_ok=True) 785 | except Exception as e: 786 | return f"创建输出目录 {output_dir} 时出错: {e}" 787 | 788 | # 使用python-pptx加载演示文稿 789 | from pptx import Presentation 790 | prs = Presentation(pptx_path) 791 | 792 | # 检查幻灯片编号范围 793 | if not 1 <= slide_number <= len(prs.slides): 794 | return f"错误:幻灯片编号 {slide_number} 超出范围 [1, {len(prs.slides)}]" 795 | 796 | # 计算索引(转换为从0开始) 797 | slide_index = slide_number - 1 798 | 799 | # 使用用户提供的方法删除幻灯片 800 | slides = list(prs.slides._sldIdLst) 801 | prs.slides._sldIdLst.remove(slides[slide_index]) 802 | 803 | # 保存文件 804 | save_path = output_path 805 | prs.save(save_path) 806 | 807 | return f"成功从 {pptx_path} 中删除第 {slide_number} 张幻灯片,结果已保存到 {save_path}" 808 | 809 | except Exception as e: 810 | error_trace = traceback.format_exc() 811 | return f"删除幻灯片时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}" 812 | 813 | @mcp.tool() 814 | def insert_blank_slide( 815 | pptx_path: str, 816 | slide_number: int, 817 | layout_index: int = 6, # 默认使用空白布局 818 | output_path: Optional[str] = None, 819 | create_if_not_exists: bool = True 820 | ) -> str: 821 | """ 822 | 在PPTX文件的指定位置插入一个空白幻灯片。 823 | 824 | !!!注意: 825 | 826 | 在使用SVG替换PPT幻灯片内容时,我们发现了一些关键点,以下是正确替换PPT内容的方法总结: 827 | 828 | ### 正确替换PPT内容的方法 829 | 830 | 1. **完全替换法**(最可靠): 831 | - 删除需要替换的幻灯片(使用`delete_slide`功能) 832 | - 在同一位置插入空白幻灯片(使用`insert_blank_slide`功能) 833 | - 将新的SVG内容插入到空白幻灯片(使用`insert_svg`功能) 834 | 835 | 2. **新文件法**(适合多页修改): 836 | - 创建全新的PPT文件 837 | - 将所有需要的SVG(包括已修改的)按顺序插入到新文件中 838 | - 这样可以避免在旧文件上操作导致的混淆和叠加问题 839 | 840 | 3. **注意事项**: 841 | - 直接对现有幻灯片插入SVG会导致新内容叠加在原内容上,而非替换 842 | - 文件名可能会随着多次操作变得过长,影响可读性 843 | - 批量插入SVG时,`svg_path`参数必须是数组形式,即使只有一个文件 844 | - 操作后应检查输出文件以确认修改是否成功 845 | 846 | ### 推荐工作流 847 | 848 | 1. 先保存修改后的SVG内容到文件 849 | 2. 创建一个全新的PPT文件 850 | 3. 按顺序一次性插入所有SVG(包括已修改和未修改的) 851 | 4. 使用简洁直观的文件名 852 | 853 | 这种方法避免了多步骤操作导致的文件混乱,也能确保每张幻灯片都是干净的、不包含叠加内容的。 854 | 855 | Args: 856 | pptx_path: PPTX文件路径 857 | slide_number: 要插入幻灯片的位置编号(从1开始) 858 | layout_index: 幻灯片布局索引,默认为6(空白布局) 859 | output_path: 输出文件路径,如果未指定则使用标准输出目录 860 | create_if_not_exists: 如果为True且PPTX文件不存在,将自动创建一个新文件 861 | 862 | Returns: 863 | 操作结果消息 864 | """ 865 | try: 866 | # 如果未提供pptx_path,使用默认输出目录创建一个 867 | if not pptx_path or pptx_path.strip() == "": 868 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 869 | pptx_path = os.path.join(get_output_dir(), f"presentation_{timestamp}.pptx") 870 | print(f"未提供PPTX路径,将使用默认路径: {pptx_path}") 871 | 872 | # 确保路径是绝对路径 873 | if not os.path.isabs(pptx_path): 874 | pptx_path = os.path.abspath(pptx_path) 875 | 876 | # 处理输出路径,如果未指定则使用标准输出目录 877 | if not output_path: 878 | # 从原始文件名生成输出文件名 879 | base_name = os.path.splitext(os.path.basename(pptx_path))[0] 880 | # 清理文件名 881 | base_name = cleanup_filename(base_name) 882 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 883 | output_path = os.path.join(get_output_dir(), f"{base_name}_inserted_{timestamp}.pptx") 884 | 885 | if output_path and not os.path.isabs(output_path): 886 | output_path = os.path.abspath(output_path) 887 | 888 | # 如果提供了输出路径,确保其父目录存在 889 | if output_path: 890 | output_dir = os.path.dirname(output_path) 891 | if not os.path.exists(output_dir): 892 | try: 893 | os.makedirs(output_dir, exist_ok=True) 894 | except Exception as e: 895 | return f"创建输出目录 {output_dir} 时出错: {e}" 896 | 897 | # 检查文件是否存在 898 | file_exists = os.path.exists(pptx_path) 899 | if not file_exists and not create_if_not_exists: 900 | return f"错误:PPTX文件 {pptx_path} 不存在,且未启用自动创建" 901 | 902 | # 使用python-pptx加载或创建演示文稿 903 | from pptx import Presentation 904 | prs = Presentation(pptx_path) if file_exists else Presentation() 905 | 906 | # 如果是新创建的演示文稿,设置为16:9尺寸 907 | if not file_exists: 908 | prs.slide_width = Inches(16) 909 | prs.slide_height = Inches(9) 910 | 911 | # 检查布局索引是否有效 912 | if layout_index >= len(prs.slide_layouts): 913 | return f"错误:无效的布局索引 {layout_index},可用范围 [0, {len(prs.slide_layouts)-1}]" 914 | 915 | # 检查幻灯片编号范围 916 | slides_count = len(prs.slides) 917 | if not 1 <= slide_number <= slides_count + 1: 918 | return f"错误:幻灯片位置 {slide_number} 超出范围 [1, {slides_count + 1}]" 919 | 920 | # 计算索引(转换为从0开始) 921 | slide_index = slide_number - 1 922 | 923 | # 在末尾添加新幻灯片 924 | new_slide = prs.slides.add_slide(prs.slide_layouts[layout_index]) 925 | 926 | # 如果不是添加到末尾,需要移动幻灯片 927 | if slide_index < slides_count: 928 | # 获取幻灯片列表 929 | slides = list(prs.slides._sldIdLst) 930 | # 将最后一张幻灯片(刚添加的)移动到目标位置 931 | last_slide = slides[-1] 932 | # 从列表中移除最后一张幻灯片 933 | prs.slides._sldIdLst.remove(last_slide) 934 | # 在目标位置插入幻灯片 935 | prs.slides._sldIdLst.insert(slide_index, last_slide) 936 | 937 | # 保存文件 938 | save_path = output_path 939 | prs.save(save_path) 940 | 941 | # 构建返回消息 942 | action = "添加" if file_exists else "创建并添加" 943 | return f"成功在 {pptx_path} 中{action}第 {slide_number} 张幻灯片,结果已保存到 {save_path}" 944 | 945 | except Exception as e: 946 | error_trace = traceback.format_exc() 947 | return f"插入幻灯片时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}" 948 | 949 | @mcp.tool() 950 | def copy_svg_slide( 951 | source_pptx_path: str, 952 | target_pptx_path: str = "", 953 | source_slide_number: int = 1, 954 | target_slide_number: Optional[int] = None, 955 | output_path: Optional[str] = None, 956 | create_if_not_exists: bool = True 957 | ) -> str: 958 | """ 959 | 专门用于复制包含SVG图像的幻灯片,确保SVG和相关引用都被正确复制。 960 | 961 | 此函数使用直接操作PPTX内部XML文件的方式,确保SVG图像及其引用在复制过程中完全保留。 962 | 与普通的copy_slide函数相比,此函数特别关注SVG图像的复制,保证SVG的矢量属性在复制后依然可用。 963 | 964 | Args: 965 | source_pptx_path: 源PPTX文件路径 966 | target_pptx_path: 目标PPTX文件路径,如果为空则创建新文件 967 | source_slide_number: 要复制的源幻灯片页码(从1开始) 968 | target_slide_number: 要插入到目标文件的位置(从1开始),如果为None则添加到末尾 969 | output_path: 输出文件路径,如果未指定则使用标准输出目录 970 | create_if_not_exists: 如果为True且目标PPTX文件不存在,将自动创建一个新文件 971 | 972 | Returns: 973 | 操作结果消息 974 | """ 975 | import zipfile 976 | import tempfile 977 | import os 978 | import shutil 979 | from lxml import etree 980 | from pptx import Presentation 981 | from pptx.util import Inches 982 | 983 | try: 984 | # 创建临时目录 985 | temp_dir = tempfile.mkdtemp() 986 | source_extract_dir = os.path.join(temp_dir, "source") 987 | target_extract_dir = os.path.join(temp_dir, "target") 988 | 989 | os.makedirs(source_extract_dir, exist_ok=True) 990 | os.makedirs(target_extract_dir, exist_ok=True) 991 | 992 | # 确保源路径是绝对路径 993 | if not os.path.isabs(source_pptx_path): 994 | source_pptx_path = os.path.abspath(source_pptx_path) 995 | 996 | # 检查源文件是否存在 997 | if not os.path.exists(source_pptx_path): 998 | return f"错误:源PPTX文件 {source_pptx_path} 不存在" 999 | 1000 | # 处理目标路径 1001 | if not target_pptx_path: 1002 | # 创建新的目标文件(基于源文件名) 1003 | base_name = os.path.splitext(os.path.basename(source_pptx_path))[0] 1004 | base_name = cleanup_filename(base_name) 1005 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 1006 | target_pptx_path = os.path.join(get_output_dir(), f"{base_name}_copied_{timestamp}.pptx") 1007 | 1008 | # 确保路径是绝对路径 1009 | if not os.path.isabs(target_pptx_path): 1010 | target_pptx_path = os.path.abspath(target_pptx_path) 1011 | 1012 | # 处理输出路径,如果未指定则使用标准输出目录 1013 | if not output_path: 1014 | # 从目标文件名生成输出文件名 1015 | base_name = os.path.splitext(os.path.basename(target_pptx_path))[0] 1016 | base_name = cleanup_filename(base_name) 1017 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 1018 | output_path = os.path.join(get_output_dir(), f"{base_name}_svg_copied_{timestamp}.pptx") 1019 | 1020 | if output_path and not os.path.isabs(output_path): 1021 | output_path = os.path.abspath(output_path) 1022 | 1023 | # 如果提供了输出路径,确保其父目录存在 1024 | if output_path: 1025 | output_dir = os.path.dirname(output_path) 1026 | if not os.path.exists(output_dir): 1027 | try: 1028 | os.makedirs(output_dir, exist_ok=True) 1029 | except Exception as e: 1030 | return f"创建输出目录 {output_dir} 时出错: {e}" 1031 | 1032 | # 解压源PPTX文件 1033 | with zipfile.ZipFile(source_pptx_path, 'r') as zip_ref: 1034 | zip_ref.extractall(source_extract_dir) 1035 | 1036 | # 创建新的目标文件或使用现有文件 1037 | if not os.path.exists(target_pptx_path): 1038 | if create_if_not_exists: 1039 | # 创建一个新的PPTX文件 1040 | prs = Presentation() 1041 | prs.slide_width = Inches(16) 1042 | prs.slide_height = Inches(9) 1043 | prs.save(target_pptx_path) 1044 | else: 1045 | return f"错误:目标PPTX文件 {target_pptx_path} 不存在,且未启用自动创建" 1046 | 1047 | # 解压目标PPTX文件 1048 | with zipfile.ZipFile(target_pptx_path, 'r') as zip_ref: 1049 | zip_ref.extractall(target_extract_dir) 1050 | 1051 | # 加载源演示文稿和目标演示文稿以获取信息 1052 | source_prs = Presentation(source_pptx_path) 1053 | target_prs = Presentation(target_pptx_path) 1054 | 1055 | # 检查源幻灯片编号范围 1056 | if not 1 <= source_slide_number <= len(source_prs.slides): 1057 | return f"错误:源幻灯片编号 {source_slide_number} 超出范围 [1, {len(source_prs.slides)}]" 1058 | 1059 | # 确定目标幻灯片位置 1060 | target_slides_count = len(target_prs.slides) 1061 | if target_slide_number is None: 1062 | # 如果未指定目标位置,添加到末尾 1063 | target_slide_number = target_slides_count + 1 1064 | 1065 | # 检查目标位置是否有效 1066 | if not 1 <= target_slide_number <= target_slides_count + 1: 1067 | # 如果目标位置超出范围,添加空白幻灯片使其有效 1068 | blank_slides_to_add = target_slide_number - target_slides_count 1069 | for _ in range(blank_slides_to_add): 1070 | target_prs.slides.add_slide(target_prs.slide_layouts[6]) # 6通常是空白布局 1071 | target_prs.save(target_pptx_path) 1072 | 1073 | # 重新解压更新后的目标文件 1074 | shutil.rmtree(target_extract_dir) 1075 | os.makedirs(target_extract_dir, exist_ok=True) 1076 | with zipfile.ZipFile(target_pptx_path, 'r') as zip_ref: 1077 | zip_ref.extractall(target_extract_dir) 1078 | 1079 | # 复制幻灯片内容 1080 | source_slide_path = os.path.join(source_extract_dir, "ppt", "slides", f"slide{source_slide_number}.xml") 1081 | source_rels_path = os.path.join(source_extract_dir, "ppt", "slides", "_rels", f"slide{source_slide_number}.xml.rels") 1082 | 1083 | target_slide_path = os.path.join(target_extract_dir, "ppt", "slides", f"slide{target_slide_number}.xml") 1084 | target_rels_path = os.path.join(target_extract_dir, "ppt", "slides", "_rels", f"slide{target_slide_number}.xml.rels") 1085 | 1086 | # 确保目标目录存在 1087 | os.makedirs(os.path.dirname(target_slide_path), exist_ok=True) 1088 | os.makedirs(os.path.dirname(target_rels_path), exist_ok=True) 1089 | 1090 | # 复制幻灯片XML 1091 | if os.path.exists(source_slide_path): 1092 | shutil.copy2(source_slide_path, target_slide_path) 1093 | print(f"已复制幻灯片XML: {source_slide_path} -> {target_slide_path}") 1094 | else: 1095 | print(f"源幻灯片文件不存在: {source_slide_path}") 1096 | return f"错误:源幻灯片文件不存在: {source_slide_path}" 1097 | 1098 | # 复制关系文件 1099 | svg_files = [] 1100 | png_files = [] 1101 | 1102 | if os.path.exists(source_rels_path): 1103 | shutil.copy2(source_rels_path, target_rels_path) 1104 | print(f"已复制幻灯片关系文件: {source_rels_path} -> {target_rels_path}") 1105 | 1106 | # 查找并复制所有媒体文件 1107 | try: 1108 | parser = etree.XMLParser(remove_blank_text=True) 1109 | rels_tree = etree.parse(source_rels_path, parser) 1110 | rels_root = rels_tree.getroot() 1111 | 1112 | for rel in rels_root.findall("{http://schemas.openxmlformats.org/package/2006/relationships}Relationship"): 1113 | target = rel.get("Target") 1114 | if target and "../media/" in target: 1115 | # 提取媒体文件名 1116 | media_file = os.path.basename(target) 1117 | source_media_path = os.path.join(source_extract_dir, "ppt", "media", media_file) 1118 | target_media_path = os.path.join(target_extract_dir, "ppt", "media", media_file) 1119 | 1120 | # 确保目标媒体目录存在 1121 | os.makedirs(os.path.dirname(target_media_path), exist_ok=True) 1122 | 1123 | # 复制媒体文件 1124 | if os.path.exists(source_media_path): 1125 | shutil.copy2(source_media_path, target_media_path) 1126 | print(f"已复制媒体文件: {source_media_path} -> {target_media_path}") 1127 | 1128 | # 检查是否为SVG或PNG文件 1129 | if media_file.lower().endswith(".svg"): 1130 | svg_files.append(media_file) 1131 | elif media_file.lower().endswith(".png"): 1132 | png_files.append(media_file) 1133 | else: 1134 | print(f"源媒体文件不存在: {source_media_path}") 1135 | except Exception as e: 1136 | print(f"处理关系文件时出错: {e}") 1137 | import traceback 1138 | print(traceback.format_exc()) 1139 | else: 1140 | print(f"源关系文件不存在: {source_rels_path}") 1141 | return f"错误:源关系文件不存在: {source_rels_path}" 1142 | 1143 | # 处理[Content_Types].xml文件以支持SVG 1144 | if svg_files: 1145 | print(f"发现SVG文件: {svg_files}") 1146 | content_types_path = os.path.join(target_extract_dir, "[Content_Types].xml") 1147 | 1148 | if os.path.exists(content_types_path): 1149 | try: 1150 | parser = etree.XMLParser(remove_blank_text=True) 1151 | content_types_tree = etree.parse(content_types_path, parser) 1152 | content_types_root = content_types_tree.getroot() 1153 | 1154 | # 检查是否已经存在SVG类型 1155 | svg_exists = False 1156 | for elem in content_types_root.findall("Default"): 1157 | if elem.get("Extension") == "svg": 1158 | svg_exists = True 1159 | break 1160 | 1161 | # 如果不存在,添加SVG类型 1162 | if not svg_exists: 1163 | print("添加SVG Content Type到[Content_Types].xml") 1164 | etree.SubElement( 1165 | content_types_root, 1166 | "Default", 1167 | Extension="svg", 1168 | ContentType="image/svg+xml" 1169 | ) 1170 | 1171 | # 保存修改后的Content Types文件 1172 | content_types_tree.write( 1173 | content_types_path, 1174 | xml_declaration=True, 1175 | encoding='UTF-8', 1176 | standalone="yes" 1177 | ) 1178 | except Exception as e: 1179 | print(f"更新Content Types时出错: {e}") 1180 | return f"错误:更新Content Types时出错: {e}" 1181 | 1182 | # 处理presentation.xml以添加幻灯片引用 1183 | # 从目标文件读取presentation.xml 1184 | pres_path = os.path.join(target_extract_dir, "ppt", "presentation.xml") 1185 | pres_rels_path = os.path.join(target_extract_dir, "ppt", "_rels", "presentation.xml.rels") 1186 | 1187 | # 更新presentation.xml.rels以添加幻灯片引用 1188 | if os.path.exists(pres_rels_path): 1189 | try: 1190 | parser = etree.XMLParser(remove_blank_text=True) 1191 | pres_rels_tree = etree.parse(pres_rels_path, parser) 1192 | pres_rels_root = pres_rels_tree.getroot() 1193 | 1194 | # 查找最大的rId 1195 | max_rid = 0 1196 | slide_rels = [] 1197 | 1198 | for rel in pres_rels_root.findall("{http://schemas.openxmlformats.org/package/2006/relationships}Relationship"): 1199 | rid = rel.get("Id", "") 1200 | if rid.startswith("rId"): 1201 | try: 1202 | rid_num = int(rid[3:]) 1203 | if rid_num > max_rid: 1204 | max_rid = rid_num 1205 | except ValueError: 1206 | pass 1207 | 1208 | # 检查是否是幻灯片关系 1209 | if rel.get("Type") == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide": 1210 | slide_rels.append(rel) 1211 | 1212 | # 检查目标幻灯片编号的关系是否已存在 1213 | slide_rel_exists = False 1214 | target_slide_path_rel = f"slides/slide{target_slide_number}.xml" 1215 | 1216 | for rel in slide_rels: 1217 | if rel.get("Target") == target_slide_path_rel: 1218 | slide_rel_exists = True 1219 | break 1220 | 1221 | # 如果需要,添加新的关系 1222 | if not slide_rel_exists: 1223 | new_rid = f"rId{max_rid + 1}" 1224 | new_rel = etree.SubElement( 1225 | pres_rels_root, 1226 | "{http://schemas.openxmlformats.org/package/2006/relationships}Relationship", 1227 | Id=new_rid, 1228 | Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide", 1229 | Target=target_slide_path_rel 1230 | ) 1231 | 1232 | # 保存修改后的关系文件 1233 | pres_rels_tree.write( 1234 | pres_rels_path, 1235 | xml_declaration=True, 1236 | encoding='UTF-8', 1237 | standalone="yes" 1238 | ) 1239 | 1240 | # 更新presentation.xml中的幻灯片列表 1241 | if os.path.exists(pres_path): 1242 | try: 1243 | pres_tree = etree.parse(pres_path, parser) 1244 | pres_root = pres_tree.getroot() 1245 | 1246 | # 查找sldIdLst元素 1247 | sld_id_lst = pres_root.find(".//{http://schemas.openxmlformats.org/presentationml/2006/main}sldIdLst") 1248 | 1249 | if sld_id_lst is not None: 1250 | # 查找最大的幻灯片ID 1251 | max_sld_id = 256 # 幻灯片ID通常从256开始 1252 | for sld_id in sld_id_lst.findall(".//{http://schemas.openxmlformats.org/presentationml/2006/main}sldId"): 1253 | try: 1254 | id_val = int(sld_id.get("id")) 1255 | if id_val > max_sld_id: 1256 | max_sld_id = id_val 1257 | except (ValueError, TypeError): 1258 | pass 1259 | 1260 | # 添加新的幻灯片引用 1261 | new_sld_id = etree.SubElement( 1262 | sld_id_lst, 1263 | "{http://schemas.openxmlformats.org/presentationml/2006/main}sldId", 1264 | id=str(max_sld_id + 1), 1265 | **{"{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id": new_rid} 1266 | ) 1267 | 1268 | # 保存修改后的presentation.xml 1269 | pres_tree.write( 1270 | pres_path, 1271 | xml_declaration=True, 1272 | encoding='UTF-8', 1273 | standalone="yes" 1274 | ) 1275 | except Exception as e: 1276 | print(f"更新presentation.xml时出错: {e}") 1277 | except Exception as e: 1278 | print(f"更新presentation.xml.rels时出错: {e}") 1279 | 1280 | # 重新打包PPTX文件 1281 | save_path = output_path or target_pptx_path 1282 | if os.path.exists(save_path): 1283 | os.remove(save_path) 1284 | 1285 | with zipfile.ZipFile(save_path, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: 1286 | for root, _, files in os.walk(target_extract_dir): 1287 | for file in files: 1288 | file_path = os.path.join(root, file) 1289 | arcname = os.path.relpath(file_path, target_extract_dir) 1290 | zipf.write(file_path, arcname) 1291 | 1292 | # 清理临时目录 1293 | shutil.rmtree(temp_dir) 1294 | 1295 | # 返回成功消息 1296 | svg_count = len(svg_files) 1297 | svg_info = f",包含{svg_count}个SVG图像" if svg_count > 0 else "" 1298 | return f"成功将幻灯片从 {source_pptx_path} 的第 {source_slide_number} 页复制到 {save_path} 的第 {target_slide_number} 页{svg_info}" 1299 | 1300 | except Exception as e: 1301 | # 清理临时目录 1302 | if 'temp_dir' in locals() and os.path.exists(temp_dir): 1303 | shutil.rmtree(temp_dir) 1304 | 1305 | error_trace = traceback.format_exc() 1306 | return f"复制SVG幻灯片时发生错误: {str(e)}\n\n详细堆栈跟踪:\n{error_trace}" 1307 | 1308 | @mcp.tool(description="使用SVG设计宗师角色提示,当用户希望大模型生成或优化ppt时,使用此角色提示") 1309 | def svg_prompt(source: str) -> str: 1310 | """ 1311 | 使用SVG设计宗师角色提示,并将用户具体需求嵌入其中。 1312 | 主要用途:当用户希望大模型生成、优化ppt时,使用此角色提示, 1313 | 引导大模型基于用户需求生成16:9的高质量SVG代码。 1314 | 1、生成svg代码后[自动调用]okppt工具的save_svg方法将svg代码保存到本地, 1315 | 2、然后调用okppt的insert_svg工具将svg代码全屏插入ppt,达到设计ppt的效果。 1316 | 输入: 1317 | source: str, 用户希望大模型生成的ppt的结构、内容或主题等相关需求。 1318 | 输出: 1319 | str, 包含用户具体需求的、完整的“SVG设计宗师”架构化提示词,大模型可直接使用该提示词生成高质量svg代码。 1320 | """ 1321 | 1322 | user_demand_snippet = f"""## 0. 当前核心设计任务 (User's Core Design Task) 1323 | 1324 | 用户提供的核心需求如下: 1325 | 1326 | ```text 1327 | {source} 1328 | ``` 1329 | 1330 | 请 SVG 设计宗师基于以上用户需求,并严格遵循后续的完整 Prompt 框架(角色定位、设计原则、内容理解、决策框架等)进行分析、设计并生成最终的SVG代码。 1331 | 在开始具体设计前,请先在“阶段一:深度聆听与精准解构”中,确认你对以上核心设计任务的理解。 1332 | """ 1333 | 1334 | # 使用占位符替换用户需求 1335 | if "%%%USER_CORE_DESIGN_TASK_HERE%%%" in PROMPT_TEMPLATE_CONTENT: 1336 | final_prompt = PROMPT_TEMPLATE_CONTENT.replace("%%%USER_CORE_DESIGN_TASK_HERE%%%", user_demand_snippet) 1337 | else: 1338 | # 如果模板中没有找到占位符,作为备选方案,仍在最前面添加 1339 | # 或者可以返回一个错误/警告,表明模板可能已损坏或不是预期版本 1340 | print(f"警告:占位符 '%%%USER_CORE_DESIGN_TASK_HERE%%%' 未在模板 '{PROMPT_TEMPLATE_CONTENT}' 中找到。用户需求将添加到Prompt开头。") 1341 | final_prompt = f"{PROMPT_TEMPLATE_CONTENT}\n\n用户的需求是:{user_demand_snippet}" 1342 | 1343 | return final_prompt 1344 | 1345 | # 启动服务器 1346 | if __name__ == "__main__": 1347 | # 确保必要的目录存在 1348 | tmp_dir = get_tmp_dir() 1349 | output_dir = get_output_dir() 1350 | 1351 | mcp.run(transport='stdio') -------------------------------------------------------------------------------- /mcp-server-okppt/src/mcp_server_okppt/svg_module.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import os 3 | import uuid 4 | import shutil 5 | from lxml import etree 6 | from reportlab.graphics import renderPM 7 | from svglib.svglib import svg2rlg 8 | from pptx.util import Inches, Pt, Cm, Emu 9 | from typing import Optional, Union, Tuple, List 10 | import traceback 11 | import sys 12 | from io import StringIO 13 | import datetime 14 | 15 | # 定义命名空间 16 | ns = { 17 | 'p': "http://schemas.openxmlformats.org/presentationml/2006/main", 18 | 'a': "http://schemas.openxmlformats.org/drawingml/2006/main", 19 | 'r': "http://schemas.openxmlformats.org/officeDocument/2006/relationships", 20 | 'asvg': "http://schemas.microsoft.com/office/drawing/2016/SVG/main" 21 | } 22 | 23 | # 路径辅助函数 24 | def get_base_dir(): 25 | """获取基础目录(服务器目录的父目录)""" 26 | current_dir = os.path.dirname(os.path.abspath(__file__)) 27 | return os.path.dirname(current_dir) 28 | 29 | def get_tmp_dir(): 30 | """获取临时文件目录,如果不存在则创建""" 31 | tmp_dir = os.path.join(get_base_dir(), "tmp") 32 | os.makedirs(tmp_dir, exist_ok=True) 33 | return tmp_dir 34 | 35 | def get_output_dir(): 36 | """获取输出文件目录,如果不存在则创建""" 37 | output_dir = os.path.join(get_base_dir(), "output") 38 | os.makedirs(output_dir, exist_ok=True) 39 | return output_dir 40 | 41 | def create_temp_dir(): 42 | """创建唯一的临时目录并返回路径""" 43 | temp_dir = os.path.join(get_tmp_dir(), f"pptx_{uuid.uuid4().hex}") 44 | os.makedirs(temp_dir, exist_ok=True) 45 | return temp_dir 46 | 47 | # 添加一个路径规范化函数 48 | def normalize_path(path: str) -> str: 49 | """ 50 | 规范化路径格式,处理不同的路径表示方法, 51 | 包括正斜杠、反斜杠、多重斜杠等情况。 52 | 53 | Args: 54 | path: 需要规范化的路径 55 | 56 | Returns: 57 | 规范化后的路径 58 | """ 59 | if not path: 60 | return path 61 | 62 | # 标准化路径,处理多重斜杠和混合斜杠的情况 63 | normalized = os.path.normpath(path) 64 | 65 | # 如果路径是绝对路径,转换为绝对路径 66 | if os.path.isabs(normalized): 67 | return normalized 68 | else: 69 | # 相对路径保持不变 70 | return normalized 71 | 72 | # 添加一个创建SVG文件的函数 73 | def create_svg_file(svg_path: str, width: int = 100, height: int = 100, text: str = "自动生成的SVG") -> bool: 74 | """ 75 | 创建一个简单的SVG文件。 76 | 77 | Args: 78 | svg_path: 要创建的SVG文件路径 79 | width: SVG宽度(像素) 80 | height: SVG高度(像素) 81 | text: 要在SVG中显示的文本 82 | 83 | Returns: 84 | bool: 如果成功创建则返回True,否则返回False 85 | """ 86 | try: 87 | # 规范化路径 88 | svg_path = normalize_path(svg_path) 89 | 90 | # 获取文件目录并确保存在 91 | svg_dir = os.path.dirname(svg_path) 92 | if svg_dir and not os.path.exists(svg_dir): 93 | os.makedirs(svg_dir, exist_ok=True) 94 | print(f"已创建SVG目录: {svg_dir}") 95 | 96 | # 创建一个简单的SVG 97 | svg_content = ( 98 | f'\n' 99 | f' \n' 100 | f' {text}\n' 101 | f'' 102 | ) 103 | 104 | with open(svg_path, "w", encoding="utf-8") as f: 105 | f.write(svg_content) 106 | 107 | print(f"成功创建SVG文件: {svg_path}") 108 | return True 109 | except Exception as e: 110 | print(f"创建SVG文件时出错: {e}") 111 | traceback.print_exc() 112 | return False 113 | 114 | def save_svg_code_to_file( 115 | svg_code: str, 116 | output_path: str = "",# 空字符串表示自动创建,否则使用绝对路径 117 | create_dirs: bool = True 118 | ) -> Tuple[bool, str, str]: 119 | """ 120 | 将SVG代码保存为SVG文件。 121 | 122 | Args: 123 | svg_code: SVG代码内容 124 | output_path: 输出文件路径,如果未指定,则生成一个带有时间戳的文件名 125 | create_dirs: 是否创建不存在的目录 126 | 127 | Returns: 128 | Tuple[bool, str, str]: (成功标志, 绝对路径, 错误消息) 129 | """ 130 | try: 131 | # 如果未提供输出路径,则生成一个带有时间戳的文件名 132 | if not output_path or output_path == "": 133 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 134 | output_path = os.path.join(get_output_dir(), f"svg_{timestamp}.svg") 135 | 136 | # 确保路径是绝对路径 137 | if not os.path.isabs(output_path): 138 | output_path = os.path.abspath(output_path) 139 | 140 | # 获取文件目录并确保存在 141 | output_dir = os.path.dirname(output_path) 142 | if output_dir and not os.path.exists(output_dir): 143 | if create_dirs: 144 | os.makedirs(output_dir, exist_ok=True) 145 | print(f"已创建目录: {output_dir}") 146 | else: 147 | return False, "", f"目录 {output_dir} 不存在" 148 | 149 | # 保存SVG代码到文件 150 | with open(output_path, "w", encoding="utf-8") as f: 151 | f.write(svg_code) 152 | 153 | print(f"成功保存SVG代码到文件: {output_path}") 154 | return True, output_path, "" 155 | except Exception as e: 156 | error_message = f"保存SVG代码到文件时出错: {e}" 157 | print(error_message) 158 | traceback.print_exc() 159 | return False, "", error_message 160 | 161 | # EMU 单位转换辅助函数 162 | def to_emu(value: Union[Inches, Pt, Cm, Emu, int, float]) -> str: 163 | """将pptx单位或数值(假定为Pt)转换为EMU字符串""" 164 | # 先处理明确的 pptx.util 类型 165 | if isinstance(value, Inches): 166 | emu_val = value.emu 167 | elif isinstance(value, Cm): 168 | emu_val = value.emu 169 | elif isinstance(value, Pt): 170 | emu_val = value.emu 171 | elif isinstance(value, Emu): 172 | emu_val = value.emu 173 | elif isinstance(value, (int, float)): 174 | # 如果是纯数字,假设单位是 Pt 175 | emu_val = Pt(value).emu 176 | else: 177 | raise TypeError(f"Unsupported unit type for EMU conversion: {type(value)}") 178 | 179 | return str(int(emu_val)) # 确保返回整数的字符串形式 180 | 181 | # 创建SVG文件的辅助函数 182 | def create_svg_file(svg_path: str, width: int = 100, height: int = 100, text: str = "自动生成的SVG") -> bool: 183 | """ 184 | 创建一个简单的SVG文件。 185 | 186 | Args: 187 | svg_path: 要创建的SVG文件路径 188 | width: SVG宽度(像素) 189 | height: SVG高度(像素) 190 | text: 要在SVG中显示的文本 191 | 192 | Returns: 193 | bool: 如果成功创建则返回True,否则返回False 194 | """ 195 | try: 196 | # 获取文件目录并确保存在 197 | svg_dir = os.path.dirname(svg_path) 198 | if svg_dir and not os.path.exists(svg_dir): 199 | os.makedirs(svg_dir, exist_ok=True) 200 | print(f"已创建SVG目录: {svg_dir}") 201 | 202 | # 创建一个简单的SVG 203 | svg_content = ( 204 | f'\n' 205 | f' \n' 206 | f' {text}\n' 207 | f'' 208 | ) 209 | 210 | with open(svg_path, "w", encoding="utf-8") as f: 211 | f.write(svg_content) 212 | 213 | print(f"成功创建SVG文件: {svg_path}") 214 | return True 215 | except Exception as e: 216 | print(f"创建SVG文件时出错: {e}") 217 | traceback.print_exc() 218 | return False 219 | 220 | 221 | def insert_svg_to_pptx( 222 | pptx_path: str, 223 | svg_path: str, 224 | slide_number: int = 1, 225 | x: Optional[Union[Inches, Pt, Cm, Emu, int]] = None, 226 | y: Optional[Union[Inches, Pt, Cm, Emu, int]] = None, 227 | width: Optional[Union[Inches, Pt, Cm, Emu, int]] = None, 228 | height: Optional[Union[Inches, Pt, Cm, Emu, int]] = None, 229 | output_path: Optional[str] = None, 230 | create_if_not_exists: bool = True 231 | ) -> Union[bool, Tuple[bool, str]]: 232 | """ 233 | 将 SVG 图像插入到 PPTX 文件指定幻灯片的指定位置。 234 | **默认行为:** 如果不提供 `x`, `y`, `width`, `height` 参数,SVG 将被插入 235 | 为幻灯片的全屏尺寸(位置 0,0,尺寸为幻灯片定义的宽度和高度)。 236 | 237 | **覆盖默认行为:** 238 | - 提供 `x` 和 `y` 来指定左上角位置 (使用 pptx.util 单位, 如 Inches(1), Pt(72))。 239 | - 提供 `width` 和/或 `height` 来指定尺寸 (使用 pptx.util 单位)。 240 | - 如果只提供了 `width` 而未提供 `height`,函数将尝试根据 SVG 的原始 241 | 宽高比计算高度。如果无法获取宽高比,将使用幻灯片的默认高度。 242 | 243 | **实现方式:** 244 | 此函数通过直接操作 PPTX 的内部 XML 文件来实现 SVG 插入。 245 | 它会自动将 SVG 转换为 PNG 作为备用图像(用于旧版Office或不支持SVG的查看器), 246 | 并将矢量 SVG 和 PNG 备用图都嵌入到 PPTX 文件中。 247 | 248 | Args: 249 | pptx_path: 原始 PPTX 文件的路径。如果文件不存在且create_if_not_exists为True,将自动创建。 250 | svg_path: 要插入的 SVG 文件的路径。 251 | slide_number: 要插入 SVG 的目标幻灯片编号 (从 1 开始)。 252 | x: 图片左上角的 X 坐标 (可选, 使用 pptx.util 单位)。默认为 0。 253 | y: 图片左上角的 Y 坐标 (可选, 使用 pptx.util 单位)。默认为 0。 254 | width: 图片的宽度 (可选, 使用 pptx.util 单位)。默认为幻灯片宽度。 255 | height: 图片的高度 (可选, 使用 pptx.util 单位)。默认为幻灯片高度。 256 | 如果只提供了 width 而未提供 height,将尝试根据 SVG 原始宽高比计算。 257 | output_path: 输出 PPTX 文件的路径。如果为 None,将覆盖原始文件。 258 | create_if_not_exists: 如果为True且PPTX文件不存在,将自动创建一个新文件。 259 | 260 | Returns: 261 | Union[bool, Tuple[bool, str]]: 如果成功插入则返回 (True, ""),否则返回 (False, error_details)。 262 | 错误细节包含所有错误消息和堆栈跟踪。 263 | 264 | Raises: 265 | FileNotFoundError: 如果 pptx_path 不存在且 create_if_not_exists 为 False。 266 | FileNotFoundError: 如果 svg_path 无效。 267 | etree.XMLSyntaxError: 如果 PPTX 内部的 XML 文件损坏或格式错误。 268 | Exception: 其他潜在错误,如图库依赖问题或文件权限问题。 269 | 270 | Dependencies: 271 | - lxml: 用于 XML 处理。 272 | - reportlab 和 svglib: 用于将 SVG 转换为 PNG。 273 | - python-pptx: 主要用于方便的单位转换 (Inches, Pt 等)。 274 | """ 275 | # 创建错误消息收集器 276 | error_log = [] 277 | def log_error(message): 278 | """记录错误消息到错误日志列表""" 279 | error_log.append(message) 280 | print(message) # 仍然打印到控制台 281 | 282 | # 创建临时目录 - 移到函数开始部分 283 | temp_dir = create_temp_dir() 284 | 285 | # 规范化并转换为绝对路径 286 | pptx_path = normalize_path(pptx_path) 287 | svg_path = normalize_path(svg_path) 288 | if output_path: 289 | output_path = normalize_path(output_path) 290 | 291 | # 转换为绝对路径 292 | if not os.path.isabs(pptx_path): 293 | pptx_path = os.path.abspath(pptx_path) 294 | 295 | if not os.path.isabs(svg_path): 296 | svg_path = os.path.abspath(svg_path) 297 | 298 | if output_path and not os.path.isabs(output_path): 299 | output_path = os.path.abspath(output_path) 300 | 301 | # 确保PPTX父目录存在 302 | pptx_dir = os.path.dirname(pptx_path) 303 | if not os.path.exists(pptx_dir): 304 | try: 305 | os.makedirs(pptx_dir, exist_ok=True) 306 | log_error(f"创建目录: {pptx_dir}") 307 | except Exception as e: 308 | error_msg = f"创建目录 {pptx_dir} 时出错: {e}" 309 | log_error(error_msg) 310 | log_error(traceback.format_exc()) 311 | return False, "\n".join(error_log) 312 | 313 | # 如果有输出路径,也确保其父目录存在 314 | if output_path: 315 | output_dir = os.path.dirname(output_path) 316 | if not os.path.exists(output_dir): 317 | try: 318 | os.makedirs(output_dir, exist_ok=True) 319 | log_error(f"创建输出目录: {output_dir}") 320 | except Exception as e: 321 | error_msg = f"创建输出目录 {output_dir} 时出错: {e}" 322 | log_error(error_msg) 323 | log_error(traceback.format_exc()) 324 | return False, "\n".join(error_log) 325 | 326 | # 输入验证并自动创建PPTX(如果需要) 327 | if not os.path.exists(pptx_path): 328 | if create_if_not_exists: 329 | try: 330 | from pptx import Presentation 331 | prs = Presentation() 332 | # 设置为16:9尺寸 333 | prs.slide_width = Inches(16) 334 | prs.slide_height = Inches(9) 335 | 336 | # 直接创建足够多的幻灯片 337 | for i in range(slide_number): 338 | prs.slides.add_slide(prs.slide_layouts[6]) # 6是空白幻灯片 339 | 340 | prs.save(pptx_path) 341 | log_error(f"已重新创建PPTX文件: {pptx_path},包含{slide_number}张幻灯片") 342 | import time 343 | time.sleep(0.5) 344 | # 再次尝试解压 345 | os.makedirs(temp_dir, exist_ok=True) # 创建临时目录 346 | with zipfile.ZipFile(pptx_path, 'r') as zip_ref: 347 | zip_ref.extractall(temp_dir) 348 | except Exception as e: 349 | error_msg = f"创建PPTX文件时出错: {e}" 350 | log_error(error_msg) 351 | log_error(traceback.format_exc()) 352 | return False, "\n".join(error_log) 353 | else: 354 | error_msg = f"PPTX file not found: {pptx_path}" 355 | log_error(error_msg) 356 | return False, "\n".join(error_log) 357 | 358 | # 检查并确保SVG文件的父目录存在 359 | svg_dir = os.path.dirname(svg_path) 360 | if not os.path.exists(svg_dir): 361 | try: 362 | os.makedirs(svg_dir, exist_ok=True) 363 | log_error(f"创建SVG目录: {svg_dir}") 364 | except Exception as e: 365 | error_msg = f"创建SVG目录 {svg_dir} 时出错: {e}" 366 | log_error(error_msg) 367 | log_error(traceback.format_exc()) 368 | return False, "\n".join(error_log) 369 | 370 | # 检查PPTX文件是否至少有一张幻灯片,如果没有则添加一张 371 | try: 372 | from pptx import Presentation 373 | prs = Presentation(pptx_path) 374 | 375 | # 检查指定的slide_number是否超出现有幻灯片数量 376 | if slide_number > len(prs.slides): 377 | log_error(f"幻灯片编号{slide_number}超出现有幻灯片数量{len(prs.slides)},将自动添加缺失的幻灯片") 378 | # 获取空白幻灯片布局 379 | blank_slide_layout = prs.slide_layouts[6] # 6是空白幻灯片 380 | 381 | # 计算需要添加的幻灯片数量 382 | slides_to_add = slide_number - len(prs.slides) 383 | 384 | # 循环添加所需数量的幻灯片 385 | for _ in range(slides_to_add): 386 | prs.slides.add_slide(blank_slide_layout) 387 | log_error(f"已添加新的空白幻灯片,当前幻灯片数量: {len(prs.slides)}") 388 | 389 | # 保存文件 390 | prs.save(pptx_path) 391 | # 给文件写入一些时间 392 | import time 393 | time.sleep(0.5) 394 | elif len(prs.slides) == 0: 395 | log_error(f"PPTX文件 {pptx_path} 没有幻灯片,添加一张空白幻灯片") 396 | blank_slide_layout = prs.slide_layouts[6] # 6是空白幻灯片 397 | slide = prs.slides.add_slide(blank_slide_layout) 398 | prs.save(pptx_path) 399 | # 给文件写入一些时间 400 | import time 401 | time.sleep(0.5) 402 | except Exception as e: 403 | error_msg = f"检查或添加幻灯片时出错: {e}" 404 | log_error(error_msg) 405 | # 如果是无效的PPTX文件,可能是因为文件损坏或不是PPTX格式 406 | if "File is not a zip file" in str(e) or "document not found" in str(e) or "Package not found" in str(e): 407 | log_error(f"PPTX文件 {pptx_path} 似乎不是有效的PowerPoint文件,尝试重新创建") 408 | try: 409 | # 确保目录存在 410 | os.makedirs(os.path.dirname(pptx_path), exist_ok=True) 411 | 412 | # 重新创建一个新的PPTX文件 413 | prs = Presentation() 414 | prs.slide_width = Inches(16) 415 | prs.slide_height = Inches(9) 416 | blank_slide_layout = prs.slide_layouts[6] 417 | 418 | # 直接创建足够多的幻灯片 419 | for i in range(slide_number): 420 | prs.slides.add_slide(blank_slide_layout) 421 | 422 | prs.save(pptx_path) 423 | log_error(f"已重新创建PPTX文件: {pptx_path},包含{slide_number}张幻灯片") 424 | except Exception as e2: 425 | error_msg = f"重新创建PPTX文件时出错: {e2}" 426 | log_error(error_msg) 427 | log_error(traceback.format_exc()) 428 | return False, "\n".join(error_log) 429 | else: 430 | # 其他类型的错误 431 | log_error(traceback.format_exc()) 432 | return False, "\n".join(error_log) 433 | 434 | # 确保文件存在且大小不为0 435 | if not os.path.exists(pptx_path) or os.path.getsize(pptx_path) == 0: 436 | error_msg = f"错误:PPTX文件 {pptx_path} 不存在或大小为0" 437 | log_error(error_msg) 438 | return False, "\n".join(error_log) 439 | 440 | if not os.path.exists(svg_path): 441 | error_msg = f"SVG file not found: {svg_path}" 442 | log_error(error_msg) 443 | return False, "\n".join(error_log) 444 | 445 | # 确定输出路径和创建临时目录 446 | output_path = output_path or pptx_path 447 | # 临时目录已在函数开始部分创建,确保目录存在 448 | os.makedirs(temp_dir, exist_ok=True) 449 | 450 | default_width_emu = None 451 | default_height_emu = None 452 | 453 | try: 454 | # 解压 PPTX 455 | try: 456 | with zipfile.ZipFile(pptx_path, 'r') as zip_ref: 457 | zip_ref.extractall(temp_dir) 458 | except zipfile.BadZipFile as e: 459 | error_msg = f"解压PPTX文件时出错: {e}" 460 | log_error(error_msg) 461 | log_error("尝试重新创建PPTX文件...") 462 | # 创建一个新的PPTX文件并再次尝试 463 | try: 464 | prs = Presentation() 465 | prs.slide_width = Inches(16) 466 | prs.slide_height = Inches(9) 467 | blank_slide_layout = prs.slide_layouts[6] 468 | 469 | # 直接创建足够多的幻灯片 470 | for i in range(slide_number): 471 | prs.slides.add_slide(blank_slide_layout) 472 | 473 | prs.save(pptx_path) 474 | log_error(f"已重新创建PPTX文件: {pptx_path},包含{slide_number}张幻灯片") 475 | import time 476 | time.sleep(0.5) 477 | # 再次尝试解压 478 | with zipfile.ZipFile(pptx_path, 'r') as zip_ref: 479 | zip_ref.extractall(temp_dir) 480 | except Exception as e2: 481 | error_msg = f"重新创建和解压PPTX文件时出错: {e2}" 482 | log_error(error_msg) 483 | log_error(traceback.format_exc()) 484 | return False, "\n".join(error_log) 485 | 486 | # --- 读取 presentation.xml 获取默认幻灯片尺寸 --- 487 | pres_path = os.path.join(temp_dir, "ppt", "presentation.xml") 488 | if os.path.exists(pres_path): 489 | try: 490 | pres_tree = etree.parse(pres_path) 491 | pres_root = pres_tree.getroot() 492 | sldSz = pres_root.find('p:sldSz', namespaces=ns) 493 | if sldSz is not None: 494 | default_width_emu = sldSz.get('cx') 495 | default_height_emu = sldSz.get('cy') 496 | if not default_width_emu or not default_height_emu: 497 | log_error("Warning: Could not read valid cx or cy from presentation.xml. Default size might be incorrect.") 498 | default_width_emu = default_height_emu = None # Reset if invalid 499 | else: 500 | log_error("Warning: element not found in presentation.xml. Cannot determine default slide size.") 501 | except etree.XMLSyntaxError as e: 502 | log_error(f"Warning: Could not parse presentation.xml: {e}. Cannot determine default slide size.") 503 | else: 504 | log_error("Warning: presentation.xml not found. Cannot determine default slide size.") 505 | 506 | # 如果无法获取默认尺寸,提供一个备用值(例如16:9宽屏的EMU值) 507 | if default_width_emu is None or default_height_emu is None: 508 | default_width_emu = "12192000" # 16 inches 509 | default_height_emu = "6858000" # 9 inches 510 | log_error(f"Warning: Using fallback default size: width={default_width_emu}, height={default_height_emu} EMU.") 511 | 512 | # --- 处理和转换图像 --- 513 | base_filename = f"image_{uuid.uuid4().hex}" 514 | media_dir = os.path.join(temp_dir, "ppt", "media") 515 | os.makedirs(media_dir, exist_ok=True) 516 | 517 | svg_filename = f"{base_filename}.svg" 518 | png_filename = f"{base_filename}.png" 519 | svg_target_path = os.path.join(media_dir, svg_filename) 520 | png_target_path = os.path.join(media_dir, png_filename) 521 | 522 | # 复制 SVG 523 | shutil.copy2(svg_path, svg_target_path) 524 | 525 | # 转换 SVG -> PNG (使用reportlab和svglib替代cairosvg) 526 | svg_width_px = None 527 | svg_height_px = None 528 | try: 529 | # 使用svglib和reportlab替代cairosvg 530 | drawing = svg2rlg(svg_path) 531 | renderPM.drawToFile(drawing, png_target_path, fmt="PNG") 532 | # 获取SVG尺寸信息 533 | svg_width_px = drawing.width 534 | svg_height_px = drawing.height 535 | except Exception as e: 536 | error_msg = f"Error converting SVG to PNG using reportlab/svglib: {e}" 537 | log_error(error_msg) 538 | log_error(traceback.format_exc()) 539 | if os.path.exists(png_target_path): os.remove(png_target_path) 540 | if os.path.exists(svg_target_path): os.remove(svg_target_path) 541 | return False, "\n".join(error_log) 542 | 543 | # --- 计算最终尺寸 (EMU) --- 544 | # 位置 545 | x_emu = "0" if x is None else to_emu(x) 546 | y_emu = "0" if y is None else to_emu(y) 547 | 548 | # 宽度 549 | width_emu = default_width_emu if width is None else to_emu(width) 550 | 551 | # 高度 552 | if height is None: 553 | if width is None: # 完全默认,使用幻灯片高度 554 | height_emu = default_height_emu 555 | else: # 指定了宽度,尝试计算高度 556 | if svg_width_px and svg_height_px and svg_width_px > 0 and svg_height_px > 0: 557 | aspect_ratio = svg_height_px / svg_width_px 558 | height_emu_val = int(int(width_emu) * aspect_ratio) 559 | height_emu = str(height_emu_val) 560 | log_error(f"Info: Calculated height based on SVG aspect ratio: {height_emu} EMU") 561 | else: 562 | log_error(f"Warning: Could not determine SVG aspect ratio. Using default height: {default_height_emu} EMU") 563 | height_emu = default_height_emu 564 | else: # 用户指定了高度 565 | height_emu = to_emu(height) 566 | 567 | # --- 修改关系文件 (.rels) --- 568 | rels_path = os.path.join(temp_dir, "ppt", "slides", "_rels", f"slide{slide_number}.xml.rels") 569 | if not os.path.exists(rels_path): 570 | error_msg = f"Error: Relationship file not found for slide {slide_number}: {rels_path}" 571 | log_error(error_msg) 572 | if os.path.exists(png_target_path): os.remove(png_target_path) 573 | if os.path.exists(svg_target_path): os.remove(svg_target_path) 574 | return False, "\n".join(error_log) 575 | 576 | parser = etree.XMLParser(remove_blank_text=True) 577 | rels_tree = etree.parse(rels_path, parser) 578 | rels_root = rels_tree.getroot() 579 | 580 | # 查找最大的现有 rId 581 | max_rid_num = 0 582 | for rel in rels_root.findall('Relationship', namespaces=rels_root.nsmap): 583 | rid = rel.get('Id') 584 | if rid and rid.startswith('rId'): 585 | try: 586 | num = int(rid[3:]) 587 | if num > max_rid_num: 588 | max_rid_num = num 589 | except ValueError: 590 | continue 591 | 592 | # 生成新的 rId 593 | rid_num_svg = max_rid_num + 1 594 | rid_num_png = max_rid_num + 2 595 | rId_svg = f"rId{rid_num_svg}" 596 | rId_png = f"rId{rid_num_png}" 597 | 598 | rel_ns = rels_root.nsmap.get(None) 599 | rel_tag = f"{{{rel_ns}}}Relationship" if rel_ns else "Relationship" 600 | 601 | # 创建 PNG 关系 602 | png_rel = etree.Element( 603 | rel_tag, 604 | Id=rId_png, 605 | Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", 606 | Target=f"../media/{png_filename}" 607 | ) 608 | # 创建 SVG 关系 609 | svg_rel = etree.Element( 610 | rel_tag, 611 | Id=rId_svg, 612 | Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", # 仍然使用 image 类型 613 | Target=f"../media/{svg_filename}" 614 | ) 615 | 616 | rels_root.append(png_rel) 617 | rels_root.append(svg_rel) 618 | 619 | rels_tree.write(rels_path, xml_declaration=True, encoding='UTF-8', standalone="yes") 620 | 621 | # --- 修改幻灯片文件 (slideX.xml) --- 622 | slide_path = os.path.join(temp_dir, "ppt", "slides", f"slide{slide_number}.xml") 623 | if not os.path.exists(slide_path): 624 | error_msg = f"Error: Slide file not found: {slide_path}" 625 | log_error(error_msg) 626 | if os.path.exists(png_target_path): os.remove(png_target_path) 627 | if os.path.exists(svg_target_path): os.remove(svg_target_path) 628 | return False, "\n".join(error_log) 629 | 630 | slide_tree = etree.parse(slide_path, parser) 631 | slide_root = slide_tree.getroot() 632 | 633 | # 查找或创建 spTree 634 | spTree = slide_root.find('.//p:spTree', namespaces=ns) 635 | if spTree is None: 636 | cSld = slide_root.find('p:cSld', namespaces=ns) 637 | if cSld is None: 638 | error_msg = f"Error: Could not find in slide {slide_number}." 639 | log_error(error_msg) 640 | if os.path.exists(png_target_path): os.remove(png_target_path) 641 | if os.path.exists(svg_target_path): os.remove(svg_target_path) 642 | return False, "\n".join(error_log) 643 | spTree = etree.SubElement(cSld, etree.QName(ns['p'], 'spTree')) 644 | max_nv_id = 0 645 | for elem in slide_root.xpath('.//p:cNvPr[@id]|.//p:cNvGrpSpPr[@id]', namespaces=ns): 646 | try: 647 | nv_id = int(elem.get('id')) 648 | if nv_id > max_nv_id: 649 | max_nv_id = nv_id 650 | except (ValueError, TypeError): 651 | continue 652 | group_shape_id = max_nv_id + 1 653 | nvGrpSpPr = etree.SubElement(spTree, etree.QName(ns['p'], 'nvGrpSpPr')) 654 | etree.SubElement(nvGrpSpPr, etree.QName(ns['p'], 'cNvPr'), id=str(group_shape_id), name="") 655 | etree.SubElement(nvGrpSpPr, etree.QName(ns['p'], 'cNvGrpSpPr')) 656 | etree.SubElement(nvGrpSpPr, etree.QName(ns['p'], 'nvPr')) 657 | grpSpPr = etree.SubElement(spTree, etree.QName(ns['p'], 'grpSpPr')) 658 | etree.SubElement(grpSpPr, etree.QName(ns['a'], 'xfrm')) 659 | 660 | max_shape_id = 0 661 | for elem in slide_root.xpath('.//p:cNvPr[@id]|.//p:cNvGrpSpPr[@id]|.//p:cNvSpPr[@id]', namespaces=ns): 662 | try: 663 | shape_id_val = int(elem.get('id')) 664 | if shape_id_val > max_shape_id: 665 | max_shape_id = shape_id_val 666 | except (ValueError, TypeError): 667 | continue 668 | shape_id = max(max_shape_id + 1, 2) # 确保ID至少为2 669 | 670 | # 构建 p:pic 元素 671 | pic = etree.Element(etree.QName(ns['p'], 'pic')) 672 | 673 | # nvPicPr 674 | nvPicPr = etree.SubElement(pic, etree.QName(ns['p'], 'nvPicPr')) 675 | cNvPr = etree.SubElement(nvPicPr, etree.QName(ns['p'], 'cNvPr'), id=str(shape_id), name=f"Vector {shape_id}") # 更新名称 676 | 677 | # 添加 a16:creationId 扩展 678 | extLst_cNvPr = etree.SubElement(cNvPr, etree.QName(ns['a'], 'extLst')) 679 | ext_cNvPr = etree.SubElement(extLst_cNvPr, etree.QName(ns['a'], 'ext'), uri="{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}") 680 | a16_ns = "http://schemas.microsoft.com/office/drawing/2014/main" 681 | etree.register_namespace('a16', a16_ns) 682 | etree.SubElement(ext_cNvPr, etree.QName(a16_ns, 'creationId'), id="{" + str(uuid.uuid4()).upper() + "}") 683 | 684 | cNvPicPr = etree.SubElement(nvPicPr, etree.QName(ns['p'], 'cNvPicPr')) 685 | etree.SubElement(cNvPicPr, etree.QName(ns['a'], 'picLocks'), noChangeAspect="1") 686 | etree.SubElement(nvPicPr, etree.QName(ns['p'], 'nvPr')) 687 | 688 | # blipFill 689 | blipFill = etree.SubElement(pic, etree.QName(ns['p'], 'blipFill')) 690 | blip = etree.SubElement(blipFill, etree.QName(ns['a'], 'blip'), {etree.QName(ns['r'], 'embed'): rId_png}) 691 | 692 | # 添加包含 svgBlip 的扩展列表 693 | extLst_blip = etree.SubElement(blip, etree.QName(ns['a'], 'extLst')) 694 | ext_blip_svg = etree.SubElement(extLst_blip, etree.QName(ns['a'], 'ext'), uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}") 695 | asvg_ns_uri = ns['asvg'] 696 | etree.register_namespace('asvg', asvg_ns_uri) 697 | etree.SubElement(ext_blip_svg, etree.QName(asvg_ns_uri, 'svgBlip'), {etree.QName(ns['r'], 'embed'): rId_svg}) 698 | 699 | stretch = etree.SubElement(blipFill, etree.QName(ns['a'], 'stretch')) 700 | etree.SubElement(stretch, etree.QName(ns['a'], 'fillRect')) 701 | 702 | # spPr (使用最终计算的 EMU 值) 703 | spPr = etree.SubElement(pic, etree.QName(ns['p'], 'spPr')) 704 | xfrm = etree.SubElement(spPr, etree.QName(ns['a'], 'xfrm')) 705 | etree.SubElement(xfrm, etree.QName(ns['a'], 'off'), x=x_emu, y=y_emu) 706 | etree.SubElement(xfrm, etree.QName(ns['a'], 'ext'), cx=width_emu, cy=height_emu) 707 | 708 | prstGeom = etree.SubElement(spPr, etree.QName(ns['a'], 'prstGeom'), prst="rect") 709 | etree.SubElement(prstGeom, etree.QName(ns['a'], 'avLst')) 710 | 711 | # 将 pic 添加到 spTree 712 | spTree.append(pic) 713 | 714 | # 写回 slide 文件 715 | slide_tree.write(slide_path, xml_declaration=True, encoding='UTF-8', standalone="yes") 716 | 717 | # --- 修改 [Content_Types].xml --- 718 | content_types_path = os.path.join(temp_dir, "[Content_Types].xml") 719 | if not os.path.exists(content_types_path): 720 | error_msg = f"Error: [Content_Types].xml not found at {content_types_path}" 721 | log_error(error_msg) 722 | if os.path.exists(png_target_path): os.remove(png_target_path) 723 | if os.path.exists(svg_target_path): os.remove(svg_target_path) 724 | return False, "\n".join(error_log) 725 | 726 | content_types_tree = etree.parse(content_types_path, parser) 727 | content_types_root = content_types_tree.getroot() 728 | ct_ns = content_types_root.nsmap.get(None) 729 | ct_tag = f"{{{ct_ns}}}Default" if ct_ns else "Default" 730 | 731 | # 检查并添加 PNG 和 SVG Content Type (如果不存在) 732 | png_exists = any(default.get('Extension') == 'png' for default in content_types_root.findall(ct_tag, namespaces=content_types_root.nsmap)) 733 | svg_exists = any(default.get('Extension') == 'svg' for default in content_types_root.findall(ct_tag, namespaces=content_types_root.nsmap)) 734 | 735 | added_new_type = False 736 | if not png_exists: 737 | log_error("Info: Adding PNG Content Type to [Content_Types].xml") 738 | png_default = etree.Element(ct_tag, Extension="png", ContentType="image/png") 739 | content_types_root.append(png_default) 740 | added_new_type = True 741 | if not svg_exists: 742 | log_error("Info: Adding SVG Content Type to [Content_Types].xml") 743 | svg_default = etree.Element(ct_tag, Extension="svg", ContentType="image/svg+xml") 744 | content_types_root.append(svg_default) 745 | added_new_type = True 746 | 747 | if added_new_type: 748 | content_types_tree.write(content_types_path, xml_declaration=True, encoding='UTF-8', standalone="yes") 749 | 750 | # --- 重新打包 PPTX --- 751 | if os.path.exists(output_path): 752 | os.remove(output_path) 753 | 754 | with zipfile.ZipFile(output_path, 'w', compression=zipfile.ZIP_DEFLATED) as zip_out: 755 | for root, _, files in os.walk(temp_dir): 756 | for file in files: 757 | file_path = os.path.join(root, file) 758 | arcname = os.path.relpath(file_path, temp_dir) 759 | zip_out.write(file_path, arcname) 760 | 761 | return True, "" 762 | 763 | except FileNotFoundError as e: 764 | error_msg = f"File not found error: {e}" 765 | log_error(error_msg) 766 | log_error(traceback.format_exc()) 767 | return False, "\n".join(error_log) 768 | except etree.XMLSyntaxError as e: 769 | error_msg = f"XML parsing error: {e}" 770 | log_error(error_msg) 771 | log_error(traceback.format_exc()) 772 | return False, "\n".join(error_log) 773 | except Exception as e: 774 | error_msg = f"An unexpected error occurred: {e}" 775 | log_error(error_msg) 776 | log_error(traceback.format_exc()) 777 | return False, "\n".join(error_log) 778 | 779 | finally: 780 | # 清理临时目录 781 | try: 782 | shutil.rmtree(temp_dir, ignore_errors=True) 783 | except Exception as e: 784 | log_error(f"清理临时目录时出错: {e}") 785 | 786 | def get_pptx_slide_count(pptx_path: str) -> Tuple[int, str]: 787 | """ 788 | 获取PPTX文件中的幻灯片数量。 789 | 790 | Args: 791 | pptx_path: PPTX文件路径 792 | 793 | Returns: 794 | Tuple[int, str]: 返回(幻灯片数量, 错误信息)的元组。 795 | 如果成功,错误信息为空字符串。 796 | 如果失败,幻灯片数量为0,错误信息包含详细错误。 797 | """ 798 | error_message = "" 799 | 800 | try: 801 | # 规范化路径 802 | pptx_path = normalize_path(pptx_path) 803 | 804 | # 检查文件是否存在 805 | if not os.path.exists(pptx_path): 806 | return 0, f"文件不存在: {pptx_path}" 807 | 808 | # 使用python-pptx库打开文件并获取幻灯片数量 809 | from pptx import Presentation 810 | prs = Presentation(pptx_path) 811 | return len(prs.slides), "" 812 | 813 | except Exception as e: 814 | error_trace = traceback.format_exc() 815 | error_message = f"获取幻灯片数量时出错: {str(e)}\n{error_trace}" 816 | return 0, error_message 817 | 818 | # --- 测试代码 --- 819 | if __name__ == '__main__': 820 | import datetime 821 | 822 | # 测试用例:使用用户提供的 svg_test.pptx 和 media 目录下的 SVG 823 | input_pptx = "svg_test.pptx" 824 | 825 | # 模拟pptx_path为空的情况 826 | test_empty_path = False # 将此设为True来测试空路径处理 827 | if test_empty_path: 828 | print("\n--- 测试空路径处理 ---") 829 | input_pptx = "" # 模拟空路径 830 | 831 | # 检查pptx_path是否为空,如果为空则创建默认路径 832 | if not input_pptx or input_pptx.strip() == "": 833 | # 创建一个基于时间戳的默认文件名 834 | timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 835 | input_pptx = f"presentation_{timestamp}.pptx" 836 | print(f"未提供PPTX路径,将使用默认路径: {input_pptx}") 837 | 838 | # 确保测试文件存在 839 | if not os.path.exists(input_pptx): 840 | print(f"Error: Test input file '{input_pptx}' not found in the workspace root.") 841 | # 可选:如果需要,可以自动创建 842 | from pptx import Presentation 843 | prs = Presentation() 844 | # 设置为16:9尺寸以便与默认备用尺寸匹配 845 | prs.slide_width = Inches(16) 846 | prs.slide_height = Inches(9) 847 | # 添加一张幻灯片,确保slide1.xml和相关关系文件存在 848 | blank_slide_layout = prs.slide_layouts[6] # 6是空白幻灯片 849 | slide = prs.slides.add_slide(blank_slide_layout) 850 | prs.save(input_pptx) 851 | print(f"Created dummy file: {input_pptx} (16:9) with one slide") 852 | 853 | svg_to_insert = "image2.svg" # 修改为当前目录下的SVG文件 854 | if not os.path.exists(svg_to_insert): 855 | print(f"Error: Test SVG file not found: {svg_to_insert}") 856 | # 可以在这里添加创建虚拟 SVG 的逻辑(如果需要) 857 | svg_content = ( 858 | '\n' 859 | ' \n' 860 | ' Test SVG\n' 861 | '' 862 | ) 863 | try: 864 | with open(svg_to_insert, "w") as f: 865 | f.write(svg_content) 866 | print(f"Created dummy SVG file for testing: {svg_to_insert}") 867 | except Exception as e: 868 | print(f"Could not create dummy SVG: {e}") 869 | 870 | # --- 测试 1:指定尺寸和位置 --- (保持不变) 871 | output_pptx_specific = "svg_test_output_specific.pptx" 872 | print(f"\n--- Test 1: Inserting with specific size and position ---") 873 | if os.path.exists(input_pptx) and os.path.exists(svg_to_insert): 874 | success1, error_details1 = insert_svg_to_pptx( 875 | pptx_path=input_pptx, 876 | svg_path=svg_to_insert, 877 | slide_number=3, 878 | x=Inches(1), 879 | y=Inches(1), 880 | width=Inches(4), 881 | height=Inches(3), 882 | output_path=output_pptx_specific 883 | ) 884 | if success1: 885 | print(f"SVG inserted with specific size/pos successfully into '{output_pptx_specific}'") 886 | else: 887 | print("Failed to insert SVG with specific size/pos.") 888 | print(error_details1) 889 | else: 890 | print("Skipping specific size test due to missing input files.") 891 | 892 | # --- 测试 2:默认全屏插入 --- (新增) 893 | output_pptx_fullscreen = "svg_test_output_fullscreen.pptx" 894 | print(f"\n--- Test 2: Inserting with default full-screen size ---") 895 | if os.path.exists(input_pptx) and os.path.exists(svg_to_insert): 896 | success2, error_details2 = insert_svg_to_pptx( 897 | pptx_path=input_pptx, 898 | svg_path=svg_to_insert, 899 | slide_number=1, 900 | # x, y, width, height 使用默认值 (None) 901 | output_path=output_pptx_fullscreen 902 | ) 903 | if success2: 904 | print(f"SVG inserted with default full-screen successfully into '{output_pptx_fullscreen}'") 905 | else: 906 | print("Failed to insert SVG with default full-screen.") 907 | print(error_details2) 908 | else: 909 | print("Skipping full-screen test due to missing input files.") 910 | 911 | # --- 测试 3:只指定宽度,高度自动计算 --- (新增) 912 | output_pptx_autoheight = "svg_test_output_autoheight.pptx" 913 | print(f"\n--- Test 3: Inserting with specific width, auto height ---") 914 | if os.path.exists(input_pptx) and os.path.exists(svg_to_insert): 915 | success3, error_details3 = insert_svg_to_pptx( 916 | pptx_path=input_pptx, 917 | svg_path=svg_to_insert, 918 | slide_number=1, 919 | x=Inches(0.5), 920 | y=Inches(0.5), 921 | width=Inches(5), # 指定宽度 922 | # height 使用默认值 (None) 923 | output_path=output_pptx_autoheight 924 | ) 925 | if success3: 926 | print(f"SVG inserted with auto height successfully into '{output_pptx_autoheight}'") 927 | else: 928 | print("Failed to insert SVG with auto height.") 929 | print(error_details3) 930 | else: 931 | print("Skipping auto height test due to missing input files.") --------------------------------------------------------------------------------