mcpcn-office-powerpoint-mcp-server 2.0.7__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.
utils/content_utils.py ADDED
@@ -0,0 +1,579 @@
1
+ """
2
+ Content management utilities for PowerPoint MCP Server.
3
+ Functions for slides, text, images, tables, charts, and shapes.
4
+ """
5
+ from pptx import Presentation
6
+ from pptx.chart.data import CategoryChartData
7
+ from pptx.enum.chart import XL_CHART_TYPE
8
+ from pptx.enum.text import PP_ALIGN
9
+ from pptx.util import Inches, Pt
10
+ from pptx.dml.color import RGBColor
11
+ from typing import Dict, List, Tuple, Optional, Any
12
+ import tempfile
13
+ import os
14
+ import base64
15
+
16
+
17
+ def add_slide(presentation: Presentation, layout_index: int = 1) -> Tuple:
18
+ """
19
+ Add a slide to the presentation.
20
+
21
+ Args:
22
+ presentation: The Presentation object
23
+ layout_index: Index of the slide layout to use
24
+
25
+ Returns:
26
+ A tuple containing the slide and its layout
27
+ """
28
+ layout = presentation.slide_layouts[layout_index]
29
+ slide = presentation.slides.add_slide(layout)
30
+ return slide, layout
31
+
32
+
33
+ def get_slide_info(slide, slide_index: int) -> Dict:
34
+ """
35
+ Get information about a specific slide.
36
+
37
+ Args:
38
+ slide: The slide object
39
+ slide_index: Index of the slide
40
+
41
+ Returns:
42
+ Dictionary containing slide information
43
+ """
44
+ try:
45
+ placeholders = []
46
+ for placeholder in slide.placeholders:
47
+ placeholder_info = {
48
+ "idx": placeholder.placeholder_format.idx,
49
+ "type": str(placeholder.placeholder_format.type),
50
+ "name": placeholder.name
51
+ }
52
+ placeholders.append(placeholder_info)
53
+
54
+ shapes = []
55
+ for i, shape in enumerate(slide.shapes):
56
+ shape_info = {
57
+ "index": i,
58
+ "name": shape.name,
59
+ "shape_type": str(shape.shape_type),
60
+ "left": shape.left,
61
+ "top": shape.top,
62
+ "width": shape.width,
63
+ "height": shape.height
64
+ }
65
+ shapes.append(shape_info)
66
+
67
+ return {
68
+ "slide_index": slide_index,
69
+ "layout_name": slide.slide_layout.name,
70
+ "placeholder_count": len(placeholders),
71
+ "placeholders": placeholders,
72
+ "shape_count": len(shapes),
73
+ "shapes": shapes
74
+ }
75
+ except Exception as e:
76
+ raise Exception(f"Failed to get slide info: {str(e)}")
77
+
78
+
79
+ def set_title(slide, title: str) -> None:
80
+ """
81
+ Set the title of a slide.
82
+
83
+ Args:
84
+ slide: The slide object
85
+ title: The title text
86
+ """
87
+ if slide.shapes.title:
88
+ slide.shapes.title.text = title
89
+
90
+
91
+ def populate_placeholder(slide, placeholder_idx: int, text: str) -> None:
92
+ """
93
+ Populate a placeholder with text.
94
+
95
+ Args:
96
+ slide: The slide object
97
+ placeholder_idx: The index of the placeholder
98
+ text: The text to add
99
+ """
100
+ placeholder = slide.placeholders[placeholder_idx]
101
+ placeholder.text = text
102
+
103
+
104
+ def add_bullet_points(placeholder, bullet_points: List[str]) -> None:
105
+ """
106
+ Add bullet points to a placeholder.
107
+
108
+ Args:
109
+ placeholder: The placeholder object
110
+ bullet_points: List of bullet point texts
111
+ """
112
+ text_frame = placeholder.text_frame
113
+ text_frame.clear()
114
+
115
+ for i, point in enumerate(bullet_points):
116
+ p = text_frame.add_paragraph()
117
+ p.text = point
118
+ p.level = 0
119
+
120
+
121
+ def add_textbox(slide, left: float, top: float, width: float, height: float, text: str,
122
+ font_size: int = None, font_name: str = None, bold: bool = None,
123
+ italic: bool = None, underline: bool = None,
124
+ color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
125
+ alignment: str = None, vertical_alignment: str = None,
126
+ auto_fit: bool = True) -> Any:
127
+ """
128
+ Add a textbox to a slide with formatting options.
129
+
130
+ Args:
131
+ slide: The slide object
132
+ left: Left position in inches
133
+ top: Top position in inches
134
+ width: Width in inches
135
+ height: Height in inches
136
+ text: Text content
137
+ font_size: Font size in points
138
+ font_name: Font name
139
+ bold: Whether text should be bold
140
+ italic: Whether text should be italic
141
+ underline: Whether text should be underlined
142
+ color: RGB color tuple (r, g, b)
143
+ bg_color: Background RGB color tuple (r, g, b)
144
+ alignment: Text alignment ('left', 'center', 'right', 'justify')
145
+ vertical_alignment: Vertical alignment ('top', 'middle', 'bottom')
146
+ auto_fit: Whether to auto-fit text
147
+
148
+ Returns:
149
+ The created textbox shape
150
+ """
151
+ textbox = slide.shapes.add_textbox(
152
+ Inches(left), Inches(top), Inches(width), Inches(height)
153
+ )
154
+
155
+ textbox.text_frame.text = text
156
+
157
+ # Apply formatting if provided
158
+ if any([font_size, font_name, bold, italic, underline, color, bg_color, alignment, vertical_alignment]):
159
+ format_text_advanced(
160
+ textbox.text_frame,
161
+ font_size=font_size,
162
+ font_name=font_name,
163
+ bold=bold,
164
+ italic=italic,
165
+ underline=underline,
166
+ color=color,
167
+ bg_color=bg_color,
168
+ alignment=alignment,
169
+ vertical_alignment=vertical_alignment
170
+ )
171
+
172
+ return textbox
173
+
174
+
175
+ def format_text(text_frame, font_size: int = None, font_name: str = None,
176
+ bold: bool = None, italic: bool = None, color: Tuple[int, int, int] = None,
177
+ alignment: str = None) -> None:
178
+ """
179
+ Format text in a text frame.
180
+
181
+ Args:
182
+ text_frame: The text frame to format
183
+ font_size: Font size in points
184
+ font_name: Font name
185
+ bold: Whether text should be bold
186
+ italic: Whether text should be italic
187
+ color: RGB color tuple (r, g, b)
188
+ alignment: Text alignment ('left', 'center', 'right', 'justify')
189
+ """
190
+ alignment_map = {
191
+ 'left': PP_ALIGN.LEFT,
192
+ 'center': PP_ALIGN.CENTER,
193
+ 'right': PP_ALIGN.RIGHT,
194
+ 'justify': PP_ALIGN.JUSTIFY
195
+ }
196
+
197
+ for paragraph in text_frame.paragraphs:
198
+ if alignment and alignment in alignment_map:
199
+ paragraph.alignment = alignment_map[alignment]
200
+
201
+ for run in paragraph.runs:
202
+ font = run.font
203
+
204
+ if font_size is not None:
205
+ font.size = Pt(font_size)
206
+ if font_name is not None:
207
+ font.name = font_name
208
+ if bold is not None:
209
+ font.bold = bold
210
+ if italic is not None:
211
+ font.italic = italic
212
+ if color is not None:
213
+ r, g, b = color
214
+ font.color.rgb = RGBColor(r, g, b)
215
+
216
+
217
+ def format_text_advanced(text_frame, font_size: int = None, font_name: str = None,
218
+ bold: bool = None, italic: bool = None, underline: bool = None,
219
+ color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
220
+ alignment: str = None, vertical_alignment: str = None) -> Dict:
221
+ """
222
+ Advanced text formatting with comprehensive options.
223
+
224
+ Args:
225
+ text_frame: The text frame to format
226
+ font_size: Font size in points
227
+ font_name: Font name
228
+ bold: Whether text should be bold
229
+ italic: Whether text should be italic
230
+ underline: Whether text should be underlined
231
+ color: RGB color tuple (r, g, b)
232
+ bg_color: Background RGB color tuple (r, g, b)
233
+ alignment: Text alignment ('left', 'center', 'right', 'justify')
234
+ vertical_alignment: Vertical alignment ('top', 'middle', 'bottom')
235
+
236
+ Returns:
237
+ Dictionary with formatting results
238
+ """
239
+ result = {
240
+ 'success': True,
241
+ 'warnings': []
242
+ }
243
+
244
+ try:
245
+ alignment_map = {
246
+ 'left': PP_ALIGN.LEFT,
247
+ 'center': PP_ALIGN.CENTER,
248
+ 'right': PP_ALIGN.RIGHT,
249
+ 'justify': PP_ALIGN.JUSTIFY
250
+ }
251
+
252
+ # Enable text wrapping
253
+ text_frame.word_wrap = True
254
+
255
+ # Apply formatting to all paragraphs and runs
256
+ for paragraph in text_frame.paragraphs:
257
+ if alignment and alignment in alignment_map:
258
+ paragraph.alignment = alignment_map[alignment]
259
+
260
+ for run in paragraph.runs:
261
+ font = run.font
262
+
263
+ if font_size is not None:
264
+ font.size = Pt(font_size)
265
+ if font_name is not None:
266
+ font.name = font_name
267
+ if bold is not None:
268
+ font.bold = bold
269
+ if italic is not None:
270
+ font.italic = italic
271
+ if underline is not None:
272
+ font.underline = underline
273
+ if color is not None:
274
+ r, g, b = color
275
+ font.color.rgb = RGBColor(r, g, b)
276
+
277
+ return result
278
+
279
+ except Exception as e:
280
+ result['success'] = False
281
+ result['error'] = str(e)
282
+ return result
283
+
284
+
285
+ def add_image(slide, image_path: str, left: float, top: float, width: float = None, height: float = None) -> Any:
286
+ """
287
+ Add an image to a slide.
288
+
289
+ Args:
290
+ slide: The slide object
291
+ image_path: Path to the image file
292
+ left: Left position in inches
293
+ top: Top position in inches
294
+ width: Width in inches (optional)
295
+ height: Height in inches (optional)
296
+
297
+ Returns:
298
+ The created image shape
299
+ """
300
+ if width is not None and height is not None:
301
+ return slide.shapes.add_picture(
302
+ image_path, Inches(left), Inches(top), Inches(width), Inches(height)
303
+ )
304
+ elif width is not None:
305
+ return slide.shapes.add_picture(
306
+ image_path, Inches(left), Inches(top), Inches(width)
307
+ )
308
+ elif height is not None:
309
+ return slide.shapes.add_picture(
310
+ image_path, Inches(left), Inches(top), height=Inches(height)
311
+ )
312
+ else:
313
+ return slide.shapes.add_picture(
314
+ image_path, Inches(left), Inches(top)
315
+ )
316
+
317
+
318
+ def add_table(slide, rows: int, cols: int, left: float, top: float, width: float, height: float) -> Any:
319
+ """
320
+ Add a table to a slide.
321
+
322
+ Args:
323
+ slide: The slide object
324
+ rows: Number of rows
325
+ cols: Number of columns
326
+ left: Left position in inches
327
+ top: Top position in inches
328
+ width: Width in inches
329
+ height: Height in inches
330
+
331
+ Returns:
332
+ The created table shape
333
+ """
334
+ return slide.shapes.add_table(
335
+ rows, cols, Inches(left), Inches(top), Inches(width), Inches(height)
336
+ )
337
+
338
+
339
+ def format_table_cell(cell, font_size: int = None, font_name: str = None,
340
+ bold: bool = None, italic: bool = None,
341
+ color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
342
+ alignment: str = None, vertical_alignment: str = None) -> None:
343
+ """
344
+ Format a table cell.
345
+
346
+ Args:
347
+ cell: The table cell object
348
+ font_size: Font size in points
349
+ font_name: Font name
350
+ bold: Whether text should be bold
351
+ italic: Whether text should be italic
352
+ color: RGB color tuple (r, g, b)
353
+ bg_color: Background RGB color tuple (r, g, b)
354
+ alignment: Text alignment
355
+ vertical_alignment: Vertical alignment
356
+ """
357
+ # Format text
358
+ if any([font_size, font_name, bold, italic, color, alignment]):
359
+ format_text_advanced(
360
+ cell.text_frame,
361
+ font_size=font_size,
362
+ font_name=font_name,
363
+ bold=bold,
364
+ italic=italic,
365
+ color=color,
366
+ alignment=alignment
367
+ )
368
+
369
+ # Set background color
370
+ if bg_color:
371
+ cell.fill.solid()
372
+ cell.fill.fore_color.rgb = RGBColor(*bg_color)
373
+
374
+
375
+ def add_chart(slide, chart_type: str, left: float, top: float, width: float, height: float,
376
+ categories: List[str], series_names: List[str], series_values: List[List[float]]) -> Any:
377
+ """
378
+ Add a chart to a slide.
379
+
380
+ Args:
381
+ slide: The slide object
382
+ chart_type: Type of chart ('column', 'bar', 'line', 'pie', etc.)
383
+ left: Left position in inches
384
+ top: Top position in inches
385
+ width: Width in inches
386
+ height: Height in inches
387
+ categories: List of category names
388
+ series_names: List of series names
389
+ series_values: List of value lists for each series
390
+
391
+ Returns:
392
+ The created chart object
393
+ """
394
+ # Map chart type names to enum values
395
+ chart_type_map = {
396
+ 'column': XL_CHART_TYPE.COLUMN_CLUSTERED,
397
+ 'stacked_column': XL_CHART_TYPE.COLUMN_STACKED,
398
+ 'bar': XL_CHART_TYPE.BAR_CLUSTERED,
399
+ 'stacked_bar': XL_CHART_TYPE.BAR_STACKED,
400
+ 'line': XL_CHART_TYPE.LINE,
401
+ 'line_markers': XL_CHART_TYPE.LINE_MARKERS,
402
+ 'pie': XL_CHART_TYPE.PIE,
403
+ 'doughnut': XL_CHART_TYPE.DOUGHNUT,
404
+ 'area': XL_CHART_TYPE.AREA,
405
+ 'stacked_area': XL_CHART_TYPE.AREA_STACKED,
406
+ 'scatter': XL_CHART_TYPE.XY_SCATTER,
407
+ 'radar': XL_CHART_TYPE.RADAR,
408
+ 'radar_markers': XL_CHART_TYPE.RADAR_MARKERS
409
+ }
410
+
411
+ xl_chart_type = chart_type_map.get(chart_type.lower(), XL_CHART_TYPE.COLUMN_CLUSTERED)
412
+
413
+ # Create chart data
414
+ chart_data = CategoryChartData()
415
+ chart_data.categories = categories
416
+
417
+ for i, series_name in enumerate(series_names):
418
+ if i < len(series_values):
419
+ chart_data.add_series(series_name, series_values[i])
420
+
421
+ # Add chart to slide
422
+ chart_shape = slide.shapes.add_chart(
423
+ xl_chart_type, Inches(left), Inches(top), Inches(width), Inches(height), chart_data
424
+ )
425
+
426
+ return chart_shape.chart
427
+
428
+
429
+ def format_chart(chart, has_legend: bool = True, legend_position: str = 'right',
430
+ has_data_labels: bool = False, title: str = None,
431
+ x_axis_title: str = None, y_axis_title: str = None,
432
+ color_scheme: str = None) -> None:
433
+ """
434
+ Format a chart with various options.
435
+
436
+ Args:
437
+ chart: The chart object
438
+ has_legend: Whether to show legend
439
+ legend_position: Position of legend ('right', 'top', 'bottom', 'left')
440
+ has_data_labels: Whether to show data labels
441
+ title: Chart title
442
+ x_axis_title: X-axis title
443
+ y_axis_title: Y-axis title
444
+ color_scheme: Color scheme to apply
445
+ """
446
+ try:
447
+ # Set chart title
448
+ if title:
449
+ chart.chart_title.text_frame.text = title
450
+
451
+ # Configure legend
452
+ if has_legend:
453
+ chart.has_legend = True
454
+ # Note: Legend position setting may vary by chart type
455
+ else:
456
+ chart.has_legend = False
457
+
458
+ # Configure data labels
459
+ if has_data_labels:
460
+ for series in chart.series:
461
+ series.has_data_labels = True
462
+
463
+ # Set axis titles if available
464
+ try:
465
+ if x_axis_title and hasattr(chart, 'category_axis'):
466
+ chart.category_axis.axis_title.text_frame.text = x_axis_title
467
+ if y_axis_title and hasattr(chart, 'value_axis'):
468
+ chart.value_axis.axis_title.text_frame.text = y_axis_title
469
+ except:
470
+ pass # Axis titles may not be available for all chart types
471
+
472
+ except Exception:
473
+ pass # Graceful degradation for chart formatting
474
+
475
+
476
+ def extract_slide_text_content(slide) -> Dict:
477
+ """
478
+ Extract all text content from a slide including placeholders and text shapes.
479
+
480
+ Args:
481
+ slide: The slide object to extract text from
482
+
483
+ Returns:
484
+ Dictionary containing all text content organized by source type
485
+ """
486
+ try:
487
+ text_content = {
488
+ "slide_title": "",
489
+ "placeholders": [],
490
+ "text_shapes": [],
491
+ "table_text": [],
492
+ "all_text_combined": ""
493
+ }
494
+
495
+ all_texts = []
496
+
497
+ # Extract title from slide if available
498
+ if hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title:
499
+ try:
500
+ title_text = slide.shapes.title.text_frame.text.strip()
501
+ if title_text:
502
+ text_content["slide_title"] = title_text
503
+ all_texts.append(title_text)
504
+ except:
505
+ pass
506
+
507
+ # Extract text from all shapes
508
+ for i, shape in enumerate(slide.shapes):
509
+ shape_text_info = {
510
+ "shape_index": i,
511
+ "shape_name": shape.name,
512
+ "shape_type": str(shape.shape_type),
513
+ "text": ""
514
+ }
515
+
516
+ try:
517
+ # Check if shape has text frame
518
+ if hasattr(shape, 'text_frame') and shape.text_frame:
519
+ text = shape.text_frame.text.strip()
520
+ if text:
521
+ shape_text_info["text"] = text
522
+ all_texts.append(text)
523
+
524
+ # Categorize by shape type
525
+ if hasattr(shape, 'placeholder_format'):
526
+ # This is a placeholder
527
+ placeholder_info = shape_text_info.copy()
528
+ placeholder_info["placeholder_type"] = str(shape.placeholder_format.type)
529
+ placeholder_info["placeholder_idx"] = shape.placeholder_format.idx
530
+ text_content["placeholders"].append(placeholder_info)
531
+ else:
532
+ # This is a regular text shape
533
+ text_content["text_shapes"].append(shape_text_info)
534
+
535
+ # Extract text from tables
536
+ elif hasattr(shape, 'table'):
537
+ table_texts = []
538
+ table = shape.table
539
+ for row_idx, row in enumerate(table.rows):
540
+ row_texts = []
541
+ for col_idx, cell in enumerate(row.cells):
542
+ cell_text = cell.text_frame.text.strip()
543
+ if cell_text:
544
+ row_texts.append(cell_text)
545
+ all_texts.append(cell_text)
546
+ if row_texts:
547
+ table_texts.append({
548
+ "row": row_idx,
549
+ "cells": row_texts
550
+ })
551
+
552
+ if table_texts:
553
+ text_content["table_text"].append({
554
+ "shape_index": i,
555
+ "shape_name": shape.name,
556
+ "table_content": table_texts
557
+ })
558
+
559
+ except Exception as e:
560
+ # Skip shapes that can't be processed
561
+ continue
562
+
563
+ # Combine all text
564
+ text_content["all_text_combined"] = "\n".join(all_texts)
565
+
566
+ return {
567
+ "success": True,
568
+ "text_content": text_content,
569
+ "total_text_shapes": len(text_content["placeholders"]) + len(text_content["text_shapes"]),
570
+ "has_title": bool(text_content["slide_title"]),
571
+ "has_tables": len(text_content["table_text"]) > 0
572
+ }
573
+
574
+ except Exception as e:
575
+ return {
576
+ "success": False,
577
+ "error": f"Failed to extract text content: {str(e)}",
578
+ "text_content": None
579
+ }
utils/core_utils.py ADDED
@@ -0,0 +1,55 @@
1
+ """
2
+ Core utility functions for PowerPoint MCP Server.
3
+ Basic operations and error handling.
4
+ """
5
+ from typing import Any, Callable, List, Tuple, Optional
6
+
7
+
8
+ def try_multiple_approaches(operation_name: str, approaches: List[Tuple[Callable, str]]) -> Tuple[Any, Optional[str]]:
9
+ """
10
+ Try multiple approaches to perform an operation, returning the first successful result.
11
+
12
+ Args:
13
+ operation_name: Name of the operation for error reporting
14
+ approaches: List of (approach_func, description) tuples to try
15
+
16
+ Returns:
17
+ Tuple of (result, None) if any approach succeeded, or (None, error_messages) if all failed
18
+ """
19
+ error_messages = []
20
+
21
+ for approach_func, description in approaches:
22
+ try:
23
+ result = approach_func()
24
+ return result, None
25
+ except Exception as e:
26
+ error_messages.append(f"{description}: {str(e)}")
27
+
28
+ return None, f"Failed to {operation_name} after trying multiple approaches: {'; '.join(error_messages)}"
29
+
30
+
31
+ def safe_operation(operation_name: str, operation_func: Callable, error_message: Optional[str] = None, *args, **kwargs) -> Tuple[Any, Optional[str]]:
32
+ """
33
+ Execute an operation safely with standard error handling.
34
+
35
+ Args:
36
+ operation_name: Name of the operation for error reporting
37
+ operation_func: Function to execute
38
+ error_message: Custom error message (optional)
39
+ *args, **kwargs: Arguments to pass to the operation function
40
+
41
+ Returns:
42
+ A tuple (result, error) where error is None if operation was successful
43
+ """
44
+ try:
45
+ result = operation_func(*args, **kwargs)
46
+ return result, None
47
+ except ValueError as e:
48
+ error_msg = error_message or f"Invalid input for {operation_name}: {str(e)}"
49
+ return None, error_msg
50
+ except TypeError as e:
51
+ error_msg = error_message or f"Type error in {operation_name}: {str(e)}"
52
+ return None, error_msg
53
+ except Exception as e:
54
+ error_msg = error_message or f"Failed to execute {operation_name}: {str(e)}"
55
+ return None, error_msg