PPT 视觉复刻
by @allergro
将PPT截图或信息图转换为可编辑的PPTX文件。此技能基于Images2Slides论文(arXiv:2602.07645)实现, 利用视觉-语言模型(VLM)进行区域理解,通过坐标映射算法将像素坐标转换为PPTX坐标。 支持复杂形状降级处理策略(custGeom/渐变/透明等无法直接还原的形状转为PNG嵌入)。...
clawhub install ppt-vision-replica📖 About This Skill
name: ppt-vision-replica description: | 将PPT截图或信息图转换为可编辑的PPTX文件。此技能基于Images2Slides论文(arXiv:2602.07645)实现, 利用视觉-语言模型(VLM)进行区域理解,通过坐标映射算法将像素坐标转换为PPTX坐标。 支持复杂形状降级处理策略(custGeom/渐变/透明等无法直接还原的形状转为PNG嵌入)。 触发场景:用户上传PPT截图并要求复刻、用户要求分析PPT结构、用户希望将图片PPT转换为可编辑版本。 version: 1.5.0 requires: os: [linux, darwin, win32] npm: [pptxgenjs] python: [Pillow] # 仅复杂形状降级时需要 filesystem: true # 读写临时PNG和输出PPTX文件 network: true # 调用外部VLM处理图像(图片数据会发送到配置的VLM服务端点)
> ⚠️ 使用前须知
> - 图片数据发送:第一步 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符号类型阶段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中存在 标签,包含贝塞尔曲线、不规则路径 |
| 透明渐变填充 | 多色渐变(3个以上 stop)或带透明度通道(alpha < 100%)的渐变 |
| 复合渐变方向 | 渐变角度非水平/垂直(非0/90/180/270度),如斜向渐变 |
| 发光/阴影特效 | , 等复杂效果 pptxgenjs 渲染误差大 |
| 不规则遮罩/裁切 | 图片经过自定义路径裁切( 包裹图片) |
| 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
关键字段提取:
...
...
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必须包含:
依赖
image 工具 / MiniMax Vision MCP)方案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. 中文文本被拆散在多个 标签中,不能直接字符串替换,需整段 替换
3. XML中特殊字符必须转义:< → <,> → >,& → &
4. 替换前先用 xmllint --noout 或 xml.etree.ElementTree 验证XML有效性
完整替换脚本模板
import zipfile, re, iosrc = '/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替换(文本完整在一个 标签内)---
格式:(旧文本含尖括号定位符, 新文本含尖括号定位符)
simple_replacements = [
('>旧文本1<', '>新文本1<'),
('>旧文本2<', '>新文本2<'),
# ⚠️ 如果新文本含 < 或 > 必须转义:
('>旧文本3<', '>误差<1%<'),
]
for old, new in simple_replacements:
xml = xml.replace(old, new)--- 策略B:整段替换(中文被拆散在多个 中时使用)---
def replace_para_containing(xml_str, search_text, new_text, rPr_override=None):
"""
找到包含 search_text 的整个 段落,替换为只含 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('', 0, idx)
end = xml_str.find(' ', idx) + 6
old_para = xml_str[start:end]
# 提取段落格式
pPr_match = re.search(r'', 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']*)*/>', old_para)
if not rPr_match:
rPr_match = re.search(r'', old_para, re.DOTALL)
rPr = rPr_match.group() if rPr_match else ' '
new_para = f'{pPr}{rPr}{new_text} '
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(简单替换)
文本被拆散为多个 ...片 ...段 → 用策略B(整段替换)
常见错误与解决
| 错误 | 原因 | 解决 |
|------|------|------|
| PowerPoint无法读取文件 | 打包基础文件不完整(缺media/theme等) | 必须用原版完整PPTX为基础,不能用手动解包目录 |
| XML解析报错(column XXXX) | 替换文本含未转义的 < > & | 转义为 < > & |
| 替换未生效 | 中文文本被拆散在多个 中 | 改用 replace_para_containing() 整段替换 |
| 找不到 search_text | 数字/标点与汉字分开在不同 run | 搜索纯汉字或标点子串定位,再整段替换 |
| slideMaster.xml.rels 为空 | 解包时内容丢失(此问题仅在手动解包时出现) | 从原版PPTX直接操作,不会有此问题 |
变更日志
v1.5.0 (2026-03-27)
requires metadata 声明运行时依赖(npm:pptxgenjs, python:Pillow)scripts/complex_shape.py 提供可直接执行的复杂形状 PNG 生成工具v1.4.0 (2026-03-26)
minimax-understand-image 技能依赖,改为开放式 VLM 调用设计,默认 OpenClaw 内置 image 工具v1.3.0 (2026-03-26)
zipfile 操作模板(只替换 slide1.xml,保留所有其他文件)
- 提供两种替换策略:策略A(简单单Run替换)+ 策略B(整段 替换)
- 提供 replace_para_containing() 通用函数,解决中文文本被拆散在多个 的问题
- 提供调试技巧(XML上下文打印、策略A/B判断方法)
- 提供常见错误排查表(文件损坏/XML解析报错/替换未生效等)
- 总结关键注意事项:必须用完整原版PPTX、特殊字符必须转义等v1.2.0 (2026-03-26)
cubicBezTo)
- 提供XML参数提取指南(路径坐标、渐变色、EMU角度转换、组合坐标变换)
- 提供 addImage 嵌入代码模板
- 补充仅有截图时的降级策略(VLM估算 + 多边形近似)
- 区域类型映射表新增 complex_shape 行v1.1.0 (2026-03-26)
normalizeSymbols()detectBullet()lineSpacing 渲染支持charSpacing 渲染支持roundedRect 类型支持