iflow-mcp_mcp-server-okppt 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iflow_mcp_mcp_server_okppt-0.2.0.dist-info/METADATA +372 -0
- iflow_mcp_mcp_server_okppt-0.2.0.dist-info/RECORD +12 -0
- iflow_mcp_mcp_server_okppt-0.2.0.dist-info/WHEEL +5 -0
- iflow_mcp_mcp_server_okppt-0.2.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_mcp_server_okppt-0.2.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_mcp_server_okppt-0.2.0.dist-info/top_level.txt +1 -0
- mcp_server_okppt/__init__.py +0 -0
- mcp_server_okppt/__main__.py +8 -0
- mcp_server_okppt/cli.py +27 -0
- mcp_server_okppt/ppt_operations.py +506 -0
- mcp_server_okppt/server.py +1619 -0
- mcp_server_okppt/svg_module.py +931 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
from pptx import Presentation
|
|
2
|
+
from pptx.util import Inches, Pt, Cm, Emu
|
|
3
|
+
from typing import Optional, Union, List, Dict, Any # Added Dict, Any for type hinting
|
|
4
|
+
import os
|
|
5
|
+
import datetime
|
|
6
|
+
import traceback
|
|
7
|
+
import logging # Added for logging
|
|
8
|
+
import sys
|
|
9
|
+
# Assuming logger is configured in server.py and can be imported or a new one is created here
|
|
10
|
+
# For simplicity, let's assume a logger is available or we'll create one.
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
if not logger.handlers:
|
|
13
|
+
logging.basicConfig(
|
|
14
|
+
level=logging.INFO,
|
|
15
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
16
|
+
stream=sys.stdout
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Path helper functions (will be imported from server.py or defined if not available)
|
|
20
|
+
# For now, we'll assume they are available from server.py
|
|
21
|
+
# from server import get_default_output_path, cleanup_filename, get_output_dir, get_tmp_dir
|
|
22
|
+
|
|
23
|
+
def get_placeholder_type_name(ph_type):
|
|
24
|
+
"""将占位符类型ID转换为可读名称"""
|
|
25
|
+
placeholder_types = {
|
|
26
|
+
1: "标题 (Title)",
|
|
27
|
+
2: "正文/内容 (Body)",
|
|
28
|
+
3: "居中标题 (Center Title)",
|
|
29
|
+
4: "副标题 (Subtitle)",
|
|
30
|
+
5: "日期和时间 (Date)",
|
|
31
|
+
6: "幻灯片编号 (Slide Number)",
|
|
32
|
+
7: "页脚 (Footer)",
|
|
33
|
+
8: "页眉 (Header)",
|
|
34
|
+
9: "对象 (Object)",
|
|
35
|
+
10: "图表 (Chart)",
|
|
36
|
+
11: "表格 (Table)",
|
|
37
|
+
12: "剪贴画 (Clip Art)",
|
|
38
|
+
13: "组织结构图 (Organization Chart)",
|
|
39
|
+
14: "媒体剪辑 (Media Clip)",
|
|
40
|
+
15: "图片 (Picture)",
|
|
41
|
+
16: "垂直对象 (Vertical Object)",
|
|
42
|
+
17: "垂直文本 (Vertical Text)"
|
|
43
|
+
}
|
|
44
|
+
return placeholder_types.get(ph_type, f"未知类型 (Unknown Type {ph_type})")
|
|
45
|
+
|
|
46
|
+
# ... rest of the functions will be added below ...
|
|
47
|
+
|
|
48
|
+
# Path helper functions will be imported from server.py
|
|
49
|
+
# For now, we assume they are available.
|
|
50
|
+
# It's better to pass them or have server.py provide them.
|
|
51
|
+
# For this step, direct import for get_default_output_path for simplicity if needed, though analyze_layout_details doesn't create files.
|
|
52
|
+
try:
|
|
53
|
+
from mcp_server_okppt.server import get_default_output_path, cleanup_filename
|
|
54
|
+
except ImportError:
|
|
55
|
+
logger.warning("Could not import path helpers from server.py for ppt_operations.py")
|
|
56
|
+
def get_default_output_path(file_type="pptx", base_name=None, op_type=None): # Dummy for standalone
|
|
57
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
58
|
+
return f"{base_name or 'temp'}_{op_type or 'op'}_{timestamp}.{file_type}"
|
|
59
|
+
def cleanup_filename(filename: str) -> str:
|
|
60
|
+
return filename
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def analyze_layout_details(prs_path: str, title: str = "演示文稿") -> Dict[str, Any]:
|
|
64
|
+
"""分析演示文稿的布局详细信息并返回结构化数据"""
|
|
65
|
+
analysis_data: Dict[str, Any] = {
|
|
66
|
+
"presentation_path": prs_path,
|
|
67
|
+
"analysis_title": title,
|
|
68
|
+
"slide_count": 0,
|
|
69
|
+
"master_count": 0,
|
|
70
|
+
"total_layouts_count": 0,
|
|
71
|
+
"layout_type_stats": {},
|
|
72
|
+
"masters_details": [],
|
|
73
|
+
"slide_layout_usage_summary": {},
|
|
74
|
+
"slides_details": [],
|
|
75
|
+
"unused_layouts_summary": [],
|
|
76
|
+
"layout_utilization": {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
prs = Presentation(prs_path)
|
|
81
|
+
analysis_data["slide_count"] = len(prs.slides)
|
|
82
|
+
analysis_data["master_count"] = len(prs.slide_masters)
|
|
83
|
+
|
|
84
|
+
total_layouts = 0
|
|
85
|
+
layout_type_stats: Dict[str, int] = {
|
|
86
|
+
"自定义布局": 0,
|
|
87
|
+
"系统布局": 0,
|
|
88
|
+
"无名称布局": 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
masters_details_list = []
|
|
92
|
+
for master_idx, master in enumerate(prs.slide_masters):
|
|
93
|
+
master_detail = {
|
|
94
|
+
"master_index": master_idx + 1,
|
|
95
|
+
"master_name": master.name,
|
|
96
|
+
"layout_count": len(master.slide_layouts),
|
|
97
|
+
"layouts": []
|
|
98
|
+
}
|
|
99
|
+
total_layouts += len(master.slide_layouts)
|
|
100
|
+
|
|
101
|
+
for layout_idx, layout in enumerate(master.slide_layouts):
|
|
102
|
+
layout_name = layout.name
|
|
103
|
+
layout_type = ""
|
|
104
|
+
layout_display = ""
|
|
105
|
+
|
|
106
|
+
if layout_name == "" or layout_name is None:
|
|
107
|
+
layout_type = "无名称布局"
|
|
108
|
+
layout_display = f"无名称(默认布局)"
|
|
109
|
+
elif layout_name.isdigit():
|
|
110
|
+
layout_type = "系统布局"
|
|
111
|
+
layout_display = f"系统布局 - {layout_name}"
|
|
112
|
+
else:
|
|
113
|
+
layout_type = "自定义布局"
|
|
114
|
+
layout_display = f"自定义布局 - {layout_name}"
|
|
115
|
+
|
|
116
|
+
layout_type_stats[layout_type] = layout_type_stats.get(layout_type, 0) + 1
|
|
117
|
+
|
|
118
|
+
layout_detail = {
|
|
119
|
+
"layout_index": layout_idx + 1,
|
|
120
|
+
"layout_name_original": layout_name,
|
|
121
|
+
"layout_display_name": layout_display,
|
|
122
|
+
"layout_type": layout_type,
|
|
123
|
+
"placeholder_count": len(layout.placeholders),
|
|
124
|
+
"placeholders": []
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if len(layout.placeholders) > 0:
|
|
128
|
+
for ph_idx, placeholder in enumerate(layout.placeholders):
|
|
129
|
+
try:
|
|
130
|
+
ph_format = placeholder.placeholder_format
|
|
131
|
+
ph_type_code = ph_format.type
|
|
132
|
+
ph_access_id = ph_format.idx
|
|
133
|
+
ph_type_name = get_placeholder_type_name(ph_type_code)
|
|
134
|
+
layout_detail["placeholders"].append({
|
|
135
|
+
"placeholder_index": ph_idx + 1,
|
|
136
|
+
"type_name": ph_type_name,
|
|
137
|
+
"access_id": ph_access_id,
|
|
138
|
+
"type_code": ph_type_code
|
|
139
|
+
})
|
|
140
|
+
except Exception as e_ph:
|
|
141
|
+
logger.warning(f"Placeholder {ph_idx + 1} in layout '{layout_name}': type无法获取 ({str(e_ph)})")
|
|
142
|
+
layout_detail["placeholders"].append({
|
|
143
|
+
"placeholder_index": ph_idx + 1,
|
|
144
|
+
"error": f"类型无法获取 ({str(e_ph)})"
|
|
145
|
+
})
|
|
146
|
+
master_detail["layouts"].append(layout_detail)
|
|
147
|
+
masters_details_list.append(master_detail)
|
|
148
|
+
|
|
149
|
+
analysis_data["masters_details"] = masters_details_list
|
|
150
|
+
analysis_data["total_layouts_count"] = total_layouts
|
|
151
|
+
analysis_data["layout_type_stats"] = {
|
|
152
|
+
k: {"count": v, "percentage": (v / total_layouts * 100) if total_layouts > 0 else 0}
|
|
153
|
+
for k, v in layout_type_stats.items()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
slides_details_list = []
|
|
157
|
+
layout_usage: Dict[str, int] = {}
|
|
158
|
+
if len(prs.slides) > 0:
|
|
159
|
+
for slide_idx, slide in enumerate(prs.slides):
|
|
160
|
+
layout_name = slide.slide_layout.name
|
|
161
|
+
layout_type = ""
|
|
162
|
+
layout_display = ""
|
|
163
|
+
|
|
164
|
+
if layout_name == "" or layout_name is None:
|
|
165
|
+
layout_type = "无名称布局"
|
|
166
|
+
layout_display = "无名称(默认布局)"
|
|
167
|
+
elif layout_name.isdigit():
|
|
168
|
+
layout_type = "系统布局"
|
|
169
|
+
layout_display = f"系统布局 - {layout_name}"
|
|
170
|
+
else:
|
|
171
|
+
layout_type = "自定义布局"
|
|
172
|
+
layout_display = f"自定义布局 - {layout_name}"
|
|
173
|
+
|
|
174
|
+
slide_title = "无标题"
|
|
175
|
+
try:
|
|
176
|
+
if slide.shapes.title and slide.shapes.title.has_text_frame and slide.shapes.title.text.strip():
|
|
177
|
+
slide_title = slide.shapes.title.text.strip()
|
|
178
|
+
except: # pylint: disable=bare-except
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
slide_detail = {
|
|
182
|
+
"slide_number": slide_idx + 1,
|
|
183
|
+
"title": slide_title,
|
|
184
|
+
"used_layout_name_original": layout_name,
|
|
185
|
+
"used_layout_display_name": layout_display,
|
|
186
|
+
"used_layout_type": layout_type,
|
|
187
|
+
"placeholder_count_on_slide": len(slide.placeholders)
|
|
188
|
+
}
|
|
189
|
+
slides_details_list.append(slide_detail)
|
|
190
|
+
|
|
191
|
+
layout_usage[layout_name] = layout_usage.get(layout_name, 0) + 1
|
|
192
|
+
|
|
193
|
+
analysis_data["slide_layout_usage_summary"] = {
|
|
194
|
+
name: {"count": count, "percentage": (count / len(prs.slides) * 100)}
|
|
195
|
+
for name, count in sorted(layout_usage.items(), key=lambda x: x[1], reverse=True)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
unused_layouts_list = []
|
|
199
|
+
for master in prs.slide_masters:
|
|
200
|
+
for layout in master.slide_layouts:
|
|
201
|
+
if layout.name not in layout_usage:
|
|
202
|
+
layout_type_unused = "自定义布局"
|
|
203
|
+
if layout.name == "" or layout.name is None: layout_type_unused = "无名称布局"
|
|
204
|
+
elif layout.name.isdigit(): layout_type_unused = "系统布局"
|
|
205
|
+
unused_layouts_list.append({"name": layout.name, "type": layout_type_unused})
|
|
206
|
+
analysis_data["unused_layouts_summary"] = unused_layouts_list
|
|
207
|
+
|
|
208
|
+
used_layouts_count = len(layout_usage)
|
|
209
|
+
total_available_layouts = sum(len(master.slide_layouts) for master in prs.slide_masters)
|
|
210
|
+
utilization_rate = (used_layouts_count / total_available_layouts * 100) if total_available_layouts > 0 else 0
|
|
211
|
+
analysis_data["layout_utilization"] = {
|
|
212
|
+
"total_available": total_available_layouts,
|
|
213
|
+
"used_count": used_layouts_count,
|
|
214
|
+
"utilization_rate_percentage": utilization_rate
|
|
215
|
+
}
|
|
216
|
+
analysis_data["slides_details"] = slides_details_list
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"status": "success",
|
|
220
|
+
"message": "Presentation analysis successful.",
|
|
221
|
+
"data": analysis_data,
|
|
222
|
+
"output_path": None
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Error in analyze_layout_details for {prs_path}: {str(e)}\n{traceback.format_exc()}")
|
|
227
|
+
return {
|
|
228
|
+
"status": "error",
|
|
229
|
+
"message": f"分析布局详情失败: {str(e)}",
|
|
230
|
+
"data": analysis_data, # Return partial data if any
|
|
231
|
+
"output_path": None
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def insert_layout(prs_path: str, layout_to_insert: str, output_path: Optional[str] = None, new_slide_title: Optional[str] = None) -> Dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
在演示文稿中插入一个使用指定布局的新幻灯片,并返回操作结果。
|
|
237
|
+
"""
|
|
238
|
+
prs = None
|
|
239
|
+
original_slide_count = 0
|
|
240
|
+
try:
|
|
241
|
+
prs = Presentation(prs_path)
|
|
242
|
+
original_slide_count = len(prs.slides)
|
|
243
|
+
|
|
244
|
+
base_name_cleaned = cleanup_filename(os.path.splitext(os.path.basename(prs_path))[0])
|
|
245
|
+
final_output_path = output_path or get_default_output_path(
|
|
246
|
+
base_name=base_name_cleaned,
|
|
247
|
+
op_type=f"inserted_layout_{layout_to_insert.replace(' ', '_')}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
available_layouts = {}
|
|
251
|
+
for master in prs.slide_masters:
|
|
252
|
+
for layout in master.slide_layouts:
|
|
253
|
+
available_layouts[layout.name] = layout
|
|
254
|
+
|
|
255
|
+
target_layout = available_layouts.get(layout_to_insert)
|
|
256
|
+
|
|
257
|
+
if target_layout:
|
|
258
|
+
new_slide = prs.slides.add_slide(target_layout)
|
|
259
|
+
logger.info(f"Added new slide using layout '{layout_to_insert}'")
|
|
260
|
+
|
|
261
|
+
if new_slide_title:
|
|
262
|
+
if new_slide.shapes.title:
|
|
263
|
+
new_slide.shapes.title.text = new_slide_title
|
|
264
|
+
logger.info(f"New slide title set to: '{new_slide_title}'")
|
|
265
|
+
else:
|
|
266
|
+
logger.warning(f"Layout '{layout_to_insert}' has no title placeholder for title '{new_slide_title}'")
|
|
267
|
+
|
|
268
|
+
prs.save(final_output_path)
|
|
269
|
+
logger.info(f"Presentation saved to {final_output_path}")
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
"status": "success",
|
|
273
|
+
"message": f"Successfully inserted slide with layout '{layout_to_insert}'.",
|
|
274
|
+
"data": {
|
|
275
|
+
"slides_total": len(prs.slides),
|
|
276
|
+
"original_slide_count": original_slide_count,
|
|
277
|
+
"new_slide_title_set": new_slide_title if new_slide.shapes.title and new_slide_title else None
|
|
278
|
+
},
|
|
279
|
+
"output_path": final_output_path
|
|
280
|
+
}
|
|
281
|
+
else:
|
|
282
|
+
logger.error(f"Layout '{layout_to_insert}' not found in {prs_path}. Available: {list(available_layouts.keys())}")
|
|
283
|
+
return {
|
|
284
|
+
"status": "error",
|
|
285
|
+
"message": f"Layout '{layout_to_insert}' not found. Available layouts: {', '.join(available_layouts.keys())}",
|
|
286
|
+
"data": {"available_layouts": list(available_layouts.keys()), "original_slide_count": original_slide_count},
|
|
287
|
+
"output_path": None
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.error(f"Error in insert_layout for {prs_path} with layout '{layout_to_insert}': {str(e)}\n{traceback.format_exc()}")
|
|
292
|
+
return {
|
|
293
|
+
"status": "error",
|
|
294
|
+
"message": f"插入布局失败: {str(e)}",
|
|
295
|
+
"data": {"original_slide_count": original_slide_count if prs else 0},
|
|
296
|
+
"output_path": None
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
def clear_placeholder_content(prs_path: str, output_path: Optional[str] = None, slide_indices: Optional[List[int]] = None) -> Dict[str, Any]:
|
|
300
|
+
"""
|
|
301
|
+
清空演示文稿中占位符的内容(保留占位符结构),并返回操作结果。
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
prs = Presentation(prs_path)
|
|
305
|
+
original_slide_count = len(prs.slides)
|
|
306
|
+
|
|
307
|
+
base_name_cleaned = cleanup_filename(os.path.splitext(os.path.basename(prs_path))[0])
|
|
308
|
+
final_output_path = output_path or get_default_output_path(
|
|
309
|
+
base_name=base_name_cleaned,
|
|
310
|
+
op_type="content_cleared"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
total_cleared = 0
|
|
314
|
+
slides_processed = 0
|
|
315
|
+
processed_slides_info = []
|
|
316
|
+
|
|
317
|
+
slides_to_process_indices = list(range(len(prs.slides))) if slide_indices is None else [i for i in slide_indices if 0 <= i < len(prs.slides)]
|
|
318
|
+
|
|
319
|
+
logger.info(f"Preparing to clear content from {len(slides_to_process_indices)} slides in {prs_path}.")
|
|
320
|
+
|
|
321
|
+
for slide_idx in slides_to_process_indices:
|
|
322
|
+
slide = prs.slides[slide_idx]
|
|
323
|
+
slide_cleared_count = 0
|
|
324
|
+
placeholders_on_slide_info = []
|
|
325
|
+
|
|
326
|
+
for ph_idx, placeholder in enumerate(slide.placeholders):
|
|
327
|
+
try:
|
|
328
|
+
placeholder_cleared_flag = False
|
|
329
|
+
ph_access_id = placeholder.placeholder_format.idx
|
|
330
|
+
ph_type_name = get_placeholder_type_name(placeholder.placeholder_format.type)
|
|
331
|
+
|
|
332
|
+
current_ph_info = {"access_id": ph_access_id, "type": ph_type_name, "cleared": False, "reason": ""}
|
|
333
|
+
|
|
334
|
+
if hasattr(placeholder, 'text_frame') and placeholder.text_frame is not None:
|
|
335
|
+
if placeholder.text_frame.text.strip():
|
|
336
|
+
placeholder.text_frame.clear()
|
|
337
|
+
placeholder_cleared_flag = True
|
|
338
|
+
current_ph_info["reason"] = "text_frame cleared"
|
|
339
|
+
logger.debug(f" Cleared text_frame for placeholder {ph_access_id} on slide {slide_idx + 1}")
|
|
340
|
+
elif hasattr(placeholder, 'text'): # pylint: disable=else-if-used
|
|
341
|
+
if placeholder.text.strip():
|
|
342
|
+
placeholder.text = ""
|
|
343
|
+
placeholder_cleared_flag = True
|
|
344
|
+
current_ph_info["reason"] = "text property cleared"
|
|
345
|
+
logger.debug(f" Cleared text property for placeholder {ph_access_id} on slide {slide_idx + 1}")
|
|
346
|
+
|
|
347
|
+
if placeholder_cleared_flag:
|
|
348
|
+
slide_cleared_count += 1
|
|
349
|
+
total_cleared += 1
|
|
350
|
+
current_ph_info["cleared"] = True
|
|
351
|
+
else:
|
|
352
|
+
current_ph_info["reason"] = "no text content or not clearable type"
|
|
353
|
+
|
|
354
|
+
placeholders_on_slide_info.append(current_ph_info)
|
|
355
|
+
|
|
356
|
+
except Exception as e_ph:
|
|
357
|
+
logger.warning(f" Error clearing placeholder {ph_idx} (ID: {ph_access_id if 'ph_access_id' in locals() else 'N/A'}) on slide {slide_idx + 1}: {str(e_ph)}")
|
|
358
|
+
placeholders_on_slide_info.append({"access_id": ph_access_id if 'ph_access_id' in locals() else 'N/A', "type": ph_type_name if 'ph_type_name' in locals() else 'N/A', "cleared": False, "reason": f"error: {str(e_ph)}"})
|
|
359
|
+
|
|
360
|
+
processed_slides_info.append({
|
|
361
|
+
"slide_number": slide_idx + 1,
|
|
362
|
+
"cleared_count_on_slide": slide_cleared_count,
|
|
363
|
+
"placeholders_status": placeholders_on_slide_info
|
|
364
|
+
})
|
|
365
|
+
if slide_cleared_count > 0:
|
|
366
|
+
slides_processed += 1
|
|
367
|
+
|
|
368
|
+
prs.save(final_output_path)
|
|
369
|
+
logger.info(f"Presentation with cleared content saved to {final_output_path}")
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
"status": "success",
|
|
373
|
+
"message": f"Successfully cleared content from {total_cleared} placeholder(s) in {slides_processed} slide(s).",
|
|
374
|
+
"data": {
|
|
375
|
+
"slides_processed_count": slides_processed,
|
|
376
|
+
"placeholders_cleared_total": total_cleared,
|
|
377
|
+
"slides_targetted_count": len(slides_to_process_indices),
|
|
378
|
+
"processed_slides_details": processed_slides_info
|
|
379
|
+
},
|
|
380
|
+
"output_path": final_output_path
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
logger.error(f"Error in clear_placeholder_content for {prs_path}: {str(e)}\n{traceback.format_exc()}")
|
|
385
|
+
return {
|
|
386
|
+
"status": "error",
|
|
387
|
+
"message": f"清空占位符内容失败: {str(e)}",
|
|
388
|
+
"data": {"placeholders_cleared_total": total_cleared if 'total_cleared' in locals() else 0, "slides_processed_count": slides_processed if 'slides_processed' in locals() else 0},
|
|
389
|
+
"output_path": None
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
def assign_placeholder_content(prs_path: str, slide_index: int, placeholder_access_id: int, content: str, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
393
|
+
"""
|
|
394
|
+
给演示文稿中特定幻灯片的特定占位符赋值,并返回操作结果。
|
|
395
|
+
"""
|
|
396
|
+
try:
|
|
397
|
+
prs = Presentation(prs_path)
|
|
398
|
+
|
|
399
|
+
if not (0 <= slide_index < len(prs.slides)):
|
|
400
|
+
message = f"幻灯片索引 {slide_index} 超出范围 (共 {len(prs.slides)} 张幻灯片)。"
|
|
401
|
+
logger.error(message)
|
|
402
|
+
return {"status": "error", "message": message, "data": None, "output_path": None}
|
|
403
|
+
|
|
404
|
+
slide = prs.slides[slide_index]
|
|
405
|
+
target_placeholder = None
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
# Attempt to access placeholder by integer index first if access_id is int
|
|
409
|
+
if isinstance(placeholder_access_id, int):
|
|
410
|
+
# Check if the direct index access is valid for the slide's placeholders list
|
|
411
|
+
if 0 <= placeholder_access_id < len(slide.placeholders):
|
|
412
|
+
# This assumes placeholder_access_id might be used as a direct 0-based index
|
|
413
|
+
# However, python-pptx usually relies on placeholder_format.idx for uniqueness if available
|
|
414
|
+
# For robustness, we should iterate or use a more reliable way if idx is not a direct list index.
|
|
415
|
+
pass # This path might need more thought if access_id isn't a direct list index
|
|
416
|
+
|
|
417
|
+
# Standard way: iterate and check placeholder_format.idx
|
|
418
|
+
found_by_idx_check = False
|
|
419
|
+
for ph in slide.placeholders:
|
|
420
|
+
if ph.placeholder_format.idx == placeholder_access_id:
|
|
421
|
+
target_placeholder = ph
|
|
422
|
+
found_by_idx_check = True
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
if not found_by_idx_check:
|
|
426
|
+
# Fallback: if placeholder_access_id was meant as a direct 0-based index for slide.placeholders list
|
|
427
|
+
# This is less robust as placeholder_format.idx is the true unique ID from the layout.
|
|
428
|
+
if isinstance(placeholder_access_id, int) and 0 <= placeholder_access_id < len(slide.placeholders):
|
|
429
|
+
target_placeholder = slide.placeholders[placeholder_access_id]
|
|
430
|
+
logger.warning(f"Accessed placeholder on slide {slide_index + 1} by direct list index {placeholder_access_id} as fallback. "
|
|
431
|
+
f"Prefer using placeholder_format.idx for 'placeholder_access_id'.")
|
|
432
|
+
else:
|
|
433
|
+
raise KeyError # Trigger the common error handling below
|
|
434
|
+
|
|
435
|
+
except KeyError:
|
|
436
|
+
available_phs_info = []
|
|
437
|
+
for ph_item in slide.placeholders:
|
|
438
|
+
available_phs_info.append({
|
|
439
|
+
"access_id": ph_item.placeholder_format.idx,
|
|
440
|
+
"type": get_placeholder_type_name(ph_item.placeholder_format.type)
|
|
441
|
+
})
|
|
442
|
+
message = f"在幻灯片 {slide_index + 1} 上未找到访问ID为 {placeholder_access_id} 的占位符。"
|
|
443
|
+
logger.error(f"{message} Layout: '{slide.slide_layout.name}'. Available IDs: {available_phs_info}")
|
|
444
|
+
return {"status": "error", "message": message, "data": {"available_placeholders": available_phs_info}, "output_path": None}
|
|
445
|
+
|
|
446
|
+
if target_placeholder is None: # Should be caught by KeyError, but as a safeguard
|
|
447
|
+
message = f"无法通过访问ID {placeholder_access_id} 找到占位符 (safeguard check)."
|
|
448
|
+
logger.error(message)
|
|
449
|
+
return {"status": "error", "message": message, "data": None, "output_path": None}
|
|
450
|
+
|
|
451
|
+
logger.info(f"Successfully found placeholder with access_id {placeholder_access_id} on slide {slide_index+1}. Type: {get_placeholder_type_name(target_placeholder.placeholder_format.type)}")
|
|
452
|
+
|
|
453
|
+
assigned = False
|
|
454
|
+
assignment_method = "none"
|
|
455
|
+
if hasattr(target_placeholder, 'text_frame') and target_placeholder.text_frame is not None:
|
|
456
|
+
target_placeholder.text_frame.text = str(content)
|
|
457
|
+
assigned = True
|
|
458
|
+
assignment_method = "text_frame"
|
|
459
|
+
logger.info(f" Assigned content to placeholder's text_frame.")
|
|
460
|
+
elif hasattr(target_placeholder, 'text'):
|
|
461
|
+
target_placeholder.text = str(content)
|
|
462
|
+
assigned = True
|
|
463
|
+
assignment_method = "text_property"
|
|
464
|
+
logger.info(f" Assigned content to placeholder's text property.")
|
|
465
|
+
else:
|
|
466
|
+
ph_type_name = get_placeholder_type_name(target_placeholder.placeholder_format.type)
|
|
467
|
+
logger.warning(f"Placeholder type '{ph_type_name}' (ID: {placeholder_access_id}) might not support direct text assignment.")
|
|
468
|
+
|
|
469
|
+
if not assigned:
|
|
470
|
+
message = f"未能成功赋值内容到占位符ID {placeholder_access_id} (类型: {get_placeholder_type_name(target_placeholder.placeholder_format.type)})."
|
|
471
|
+
logger.error(message)
|
|
472
|
+
return {"status": "error", "message": message, "data": {"placeholder_type": get_placeholder_type_name(target_placeholder.placeholder_format.type)}, "output_path": None}
|
|
473
|
+
|
|
474
|
+
base_name_cleaned = cleanup_filename(os.path.splitext(os.path.basename(prs_path))[0])
|
|
475
|
+
final_output_path = output_path or get_default_output_path(
|
|
476
|
+
base_name=base_name_cleaned,
|
|
477
|
+
op_type=f"assigned_S{slide_index}_P{placeholder_access_id}"
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
prs.save(final_output_path)
|
|
481
|
+
logger.info(f"Presentation with assigned content saved to {final_output_path}")
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
"status": "success",
|
|
485
|
+
"message": f"Successfully assigned content to placeholder ID {placeholder_access_id} on slide {slide_index + 1}.",
|
|
486
|
+
"data": {
|
|
487
|
+
"slide_index": slide_index,
|
|
488
|
+
"placeholder_access_id": placeholder_access_id,
|
|
489
|
+
"content_assigned_length": len(str(content)),
|
|
490
|
+
"assignment_method": assignment_method
|
|
491
|
+
},
|
|
492
|
+
"output_path": final_output_path
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
except Exception as e:
|
|
496
|
+
logger.error(f"Error in assign_placeholder_content for {prs_path}, slide {slide_index}, ph_id {placeholder_access_id}: {str(e)}\n{traceback.format_exc()}")
|
|
497
|
+
return {
|
|
498
|
+
"status": "error",
|
|
499
|
+
"message": f"给占位符赋值时发生严重错误: {str(e)}",
|
|
500
|
+
"data": None,
|
|
501
|
+
"output_path": None
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
# ... other functions will follow (placeholder for future, or end of relevant functions)
|
|
505
|
+
|
|
506
|
+
# ... other functions will follow ...
|