Back to skills
extension
Category: OtherNo API key required

PPT 视觉复刻

将PPT截图或信息图转换为可编辑的PPTX文件。此技能基于Images2Slides论文(arXiv:2602.07645)实现, 利用视觉-语言模型(VLM)进行区域理解,通过坐标映射算法将像素坐标转换为PPTX坐标。 支持复杂形状降级处理策略(custGeom/渐变/透明等无法直接还原的形状转为PNG嵌入)。...

personAuthor: allergrohubclawhub

⚠️ 使用前须知

  • 图片数据发送:第一步 VLM 图像分析会将截图发送到配置的 VLM 服务端点(如 OpenClaw image 工具 / MiniMax Vision / GPT-4V 等)。请勿使用本技能处理涉密或敏感的幻灯片。
  • 文件系统:技能会生成临时 PNG 文件和输出 PPTX 文件,确保运行环境允许文件读写操作。
  • 运行时依赖:需提前安装 npm install -g pptxgenjs;如需复杂形状降级,还需安装 Python + Pillow。

PPT视觉复刻技能 (v1.5)

将静态PPT截图/信息图转换为可编辑PPTX文件的完整工作流。

核心算法 (Images2Slides论文适配)

坐标映射 (Pixel → EMU)

EMU = Pixel × (914400 / DPI)
Slide_X = (Image_X / Image_Width) × Slide_Width_EMU
Slide_Y = (Image_Y / Image_Height) × Slide_Height_EMU

标准16:9幻灯片尺寸: 9144000 × 5143500 EMU

工作流程

阶段1: 图像分析(开放式 VLM 调用)

第一步的图片识别为开放式设计,可灵活接入任意具备图像理解能力的工具—— 如 OpenClaw 内置 image 工具(默认,MiniMax Vision VLM)、MCP VLM 服务、GPT-4V 等。 后续坐标映射和 PPT 生成逻辑与具体 VLM 解耦,保持稳定。

必须使用以下增强版 Prompt,以确保符号精度和格式完整性:

const ENHANCED_PROMPT = `分析这张PPT截图,精确输出所有可见区域的信息。

输出格式为JSON数组,每个元素包含:
{
  "id": "数字",
  "type": "rectangle | roundedRect | text | line | image",
  "role": "header_bar | title | subtitle | body | data_card | footer | decorator | bullet_list",
  "bounds": { "x": 像素, "y": 像素, "width": 像素, "height": 像素 },
  "color": "#十六进制颜色(文字颜色或形状填充色)",
  "backgroundColor": "#背景填充色(仅形状适用)",
  "lineColor": "#边框颜色(如有边框)",
  "lineWidth": 边框宽度,
  "content": "文字内容(保持原版的所有标点符号,全角/半角符号原样保留)",
  "fontSize": 字号数字(磅),
  "fontFamily": "字体名称",
  "fontWeight": "normal | bold",
  "align": "left | center | right",
  "lineSpacing": 行间距倍数(1.0/1.5/2.0),
  "charSpacing": 字符间距(磅),
  "bulletType": "无bullet则null,有bullet则填 ■ / ✓ / - 等符号",
  "cornerRadius": 圆角大小(仅roundedRect适用),
  "zIndex": 叠加层级(从0开始)
}

⚠️ 特别注意:
1. content 必须保持原版的所有标点符号,包括全角括号()和半角括号()的区别
2. 不要添加或删除任何字符
3. 项目符号(■、✓、1.、-)必须原样记录在 bulletType 字段
4. 识别所有行间距较大的文本框,填写 lineSpacing
5. 边框矩形必须填写 lineColor,填充矩形填写 backgroundColor
6. zIndex按照视觉层叠顺序从后到前递增`;

阶段2: 结构化数据解析

调用 scripts/coordinate_mapper.js,支持4种JSON提取策略,自动容错:

const { parseAnalysisToRegions, mapAllRegions } = require('./coordinate_mapper');

// 解析分析结果(自动处理全角符号校正和bullet识别)
const regions = parseAnalysisToRegions(analysisText);

// 批量映射坐标
const mappedRegions = mapAllRegions(regions, imageWidth, imageHeight);

新增功能(v1.1):

  • normalizeSymbols(text) — 全角/半角符号校正
  • detectBullet(text) — 识别并提取bullet符号类型
  • 4策略JSON解析容错(直接解析 → 数组提取 → 对象提取 → 代码块提取)

阶段3: PPT生成

调用 scripts/ppt_generator.js

const { saveFromRegions } = require('./ppt_generator');

await saveFromRegions(mappedRegions, 'output.pptx', {
  imageWidth: 2100,
  imageHeight: 1192,
  background: '#FFFFFF'  // 可选背景色
});

区域类型映射

| VLM输出 | PptxGenJS方法 | v1.1新增支持 | |--------|--------------|------------| | rectangle (filled) | slide.addShape('rect') | backgroundColor / lineColor | | rectangle (border) | slide.addShape('rect') | lineWidth / dashType | | roundedRect | slide.addShape('roundRect') | ✅ 新增 cornerRadius | | text | slide.addText() | ✅ lineSpacing / charSpacing / bullet | | line | slide.addShape('line') | dashType | | image | slide.addImage() | data(base64) / path | | 复杂形状 | slide.addImage() + Python绘制PNG | ✅ v1.2新增 |


复杂形状降级处理策略 (v1.2)

核心思路:pptxgenjs 无法还原的复杂图形,改用 Python/Pillow 绘制精确 PNG(透明背景)→ 嵌入 PPT 的方式替代。

何时触发降级

遇到以下情况时,应将该形状标记为 type: "complex_shape",使用降级策略处理:

| 形状特征 | 判断依据 | |---------|---------| | 自定义多边形路径 | XML中存在 <a:custGeom> 标签,包含贝塞尔曲线、不规则路径 | | 透明渐变填充 | 多色渐变(3个以上 stop)或带透明度通道(alpha < 100%)的渐变 | | 复合渐变方向 | 渐变角度非水平/垂直(非0/90/180/270度),如斜向渐变 | | 发光/阴影特效 | <a:outerShdw>, <a:glow> 等复杂效果 pptxgenjs 渲染误差大 | | 不规则遮罩/裁切 | 图片经过自定义路径裁切(<p:spPr><a:custGeom> 包裹图片) | | VLM识别为渐变箭头/装饰图形 | 从截图分析中识别出类型为装饰性渐变形状 |


降级处理工作流

┌─────────────────────────────────────────────────────┐
│ 1. 判断是否为复杂形状(见上表)                          │
│    ├── 有原版PPTX → 从XML提取精确路径和颜色参数            │
│    └── 仅截图 → 用VLM估算形状轮廓和渐变颜色                │
│                                                     │
│ 2. 用Python/Pillow绘制透明背景PNG                       │
│    ├── 绘制精确路径(参考XML坐标 或 VLM估算像素)           │
│    ├── 应用渐变/透明/特效                               │
│    └── 输出 RGBA 透明背景 PNG                          │
│                                                     │
│ 3. 嵌入PPT(addImage)                               │
│    └── 坐标与原版对应(EMU换算或比例缩放)                  │
└─────────────────────────────────────────────────────┘

从原版XML提取形状参数(有源文件时优先)

当有原版 .pptx 可解包时,可获取最精确的参数:

# 解包PPTX
unzip original.pptx -d pptx_unpacked/
# 查看形状XML
cat pptx_unpacked/ppt/slides/slide1.xml

关键字段提取:

<!-- 自定义路径形状 -->
<a:custGeom>
  <a:pathLst>
    <a:path w="5725" h="1294">   <!-- 路径画布尺寸(EMU相对坐标)-->
      <a:moveTo><a:pt x="2852" y="0"/></a:moveTo>  <!-- 起点 -->
      <a:cubicBezTo>...</a:cubicBezTo>              <!-- 贝塞尔曲线 -->
      <a:lnTo>...</a:lnTo>                          <!-- 直线段 -->
    </a:path>
  </a:pathLst>
</a:custGeom>

<!-- 渐变填充 -->
<a:gradFill>
  <a:gsLst>
    <a:gs pos="0"><a:srgbClr val="FFFFFF"/></a:gs>    <!-- 白色,pos=0% -->
    <a:gs pos="100000"><a:srgbClr val="C00000"/></a:gs> <!-- 红色,pos=100% -->
  </a:gsLst>
  <a:lin ang="16200000" scaled="0"/>  <!-- ang=16200000 = 270°(EMU角度单位:60000/度)-->
</a:gradFill>

<!-- 形状在组合(grpSp)中的位置 -->
<a:off x="696" y="2680"/>  <!-- 相对于组的偏移 -->
<a:ext cx="5725" cy="1294"/>  <!-- 形状尺寸 -->

EMU角度转换公式:

实际角度(度)= EMU角度值 / 60000
例: 16200000 / 60000 = 270°(即从上到下的渐变方向)

组合坐标转绝对坐标公式:

// grp = { off: {x, y}, ext: {cx, cy}, chOff: {x, y}, chExt: {cx, cy} }
function absPos(grp, shape) {
  const scaleX = grp.ext.cx / grp.chExt.cx;
  const scaleY = grp.ext.cy / grp.chExt.cy;
  return {
    x: grp.off.x + (shape.x - grp.chOff.x) * scaleX,
    y: grp.off.y + (shape.y - grp.chOff.y) * scaleY,
    cx: shape.cx * scaleX,
    cy: shape.cy * scaleY,
  };
}

Python/Pillow 绘制复杂形状 PNG(模板)

"""
复杂形状PNG生成模板
适用于:自定义多边形 + 渐变填充 + 透明背景
"""
from PIL import Image, ImageDraw
import numpy as np

# ========== 配置区 ==========
OUTPUT_PATH = "complex_shape.png"
# 画布尺寸(可按EMU比例或屏幕像素设定,建议宽度1000+)
CANVAS_W = 1200
CANVAS_H = 280

# 渐变颜色(从上到下,可扩展多个停止点)
GRADIENT_STOPS = [
    (0.0,   (255, 255, 255)),   # 白色 #FFFFFF
    (1.0,   (192,   0,   0)),   # 深红 #C00000
]

# 形状路径点(归一化坐标 0~1,按原版路径节点填写)
# 说明:从XML的 custGeom 路径坐标除以路径画布宽高即可归一化
SHAPE_POINTS_NORMALIZED = [
    (0.498, 0.0),   # 顶部尖角
    (1.0,   0.25),  # 右上
    (1.0,   1.0),   # 右下
    (0.0,   1.0),   # 左下
    (0.0,   0.25),  # 左上
]
# ========== 配置区结束 ==========

def lerp_color(c1, c2, t):
    return tuple(int(c1[i] + (c2[i] - c1[i]) * t) for i in range(3))

def gradient_color(y_ratio, stops):
    """根据y比例插值渐变颜色"""
    for i in range(len(stops) - 1):
        p0, c0 = stops[i]
        p1, c1 = stops[i + 1]
        if p0 <= y_ratio <= p1:
            t = (y_ratio - p0) / (p1 - p0)
            return lerp_color(c0, c1, t)
    return stops[-1][1]

# 创建透明画布
img = Image.new("RGBA", (CANVAS_W, CANVAS_H), (0, 0, 0, 0))

# 构建蒙版(形状区域)
mask = Image.new("L", (CANVAS_W, CANVAS_H), 0)
draw_mask = ImageDraw.Draw(mask)
pixel_points = [(int(x * CANVAS_W), int(y * CANVAS_H)) for x, y in SHAPE_POINTS_NORMALIZED]
draw_mask.polygon(pixel_points, fill=255)

# 逐行填充渐变色
pixels = img.load()
mask_pixels = mask.load()
for row in range(CANVAS_H):
    y_ratio = row / CANVAS_H
    color = gradient_color(y_ratio, GRADIENT_STOPS)
    for col in range(CANVAS_W):
        if mask_pixels[col, row] > 0:
            pixels[col, row] = (*color, mask_pixels[col, row])

img.save(OUTPUT_PATH)
print(f"已生成: {OUTPUT_PATH} ({CANVAS_W}x{CANVAS_H})")

贝塞尔曲线采样(针对含 cubicBezTo 的复杂路径):

完整实现已打包在 scripts/complex_shape.py,支持命令行直接调用:

python scripts/complex_shape.py \
    --output complex_shape.png \
    --width 1200 --height 280 \
    --gradient-stops "0.0:#FFFFFF,1.0:#C00000" \
    --polygon "0.498,0.0 1.0,0.25 1.0,1.0 0.0,1.0 0.0,0.25"

嵌入PPT(addImage)

const PptxGenJS = require('pptxgenjs');

// EMU → 英寸(pptxgenjs坐标单位为英寸)
const EMU_PER_INCH = 914400;
function emuToInch(emu) { return emu / EMU_PER_INCH; }

// 嵌入生成的PNG(坐标来自absPos计算结果)
slide.addImage({
  path: './complex_shape.png',    // 或 data: base64String
  x: emuToInch(absPos.x),
  y: emuToInch(absPos.y),
  w: emuToInch(absPos.cx),
  h: emuToInch(absPos.cy),
});

仅有截图时的降级策略

当没有原版PPTX只有截图时:

  1. 用VLM分析形状:在分析Prompt中要求输出形状类型为 complex_shape,描述渐变颜色和方向
  2. 坐标估算:从像素坐标按比例转换为PPT EMU坐标(精度约±5%)
  3. 颜色提取:要求VLM输出渐变起止色的十六进制值
  4. 形状近似:若路径复杂,可用多边形近似(顶点数≥6通常足够)

更新VLM分析Prompt中的type枚举,增加:

"type": "complex_shape",
"shapeDescription": "渐变方向/颜色/形状类型的文字描述,供Python绘制参考",
"gradientStops": [{"pos": 0, "color": "#FFFFFF"}, {"pos": 1, "color": "#C00000"}],
"gradientAngle": 270

短期优化清单(v1.1已实现)

| 优先级 | 问题 | 解决方案 | |--------|------|----------| | P0 | 括号全角/半角差异 | 增强Prompt要求保留原版符号 | | P0 | bullet符号丢失或错误 | 新增 detectBullet() + bulletType字段 | | P0 | 行间距不一致 | 支持 lineSpacing 参数映射 | | P0 | 字符间距丢失 | 支持 charSpacing 参数映射 | | P1 | JSON解析失败 | 4策略容错解析 | | P1 | 圆角矩形显示为直角 | 新增 roundedRect 类型 |

快速执行命令

const { parseAnalysisToRegions, mapAllRegions } = require('./coordinate_mapper');
const { saveFromRegions } = require('./ppt_generator');

async function main(analysisResult, imageWidth, imageHeight, outputPath) {
  // 1. 解析区域(含符号校正)
  const regions = parseAnalysisToRegions(analysisResult);
  // 2. 坐标映射
  const mapped = mapAllRegions(regions, imageWidth, imageHeight);
  // 3. 生成PPTX
  await saveFromRegions(mapped, outputPath, { imageWidth, imageHeight });
}

输出规范

生成的PPTX必须包含:

  • 所有可见区域精确复刻
  • 正确的叠加顺序(zIndex)
  • 准确的颜色值
  • 可编辑的文本内容(含正确的符号和行间距)

依赖

  • Node.js + pptxgenjs
  • 图像理解工具(开放式,默认 OpenClaw 内置 image 工具 / MiniMax Vision MCP)
  • coordinate_mapper.js(坐标转换 + 符号校正)
  • ppt_generator.js(PPT生成 + 格式支持)

方案B:基于原版PPTX的内容替换(ZIP直接操作法)

适用场景:用户有原版 .pptx 文件,只需替换文字内容,保留所有样式、母版、配色、custGeom、媒体文件。
优点:100%保留原版视觉效果,无需重建任何形状,文件结构完整可靠。
与方案A的选择原则:有原版PPTX → 优先方案B;仅有截图 → 方案A(从零生成)。


核心思路

PPTX 本质是一个 ZIP 压缩包。直接用 Python zipfile 操作:

  • 只替换 ppt/slides/slide1.xml(幻灯片内容)
  • 完整保留其他所有文件(母版/布局/主题/媒体/标签等)
原版PPTX (89个文件)
  └── ppt/slides/slide1.xml  ← 只替换这一个文件
  └── ppt/slideMasters/      ← 保留
  └── ppt/slideLayouts/      ← 保留
  └── ppt/theme/             ← 保留
  └── ppt/media/             ← 保留(图片、SVG等)
  └── [Content_Types].xml    ← 保留
  └── ...所有其他文件         ← 保留

⚠️ 关键注意事项

  1. 必须用完整原版PPTX为基础,不能用手动解包再重新打包的目录(极易缺失文件)
  2. 中文文本被拆散在多个 <a:r> 标签中,不能直接字符串替换,需整段 <a:p> 替换
  3. XML中特殊字符必须转义<&lt;>&gt;&&amp;
  4. 替换前先用 xmllint --nooutxml.etree.ElementTree 验证XML有效性

完整替换脚本模板

import zipfile, re, io

src = '/path/to/original.pptx'   # 原版完整PPTX
out = '/path/to/output.pptx'      # 输出路径

# 1. 读取 slide1.xml
with zipfile.ZipFile(src, 'r') as z:
    xml = z.read('ppt/slides/slide1.xml').decode('utf-8')

# =============================================
# 2. 文本替换(两种策略,按情况选用)
# =============================================

# --- 策略A:简单单Run替换(文本完整在一个 <a:t> 标签内)---
# 格式:(旧文本含尖括号定位符, 新文本含尖括号定位符)
simple_replacements = [
    ('>旧文本1<', '>新文本1<'),
    ('>旧文本2<', '>新文本2<'),
    # ⚠️ 如果新文本含 < 或 > 必须转义:
    ('>旧文本3<', '>误差&lt;1%<'),
]
for old, new in simple_replacements:
    xml = xml.replace(old, new)

# --- 策略B:整段替换(中文被拆散在多个 <a:r> 中时使用)---
def replace_para_containing(xml_str, search_text, new_text, rPr_override=None):
    """
    找到包含 search_text 的整个 <a:p> 段落,替换为只含 new_text 的新段落。
    保留原段落的 pPr(段落格式)和 rPr(文字格式)。
    """
    idx = xml_str.find(f'>{search_text}<')
    if idx < 0:
        print(f'⚠️  未找到文本:{search_text}')
        return xml_str
    start = xml_str.rfind('<a:p>', 0, idx)
    end = xml_str.find('</a:p>', idx) + 6
    old_para = xml_str[start:end]
    # 提取段落格式
    pPr_match = re.search(r'<a:pPr.*?</a:pPr>', old_para, re.DOTALL)
    pPr = pPr_match.group() if pPr_match else ''
    # 提取文字格式(优先自闭合 rPr)
    if rPr_override:
        rPr = rPr_override
    else:
        rPr_match = re.search(r'<a:rPr(?:\s[^>]*)*/>', old_para)
        if not rPr_match:
            rPr_match = re.search(r'<a:rPr.*?</a:rPr>', old_para, re.DOTALL)
        rPr = rPr_match.group() if rPr_match else '<a:rPr/>'
    new_para = f'<a:p>{pPr}<a:r>{rPr}<a:t>{new_text}</a:t></a:r></a:p>'
    return xml_str.replace(old_para, new_para, 1)

# 调用示例
xml = replace_para_containing(xml, '原始文本片段', '替换后的完整文本')
# 如果连 search_text 的定位符也找不到,说明文本被拆散更细,
# 此时需检查XML原文,找到任意未被拆散的子串作为 search_text

# =============================================
# 3. 验证XML有效性(替换后必做)
# =============================================
import xml.etree.ElementTree as ET
try:
    ET.fromstring(xml)
    print('✅ XML验证通过')
except ET.ParseError as e:
    print(f'❌ XML解析错误:{e}')
    # 定位错误行列,检查是否有未转义的特殊字符

# =============================================
# 4. 打包输出(保留原文件所有内容)
# =============================================
buf = io.BytesIO()
with zipfile.ZipFile(src, 'r') as zin:
    with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zout:
        for item in zin.infolist():
            if item.filename == 'ppt/slides/slide1.xml':
                zout.writestr(item, xml.encode('utf-8'))
            else:
                zout.writestr(item, zin.read(item.filename))

with open(out, 'wb') as f:
    f.write(buf.getvalue())
print(f'✅ 已生成:{out}')

调试技巧

查找文本在XML中的实际位置:

# 先解包查看原始XML结构
with zipfile.ZipFile(src, 'r') as z:
    xml = z.read('ppt/slides/slide1.xml').decode('utf-8')

# 搜索目标文本(注意:可能是片段,不是完整句子)
target = '要查找的文本'
idx = xml.find(target)
if idx >= 0:
    print(xml[max(0, idx-200):idx+200])  # 打印前后200字符上下文
else:
    # 中文被拆散时,尝试搜索其中一个汉字或标点
    print('未找到完整文本,尝试搜索子串')

判断应该用策略A还是B:

文本在XML中是连续的 <a:t>完整文本</a:t> → 用策略A(简单替换)
文本被拆散为多个 <a:r>...<a:t>片</a:t></a:r><a:r>...<a:t>段</a:t></a:r> → 用策略B(整段替换)

常见错误与解决

| 错误 | 原因 | 解决 | |------|------|------| | PowerPoint无法读取文件 | 打包基础文件不完整(缺media/theme等) | 必须用原版完整PPTX为基础,不能用手动解包目录 | | XML解析报错(column XXXX) | 替换文本含未转义的 < > & | 转义为 &lt; &gt; &amp; | | 替换未生效 | 中文文本被拆散在多个 <a:r> 中 | 改用 replace_para_containing() 整段替换 | | 找不到 search_text | 数字/标点与汉字分开在不同 run | 搜索纯汉字或标点子串定位,再整段替换 | | slideMaster.xml.rels 为空 | 解包时内容丢失(此问题仅在手动解包时出现) | 从原版PPTX直接操作,不会有此问题 |


变更日志

v1.5.0 (2026-03-27)

  • 修复 ClawHub 审查问题:新增 requires metadata 声明运行时依赖(npm:pptxgenjs, python:Pillow)
  • 新增完整脚本scripts/complex_shape.py 提供可直接执行的复杂形状 PNG 生成工具
  • 新增使用前须知:明确图片数据发送至外部 VLM 的隐私说明、文件系统读写说明、运行时依赖

v1.4.0 (2026-03-26)

  • VLM 调用方式升级:移除 minimax-understand-image 技能依赖,改为开放式 VLM 调用设计,默认 OpenClaw 内置 image 工具
  • 适配 MiniMax 作为模型提供商时的内置 Vision 能力

v1.3.0 (2026-03-26)

  • 新增"方案B:基于原版PPTX的内容替换(ZIP直接操作法)"
    • 明确方案A(从零生成)vs 方案B(原版替换)的选择原则
    • 提供完整的 Python zipfile 操作模板(只替换 slide1.xml,保留所有其他文件)
    • 提供两种替换策略:策略A(简单单Run替换)+ 策略B(整段 <a:p> 替换)
    • 提供 replace_para_containing() 通用函数,解决中文文本被拆散在多个 <a:r> 的问题
    • 提供调试技巧(XML上下文打印、策略A/B判断方法)
    • 提供常见错误排查表(文件损坏/XML解析报错/替换未生效等)
    • 总结关键注意事项:必须用完整原版PPTX、特殊字符必须转义等

v1.2.0 (2026-03-26)

  • 新增"复杂形状降级处理策略"(核心功能升级)
    • 明确7类需要降级的形状判断标准(custGeom/透明渐变/斜向渐变/特效/裁切等)
    • 提供完整的 Python/Pillow 绘制模板(含多边形 + 逐行渐变 + 透明背景)
    • 提供贝塞尔曲线采样函数(适配 XML cubicBezTo
    • 提供XML参数提取指南(路径坐标、渐变色、EMU角度转换、组合坐标变换)
    • 提供 addImage 嵌入代码模板
    • 补充仅有截图时的降级策略(VLM估算 + 多边形近似)
    • 区域类型映射表新增 complex_shape

v1.1.0 (2026-03-26)

  • 增强分析Prompt(短期优化P0方案)
  • 增加全角/半角符号校正 normalizeSymbols()
  • 增加bullet符号识别 detectBullet()
  • 增加行间距 lineSpacing 渲染支持
  • 增加字符间距 charSpacing 渲染支持
  • 增加圆角矩形 roundedRect 类型支持
  • 增加JSON解析4策略容错
  • 改进多行文本段落渲染

v1.0.0 (2026-03-26)

  • 初始版本,基于Images2Slides论文实现基本复刻流程