Skill: zhangjilei123-1/auto-plantuml-edit (version 1.0.0)

--- name: AutoPlantUMLEdit description: Convert natural language to UML diagrams, export editable PPT/EMF with individually editable shapes in PowerPoint. --- # AutoPlantUMLEdit Auto-generate UML diagrams from natural language, one-click export to editable PPT/EMF with individually editable shapes in PowerPoint. ## Core Capabilities - **Natural Language → PlantUML**: Generate PlantUML syntax from user requirements - **One-click Export**: SVG → EMF → PPT, supports SVG/PNG/EMF/PPT formats - **Fully Editable**: EMF/PPT can be ungrouped in PowerPoint for independent shape editing ## Trigger Conditions Activate when user expresses: - "Generate a... sequence/class/flowchart diagram" - "Draw me a... architecture diagram" - "Create a... state diagram" - "I want to see... UML diagram" ## Usage Flow 1. Generate PlantUML syntax based on user requirements 2. Save as `{name}.puml` file 3. Run script to generate output ## Script Commands ```bash python svg2emf.py <input.puml> [-f ppt|svg|png|emf] [-o <output>] ``` **Parameters:** | Parameter | Description | Default | |-----------|-------------|---------| | `<input.puml>` | PlantUML source file | Required | | `-f` | Output format | `ppt` | | `-o` | Output file path | Same name .pptx | **Examples:** ```bash # Generate PPT (default) python svg2emf.py sequence.puml # Export SVG python svg2emf.py sequence.puml -f svg # Export PNG python svg2emf.py sequence.puml -f png # Export EMF (editable) python svg2emf.py sequence.puml -f emf # Specify output path python svg2emf.py sequence.puml -o my_diagram.pptx ``` ## PlantUML Syntax Reference ### Sequence Diagram ```plantuml @startuml participant Alice as A participant Bob as B A -> B : message B --> A : response @enduml ``` ### Class Diagram ```plantuml @startuml class User { +name: String +login() } class Order { +items: List +calculate() } User "1" -- "*" Order : places @enduml ``` ### Activity Diagram (Flowchart) ```plantuml @startuml start :Login system; if (Password correct?) then (Yes) :Go to home; else (No) :Show error; stop endif stop @enduml ``` ### State Diagram ```plantuml @startuml [*] --> Waiting Waiting --> Processing: Start Processing --> Completed: Success Processing --> Failed: Error Completed --> [*] Failed --> [*] @enduml ``` ### Use Case Diagram ```plantuml @startuml left to right direction actor User as U rectangle System { U -- (Login) U -- (Place Order) U -- (Query Order) } @enduml ``` ## Output Format Guide | Format | Description | PowerPoint Editable | |--------|------------|---------------------| | `.pptx` | Slide with embedded EMF | ✅ Ungroup to edit | | `.emf` | Enhanced Metafile | ✅ Ungroup to edit | | `.svg` | Vector image | ❌ Needs conversion | | `.png` | Raster image | ❌ Not editable | **Recommended Workflow:** 1. Generate `.pptx` format 2. Select the image in PowerPoint 3. Right-click → `Group` → `Ungroup` (may need to ungroup multiple times to fully separate all elements) 4. Edit each element independently ## ⚠️ Important Notes **After generating PPT, you MUST tell the user the following:** > "PPT generated! To edit each shape: Select the image → Right-click → Group → Ungroup. **Note: You may need to ungroup multiple times (Ctrl+Shift+G) to fully separate all elements and ensure each shape can be edited independently.**" ## Notes 1. Avoid Chinese characters, spaces, and special characters in `.puml` filenames 2. PlantUML syntax requires matching `@startuml` and `@enduml` 3. Output files are saved in the current working directory 4. **Inkscape and JDK must be installed and configured in PATH** (see dependency section below) 5. **Use system Python to run the script**: Ensure dependencies are installed via `pip install -r requirements.txt` ## ⚠️ Swimlane/Participant Names Must Be in English Due to PlantUML encoding limitations, swimlane names and participant names must be in English. Activity descriptions and messages should use the user's language. ```plantuml @startuml ecommerce_flow |Customer| start :Browse Products; :Place Order; |OrderService| :Process Order; @enduml ``` **Rules:** - **Swimlane/Participant names**: Must be in English (e.g., Customer, OrderService) - **Activity descriptions/messages**: Use user's language (if user speaks Chinese, use Chinese "浏览商品"; if English, use "Browse Products") - **State names**: Use English (e.g., Active, Pending, Completed) **Reason:** PlantUML has an encoding bug when handling non-English swimlane/participant names, producing SVGs with corrupted HTML entity references. This is a PlantUML limitation that cannot be resolved via configuration. ## Dependency Instructions ### External Dependencies Check **Before using this skill, verify the following dependencies are installed:** #### 1. Inkscape (Vector Conversion) Inkscape is used to convert SVG to EMF format, it's the core dependency. **Check if installed:** ```bash # Run in command line inkscape --version ``` **If not installed, follow these steps:** 1. Visit Inkscape download page: https://inkscape.org/release/ 2. Download Windows installer (`.exe` or `.msi`) 3. Run the installer, **make sure to check "Add Inkscape to PATH"** during installation 4. After installation, reopen command line window and verify: ```bash inkscape --version ``` **Version requirement:** Inkscape 1.0 or higher **Important:** If not added to PATH during installation, you can add manually: - Add `C:\Program Files\Inkscape\bin` to system PATH environment variable #### 2. OpenJDK (Run PlantUML) **Check if installed:** ```bash java --version ``` **If not installed, follow these steps:** 1. Visit Adoptium download page: https://adoptium.net/zh-CN/temurin/releases?version=8 2. Download Windows x64 JDK 8 (.msi or .exe) 3. Run the installer 4. Configure environment variables: | Variable | Value | |----------|-------| | `JAVA_HOME` | `C:\Program Files\Eclipse Adoptium\jdk-8.x.x.x-hotspot` (adjust to actual install path) | | `PATH` (append) | `%JAVA_HOME%\bin` | 5. Verify installation: ```bash java --version ``` ### Python Dependencies Install via pip: ```bash pip install -r requirements.txt ``` **requirements.txt contents:** - pillow - python-pptx ## PlantUML Jar Download **Important:** If `scripts/plantuml.jar` does not exist, the AI model should download it automatically. **Download command:** ```bash # Navigate to AutoPlantUMLEdit skill directory first curl -L -o "scripts/plantuml.jar" "https://github.com/plantuml/plantuml/releases/download/v1.2026.2/plantuml-1.2026.2.jar" ``` **Or using PowerShell:** ```powershell # Navigate to AutoPlantUMLEdit skill directory first Invoke-WebRequest -Uri "https://github.com/plantuml/plantuml/releases/download/v1.2026.2/plantuml-1.2026.2.jar" -OutFile "scripts/plantuml.jar" ``` **Verify after download:** ```bash java -jar scripts/plantuml.jar -version ```

path: SKILL-cn.md
content: ---
name: AutoPlantUMLEdit
description: 将自然语言描述转换为 UML 图表，一键导出可编辑的 PPT/EMF，PowerPoint 中可单独编辑每个形状。
---

# AutoPlantUMLEdit

自动生成 UML 图表，一键导出可编辑的 PPT/EMF，支持 PowerPoint 直接编辑每个形状。

## 核心能力

- **自然语言 → PlantUML**：大模型根据用户需求生成 PlantUML 语法
- **一键导出**：生成 SVG → EMF → PPT，支持 SVG/PNG/EMF/PPT 四种格式
- **完全可编辑**：EMF/PPT 导入 PowerPoint 后可取消组合，每个形状独立编辑

## 触发条件

用户表达以下意图时激活：

- "生成一个...时序图/类图/流程图"
- "帮我画一个...架构图"
- "创建一个...状态图"
- "我想看...的 UML 图"

## 使用流程

1. 根据用户需求生成 PlantUML 语法
2. 保存为 `{name}.puml` 文件
3. 调用脚本生成输出

## 脚本命令

```bash
python svg2emf.py <input.puml> [-f ppt|svg|png|emf] [-o <output>]
```

**参数说明：**

| 参数 | 说明 | 默认值 |
|------|------|--------|
| `<input.puml>` | PlantUML 源文件 | 必填 |
| `-f` | 输出格式 | `ppt` |
| `-o` | 输出文件路径 | 同名 .pptx |

**示例：**

```bash
# 生成 PPT（默认）
python svg2emf.py sequence.puml

# 导出 SVG
python svg2emf.py sequence.puml -f svg

# 导出 PNG
python svg2emf.py sequence.puml -f png

# 导出 EMF（可编辑）
python svg2emf.py sequence.puml -f emf

# 指定输出路径
python svg2emf.py sequence.puml -o my_diagram.pptx
```

## PlantUML 语法速查

### 时序图 (Sequence Diagram)

```plantuml
@startuml
participant Alice as A
participant Bob as B
A -> B : message
B --> A : response
@enduml
```

### 类图 (Class Diagram)

```plantuml
@startuml
class User {
  +name: String
  +login()
}
class Order {
  +items: List
  +calculate()
}
User "1" -- "*" Order : places
@enduml
```

### 流程图 (Activity Diagram)

```plantuml
@startuml
start
:登录系统;
if (密码正确?) then (是)
  :进入首页;
else (否)
  :提示错误;
  stop
endif
stop
@enduml
```

### 状态图 (State Diagram)

```plantuml
@startuml
[*] --> 等待中
等待中 --> 处理中: 开始
处理中 --> 已完成: 成功
处理中 --> 失败: 错误
已完成 --> [*]
失败 --> [*]
@enduml
```

### 用例图 (Use Case)

```plantuml
@startuml
left to right direction
actor 用户 as U
rectangle 系统 {
  U -- (登录)
  U -- (下单)
  U -- (查询订单)
}
@enduml
```

## 输出格式说明

| 格式 | 说明 | PowerPoint 可编辑 |
|------|------|------------------|
| `.pptx` | 嵌入 EMF 的幻灯片 | ✅ 取消组合后可编辑 |
| `.emf` | 增强图元文件 | ✅ 取消组合后可编辑 |
| `.svg` | 矢量图 | ❌ 需转换 |
| `.png` | 位图 | ❌ 不可编辑 |

**推荐工作流：**
1. 生成 `.pptx` 格式
2. 在 PowerPoint 中选中图片
3. 右键 → `组合` → `取消组合`（可能需要多次取消组合才能完全拆分每个元素）
4. 每个元素独立编辑

## ⚠️ 重要提示

**生成 PPT 后，大模型必须告知用户以下操作步骤：**

> 生成完成后，告知用户："PPT 已生成！如需编辑各个形状，请执行：选中图片 → 右键 → 组合 → 取消组合。**注意：可能需要多次取消组合（Ctrl+Shift+G）才能将所有元素完全拆分，确保每个形状都可以独立编辑。**"

## 注意事项

1. `.puml` 文件名避免使用中文、空格、特殊字符
2. PlantUML 语法严格，`@startuml` 和 `@enduml` 必须配对
3. 输出文件默认保存在当前工作目录
4. **必须先安装 Inkscape 和 JDK 并配置环境变量**（见上方依赖说明）
5. **使用系统 Python 运行脚本**：确保已安装依赖 `pip install -r requirements.txt`

## ⚠️ 泳道名/参与者名必须使用英文

由于 PlantUML 的编码限制，泳道名和参与者名必须使用英文。活动描述、消息文字等使用用户语言。

```plantuml
@startuml ecommerce_flow

|Customer|
start
:Browse Products;
:Place Order;
|OrderService|
:Process Order;

@enduml
```

**规则：**
- **泳道名/参与者名**：必须使用英文（如 Customer、OrderService）
- **活动描述/消息**：使用用户语言（如用户说中文就用中文"浏览商品"，说英文就用英文"Browse Products"）
- **状态名称**：使用英文（如 Active、Pending、Completed）

**原因：** PlantUML 在处理非英文泳道名/参与者名时存在编码 bug，生成的 SVG 包含损坏的 HTML 实体引用。这是 PlantUML 本身的限制，无法通过配置解决。

## 依赖说明

### 外部依赖检查

**使用本 skill 前，请先检查以下依赖是否已安装：**

#### 1. Inkscape（矢量图转换）

Inkscape 用于将 SVG 转换为 EMF 格式，是核心依赖。

**检查是否已安装：**

```bash
# 在命令行中运行
inkscape --version
```

**如果未安装，请按以下步骤安装：**

1. 访问 Inkscape 官网下载页面：https://inkscape.org/release/
2. 下载 Windows 安装包（`.exe` 或 `.msi`）
3. 运行安装程序，**安装时注意勾选"Add Inkscape to PATH"**（添加到系统环境变量）
4. 安装完成后，重新打开命令行窗口验证：

```bash
inkscape --version
```

**版本要求：** Inkscape 1.0 或更高版本

**重要：** 如果安装时没有添加到 PATH，可以手动添加：
- 将 `C:\Program Files\Inkscape\bin` 添加到系统环境变量 PATH

#### 2. OpenJDK（运行 PlantUML）

**检查是否已安装：**

```bash
java --version
```

**如果未安装，请按以下步骤安装：**

1. 访问 Adoptium 下载页面：https://adoptium.net/zh-CN/temurin/releases?version=8
2. 下载 Windows x64 JDK 8 (.msi 或 .exe)
3. 运行安装程序
4. 配置环境变量：

| 变量名 | 值 |
|--------|-----|
| `JAVA_HOME` | `C:\Program Files\Eclipse Adoptium\jdk-8.x.x.x-hotspot`（根据实际安装路径） |
| `PATH`（追加） | `%JAVA_HOME%\bin` |

5. 验证安装：

```bash
java --version
```

### Python 依赖

通过 pip 安装：

```bash
pip install -r requirements.txt
```

**requirements.txt 内容：**
- pillow
- python-pptx

## PlantUML Jar 下载

**重要：** 如果 `scripts/plantuml.jar` 文件不存在，大模型需要自动下载。

**下载命令：**

```bash
# 切换到 AutoPlantUMLEdit skill 目录后执行
curl -L -o "scripts/plantuml.jar" "https://github.com/plantuml/plantuml/releases/download/v1.2026.2/plantuml-1.2026.2.jar"
```

**或使用 PowerShell：**

```powershell
# 切换到 AutoPlantUMLEdit skill 目录后执行
Invoke-WebRequest -Uri "https://github.com/plantuml/plantuml/releases/download/v1.2026.2/plantuml-1.2026.2.jar" -OutFile "scripts/plantuml.jar"
```

**下载后验证：**

```bash
java -jar scripts/plantuml.jar -version
```


path: requirements.txt
content: pillow>=12.0.0
python-pptx>=1.0.0


path: scripts/svg2emf.py
content: """
PlantUML to SVG/EMF/PPT converter using Inkscape CLI.

Usage:
    python svg2emf.py input.puml [-f ppt|emf|svg|png] [-o output]

Requirements:
    - Inkscape installed
    - Java for PlantUML
    - python-pptx (for PPT output)
"""

import os
import sys
import struct
import subprocess
import shutil

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
INKSCAPE_PATH = os.path.join(SCRIPT_DIR, "inkscape", "bin", "inkscape.exe")
PLANTUML_JAR = os.path.join(SCRIPT_DIR, "plantuml.jar")

EMBEDDED_JAVA_PATH = os.path.join(SCRIPT_DIR, "jdk", "jdk8u482-b08", "bin", "java.exe")

def find_java():
    """Find Java: JAVA_HOME > PATH > embedded JDK"""
    java_home = os.environ.get("JAVA_HOME")
    if java_home:
        java_exe = os.path.join(java_home, "bin", "java.exe")
        if os.path.exists(java_exe):
            return java_exe
    # Try PATH
    from shutil import which
    java_in_path = which("java")
    if java_in_path:
        return java_in_path
    # Fall back to embedded JDK
    if os.path.exists(EMBEDDED_JAVA_PATH):
        return EMBEDDED_JAVA_PATH
    return None

def find_inkscape():
    if os.path.exists(INKSCAPE_PATH):
        return INKSCAPE_PATH
    env_path = os.environ.get("INKSCAPE_PATH")
    if env_path and os.path.exists(env_path):
        return env_path
    common_paths = [
        r"C:\Program Files\Inkscape\bin\inkscape.exe",
        r"C:\Program Files (x86)\Inkscape\bin\inkscape.exe",
    ]
    for path in common_paths:
        if os.path.exists(path):
            return path
    if shutil.which("inkscape"):
        return "inkscape"
    return None


def puml_to_svg(puml_file):
    """PlantUML -> SVG"""
    print("[1/2] Generating SVG via PlantUML...")
    java_path = find_java()
    if not java_path:
        raise RuntimeError(
            "Java not found. Please install JDK 8 and set JAVA_HOME environment variable.\n"
            "Download: https://adoptium.net/zh-CN/temurin/releases?version=8"
        )
    result = subprocess.run(
        [java_path, "-jar", PLANTUML_JAR, "-charset", "UTF-8", "-tsvg", puml_file, "-o", os.path.dirname(puml_file) or "."],
        capture_output=True, text=True
    )
    if result.returncode != 0:
        raise RuntimeError(f"PlantUML failed: {result.stderr}")

    base = os.path.splitext(puml_file)[0]
    svg_out = os.path.join(os.path.dirname(puml_file) or ".", f"{os.path.basename(base)}.svg")
    if not os.path.exists(svg_out):
        raise RuntimeError(f"SVG not created: {svg_out}")

    # Fix numeric character references to UTF-8 characters for proper font rendering
    import re
    with open(svg_out, 'r', encoding='utf-8') as f:
        content = f.read()
    # Replace numeric character references (e.g., &#27979;) with actual UTF-8 chars
    fixed = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), content)
    fixed = fixed.replace('encoding="us-ascii"', 'encoding="UTF-8"')
    with open(svg_out, 'w', encoding='utf-8') as f:
        f.write(fixed)

    return svg_out


def svg_to_emf(svg_file):
    """SVG -> EMF with ungroup via Inkscape"""
    print("[2/2] Converting to EMF via Inkscape (with ungroup)...")
    inkscape = find_inkscape()
    if not inkscape:
        raise RuntimeError("Inkscape not found")

    base = os.path.splitext(svg_file)[0]
    emf_out = f"{base}.emf"

    # Use text-to-path to convert text to vector paths, avoiding font dependency issues
    result = subprocess.run(
        [inkscape, svg_file,
         "--actions", "select-all;selection-ungroup;selection-ungroup;selection-ungroup",
         "--export-text-to-path",
         "--export-filename", emf_out,
         "--export-type", "emf"],
        capture_output=True, text=True
    )
    if result.returncode != 0:
        raise RuntimeError(f"Inkscape failed: {result.stderr}")

    if not os.path.exists(emf_out):
        raise RuntimeError(f"EMF not created: {emf_out}")
    return emf_out


def svg_to_png(svg_file):
    """SVG -> PNG via Inkscape"""
    print("[2/2] Converting to PNG via Inkscape...")
    inkscape = find_inkscape()
    if not inkscape:
        raise RuntimeError("Inkscape not found")

    base = os.path.splitext(svg_file)[0]
    png_out = f"{base}.png"

    result = subprocess.run(
        [inkscape, svg_file,
         "--export-filename", png_out,
         "--export-type", "png"],
        capture_output=True, text=True
    )
    if result.returncode != 0:
        raise RuntimeError(f"Inkscape failed: {result.stderr}")

    if not os.path.exists(png_out):
        raise RuntimeError(f"PNG not created: {png_out}")
    return png_out


def get_emf_bounds_himetric(emf_file):
    """Parse EMF header to get bounds in HIMETRIC units."""
    try:
        with open(emf_file, 'rb') as f:
            data = f.read(88)
            if len(data) < 88:
                return None
            left, top, right, bottom = struct.unpack('<IIII', data[24:40])
            return right - left, bottom - top
    except:
        return None


def create_ppt_with_emf(emf_file, pptx_file):
    """Create a PPT and insert the EMF file with proper sizing."""
    try:
        from pptx import Presentation
        from pptx.util import Inches
        from PIL import Image
    except ImportError:
        raise RuntimeError("python-pptx not installed")

    # Disable PIL decompression bomb check (EMF stores bounds in HIMETRIC, not pixels)
    Image.MAX_IMAGE_PIXELS = None

    print("[3/3] Creating PPT...")

    # Parse EMF bounds to calculate correct size
    # HIMETRIC units: 1 unit = 0.01 mm
    # 1 inch = 25.4 mm = 2540 HIMETRIC units
    himetric_per_inch = 2540

    bounds = get_emf_bounds_himetric(emf_file)
    if bounds:
        width_him, height_him = bounds
        width_inch = width_him / himetric_per_inch
        height_inch = height_him / himetric_per_inch
        print(f"  EMF size: {width_inch:.2f} x {height_inch:.2f} inches")
    else:
        width_inch = 8
        height_inch = 6

    prs = Presentation()
    prs.slide_width = Inches(10)
    prs.slide_height = Inches(7.5)

    slide = prs.slides.add_slide(prs.slide_layouts[6])  # Blank layout

    # Calculate margins
    margin = Inches(0.5)
    available_width = prs.slide_width - margin * 2
    available_height = prs.slide_height - margin * 2

    # Scale to fit within slide while maintaining aspect ratio
    if bounds:
        aspect_ratio = width_inch / height_inch
        available_aspect = available_width / available_height

        if aspect_ratio > available_aspect:
            emf_width = available_width
            emf_height = int(available_width / aspect_ratio)
        else:
            emf_height = available_height
            emf_width = int(available_height * aspect_ratio)
    else:
        emf_width = available_width
        emf_height = Inches(6)

    left = (prs.slide_width - emf_width) // 2
    top = (prs.slide_height - emf_height) // 2

    slide.shapes.add_picture(emf_file, left, top, width=emf_width)

    prs.save(pptx_file)
    print(f"[OK] PPT saved: {pptx_file}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python svg2emf.py input.puml [-f ppt|emf|svg|png] [-o output]")
        print("  -f: output format, default: ppt")
        print("  -o: output file path (optional)")
        sys.exit(1)

    puml_file = sys.argv[1]
    if not puml_file.lower().endswith(".puml"):
        print("Error: only .puml files supported")
        sys.exit(1)

    # Parse arguments
    fmt = "ppt"
    output_path = None
    i = 2
    while i < len(sys.argv):
        if sys.argv[i] == "-f" and i + 1 < len(sys.argv):
            fmt = sys.argv[i + 1].lower()
            i += 2
        elif sys.argv[i] == "-o" and i + 1 < len(sys.argv):
            output_path = sys.argv[i + 1]
            i += 2
        else:
            i += 1

    if fmt not in ("ppt", "emf", "svg", "png"):
        print("Error: format must be ppt, emf, svg, or png")
        sys.exit(1)

    base = os.path.splitext(puml_file)[0]

    if fmt == "svg":
        svg_file = puml_to_svg(puml_file)
        print(f"[OK] SVG generated: {svg_file}")
    elif fmt == "png":
        svg_file = puml_to_svg(puml_file)
        png_file = svg_to_png(svg_file)
        print(f"[OK] PNG generated: {png_file}")
    elif fmt == "emf":
        svg_file = puml_to_svg(puml_file)
        emf_file = svg_to_emf(svg_file)
        print(f"[OK] EMF generated: {emf_file}")
    else:  # ppt
        svg_file = puml_to_svg(puml_file)
        emf_file = svg_to_emf(svg_file)
        pptx_file = output_path or f"{base}.pptx"
        create_ppt_with_emf(emf_file, pptx_file)


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)