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