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.
@@ -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 ...