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截圖/資訊圖轉換為可編輯PPTX檔案的完整工作流。
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
第一步的圖片識別為開放式設計,可靈活接入任意具備影像理解能力的工具——
如 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按照視覺層疊順序從後到前遞增`;
呼叫 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解析容錯(直接解析 → 陣列提取 → 物件提取 → 程式碼塊提取)
呼叫 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新增 |
核心思路: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換算或比例縮放) │
└─────────────────────────────────────────────────────┘
當有原版 .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,
};
}
"""
複雜形狀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,支援命令列直接呼叫:bash 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"
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只有截圖時:
complex_shape,描述漸變顏色和方向更新VLM分析Prompt中的type列舉,增加:
"type": "complex_shape",
"shapeDescription": "漸變方向/顏色/形狀型別的文字描述,供Python繪製參考",
"gradientStops": [{"pos": 0, "color": "#FFFFFF"}, {"pos": 1, "color": "#C00000"}],
"gradientAngle": 270
| 優先順序 | 問題 | 解決方案 |
|---|---|---|
| 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) - 準確的顏色值 - 可編輯的文本內容(含正確的符號和行間距)
image 工具 / MiniMax Vision MCP)更多技能請訪問小蔥技能站7w4.net。
適用場景:使用者有原版
.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 ← 保留
└── ...所有其他檔案 ← 保留
<a:r> 標籤中,不能直接字串替換,需整段 <a:p> 替換< → <,> → >,& → &xmllint --noout 或 xml.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<', '>誤差<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) | 替換文本含未轉義的 < > & |
轉義為 < > & |
| 替換未生效 | 中文文本被拆散在多個 <a:r> 中 |
改用 replace_para_containing() 整段替換 |
| 找不到 search_text | 數字/標點與漢字分開在不同 run | 搜尋純漢字或標點子串定位,再整段替換 |
| slideMaster.xml.rels 為空 | 解包時內容丟失(此問題僅在手動解包時出現) | 從原版PPTX直接操作,不會有此問題 |
requires metadata 宣告執行時依賴(npm:pptxgenjs, python:Pillow)scripts/complex_shape.py 提供可直接執行的複雜形狀 PNG 生成工具minimax-understand-image 技能依賴,改為開放式 VLM 呼叫設計,預設 OpenClaw 內建 image 工具zipfile 操作模板(只替換 slide1.xml,保留所有其他檔案)<a:p> 替換)replace_para_containing() 通用函式,解決中文文本被拆散在多個 <a:r> 的問題cubicBezTo)addImage 嵌入程式碼模板complex_shape 行normalizeSymbols()detectBullet()lineSpacing 渲染支援charSpacing 渲染支援roundedRect 型別支援這個技能能把 PPT 截圖轉換成可編輯的 PPTX 檔案,功能實用且技術文件非常詳細。不過實際使用時可能遇到版本號對不上的問題,而且文件缺少簡單直觀的完整操作示例,對於不熟悉技術細節的使用者來說,上手可能有一定門檻。總體質量中規中矩,理論基礎紮實,但配套的指引材料還可以更完善。