# Directory Structure
```
├── .gitignore
├── README.md
└── server.py
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | __pycache__
2 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP PPTX Server
2 | This is a FastMCP server that can be used to create and manipulate PowerPoints.
3 |
4 | ## How to install to Claude Desktop
5 | ```bash
6 | fastmcp install .\server.py
7 | ```
```
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | FastMCP PowerPoint Manager
3 | Give Claude tools to create and manipulate PowerPoint presentations.
4 | """
5 | from fastmcp import FastMCP, Image
6 | import io
7 | from typing import List, Optional, Dict, Any, Union
8 | from pydantic import BaseModel, Field
9 |
10 | # Create server with dependencies
11 | mcp = FastMCP(
12 | "PowerPoint Manager",
13 | dependencies=["python-pptx", "Pillow"]
14 | )
15 |
16 | # === Models for structured inputs ===
17 |
18 | class TextFormatting(BaseModel):
19 | """Text formatting options"""
20 | font_name: Optional[str] = Field(None, description="Font name (e.g., 'Calibri', 'Arial')")
21 | font_size: Optional[int] = Field(None, description="Font size in points (e.g., 12, 18, 24)")
22 | bold: Optional[bool] = Field(None, description="Bold text")
23 | italic: Optional[bool] = Field(None, description="Italic text")
24 | color: Optional[str] = Field(None, description="Text color in hex (e.g., '#000000' for black)")
25 |
26 | class BulletPoint(BaseModel):
27 | """Model for a bullet point with text and level"""
28 | text: str = Field(..., description="Text content of the bullet point")
29 | level: int = Field(0, description="Indentation level (0 = main, 1 = sub-bullet, etc.)")
30 | formatting: Optional[TextFormatting] = Field(None, description="Optional formatting for this bullet point")
31 |
32 | class Slide(BaseModel):
33 | """Model for slide content with professional formatting"""
34 | title: Optional[str] = Field(None, description="Slide title")
35 | subtitle: Optional[str] = Field(None, description="Slide subtitle (for title slides)")
36 | bullets: Optional[List[BulletPoint]] = Field(None, description="List of bullet points with levels")
37 | content: Optional[str] = Field(None, description="Plain text content (as alternative to bullets)")
38 | layout: str = Field("Title and Content", description="Slide layout name")
39 | title_formatting: Optional[TextFormatting] = Field(None, description="Formatting for the title")
40 | content_formatting: Optional[TextFormatting] = Field(None, description="Base formatting for content")
41 | notes: Optional[str] = Field(None, description="Speaker notes for this slide")
42 |
43 | class TextPosition(BaseModel):
44 | """Model for text position and formatting"""
45 | left: float = Field(..., description="Left position in inches")
46 | top: float = Field(..., description="Top position in inches")
47 | width: Optional[float] = Field(None, description="Width in inches")
48 | height: Optional[float] = Field(None, description="Height in inches")
49 | formatting: Optional[TextFormatting] = Field(None, description="Text formatting options")
50 |
51 | class PresentationTheme(BaseModel):
52 | """Model for presentation-wide theme settings"""
53 | title_font: Optional[TextFormatting] = Field(None, description="Formatting for slide titles")
54 | body_font: Optional[TextFormatting] = Field(None, description="Formatting for body text")
55 | background_color: Optional[str] = Field(None, description="Background color in hex")
56 |
57 | # === Helper Functions ===
58 |
59 | def apply_text_formatting(text_frame, formatting):
60 | """Apply text formatting to a text frame"""
61 | from pptx.dml.color import RGBColor
62 | from pptx.util import Pt
63 |
64 | if not formatting:
65 | return
66 |
67 | for paragraph in text_frame.paragraphs:
68 | for run in paragraph.runs:
69 | if formatting.font_name:
70 | run.font.name = formatting.font_name
71 |
72 | if formatting.font_size:
73 | run.font.size = Pt(formatting.font_size)
74 |
75 | if formatting.bold is not None:
76 | run.font.bold = formatting.bold
77 |
78 | if formatting.italic is not None:
79 | run.font.italic = formatting.italic
80 |
81 | if formatting.color:
82 | # Convert hex color to RGB
83 | color = formatting.color.lstrip('#')
84 | r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
85 | run.font.color.rgb = RGBColor(r, g, b)
86 |
87 | def apply_theme_to_presentation(prs, theme):
88 | """Apply a theme to all slides in the presentation"""
89 | if not theme:
90 | return
91 |
92 | # Apply to each slide
93 | for slide in prs.slides:
94 | # Apply title formatting
95 | if theme.title_font and slide.shapes.title:
96 | apply_text_formatting(slide.shapes.title.text_frame, theme.title_font)
97 |
98 | # Apply body formatting to all text placeholders
99 | if theme.body_font:
100 | for shape in slide.placeholders:
101 | if hasattr(shape, 'text_frame'):
102 | apply_text_formatting(shape.text_frame, theme.body_font)
103 |
104 | # Apply background color
105 | if theme.background_color:
106 | color = theme.background_color.lstrip('#')
107 | r, g, b = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
108 | from pptx.dml.color import RGBColor
109 | slide.background.fill.solid()
110 | slide.background.fill.fore_color.rgb = RGBColor(r, g, b)
111 |
112 | # === Core Presentation Tools ===
113 |
114 | @mcp.tool()
115 | def create_presentation(title: str, slides: List[Slide], theme: Optional[PresentationTheme] = None) -> str:
116 | """
117 | Create a new PowerPoint presentation with the given title and slides.
118 | Applies professional formatting and layout.
119 | Returns the filename of the saved presentation.
120 | """
121 | from pptx import Presentation
122 | from pptx.util import Pt
123 | import os
124 |
125 | # Create a new presentation
126 | prs = Presentation()
127 |
128 | # Create a user-accessible directory
129 | user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
130 | os.makedirs(user_docs, exist_ok=True)
131 |
132 | # Process each slide
133 | for slide_data in slides:
134 | # Get the appropriate layout
135 | layout_name = slide_data.layout
136 | layout = None
137 |
138 | # Find the right layout by name
139 | for layout_idx, layout_obj in enumerate(prs.slide_layouts):
140 | if layout_obj.name.lower() == layout_name.lower():
141 | layout = layout_obj
142 | break
143 |
144 | # If layout not found, default to Title and Content
145 | if not layout:
146 | layout = prs.slide_layouts[1] # Title and Content
147 |
148 | # Add a slide
149 | slide = prs.slides.add_slide(layout)
150 |
151 | # Set title if applicable and if the slide has a title placeholder
152 | if slide_data.title and hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title:
153 | slide.shapes.title.text = slide_data.title
154 |
155 | # Apply title formatting
156 | if slide_data.title_formatting:
157 | apply_text_formatting(slide.shapes.title.text_frame, slide_data.title_formatting)
158 |
159 | # Set subtitle if applicable (usually for title slides)
160 | if slide_data.subtitle:
161 | for shape in slide.placeholders:
162 | if shape.placeholder_format.type == 2: # Subtitle placeholder
163 | shape.text = slide_data.subtitle
164 | break
165 |
166 | # Add content - either as bullets or plain text
167 | if slide_data.bullets or slide_data.content:
168 | content_shape = None
169 |
170 | # Find the content placeholder
171 | for shape in slide.placeholders:
172 | if shape.placeholder_format.type not in [1, 2]: # Not title or subtitle
173 | content_shape = shape
174 | break
175 |
176 | if content_shape:
177 | text_frame = content_shape.text_frame
178 | text_frame.clear() # Clear any default text
179 |
180 | # If using structured bullets
181 | if slide_data.bullets:
182 | for i, bullet in enumerate(slide_data.bullets):
183 | if i == 0:
184 | p = text_frame.paragraphs[0]
185 | else:
186 | p = text_frame.add_paragraph()
187 |
188 | p.text = bullet.text
189 | p.level = bullet.level
190 |
191 | # Apply individual bullet formatting if specified
192 | if bullet.formatting:
193 | for run in p.runs:
194 | if bullet.formatting.font_name:
195 | run.font.name = bullet.formatting.font_name
196 | if bullet.formatting.font_size:
197 | run.font.size = Pt(bullet.formatting.font_size)
198 | if bullet.formatting.bold is not None:
199 | run.font.bold = bullet.formatting.bold
200 | if bullet.formatting.italic is not None:
201 | run.font.italic = bullet.formatting.italic
202 |
203 | # If using simple content
204 | elif slide_data.content:
205 | lines = slide_data.content.strip().split('\n')
206 | for i, line in enumerate(lines):
207 | if i == 0:
208 | p = text_frame.paragraphs[0]
209 | else:
210 | p = text_frame.add_paragraph()
211 | p.text = line
212 |
213 | # Apply overall content formatting
214 | if slide_data.content_formatting:
215 | apply_text_formatting(text_frame, slide_data.content_formatting)
216 |
217 | # Add speaker notes if provided
218 | if slide_data.notes:
219 | notes_slide = slide.notes_slide
220 | notes_slide.notes_text_frame.text = slide_data.notes
221 |
222 | # Apply theme if provided
223 | if theme:
224 | apply_theme_to_presentation(prs, theme)
225 |
226 | # Save the presentation in the user-accessible directory
227 | filename = f"{title.replace(' ', '_')}.pptx"
228 | full_path = os.path.join(user_docs, filename)
229 | prs.save(full_path)
230 |
231 | return f"Created presentation at: {full_path}"
232 |
233 |
234 | @mcp.tool()
235 | def add_slide(filename: str, new_slide: Slide) -> str:
236 | """
237 | Add a professionally formatted slide to an existing presentation.
238 | Returns the filename of the updated presentation.
239 | """
240 | from pptx import Presentation
241 | import os
242 |
243 | # If filename doesn't include path, assume it's in the user directory
244 | if not os.path.dirname(filename):
245 | user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
246 | filename = os.path.join(user_docs, filename)
247 |
248 | # Check if file exists
249 | if not os.path.exists(filename):
250 | return f"Error: File not found: {filename}"
251 |
252 | # Open the existing presentation
253 | prs = Presentation(filename)
254 |
255 | # Get the appropriate layout
256 | layout_name = new_slide.layout
257 | layout = None
258 | for layout_idx, layout_obj in enumerate(prs.slide_layouts):
259 | if layout_obj.name.lower() == layout_name.lower():
260 | layout = layout_obj
261 | break
262 |
263 | # If layout not found, default to Title and Content
264 | if not layout:
265 | layout = prs.slide_layouts[1] # Title and Content
266 |
267 | # Add a slide
268 | slide = prs.slides.add_slide(layout)
269 |
270 | # Set title if applicable
271 | if new_slide.title and hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title:
272 | slide.shapes.title.text = new_slide.title
273 |
274 | # Apply title formatting
275 | if new_slide.title_formatting:
276 | apply_text_formatting(slide.shapes.title.text_frame, new_slide.title_formatting)
277 |
278 | # Set subtitle if applicable (usually for title slides)
279 | if new_slide.subtitle:
280 | for shape in slide.placeholders:
281 | if shape.placeholder_format.type == 2: # Subtitle placeholder
282 | shape.text = new_slide.subtitle
283 | break
284 |
285 | # Add content - either as bullets or plain text
286 | if new_slide.bullets or new_slide.content:
287 | content_shape = None
288 |
289 | # Find the content placeholder
290 | for shape in slide.placeholders:
291 | if shape.placeholder_format.type not in [1, 2]: # Not title or subtitle
292 | content_shape = shape
293 | break
294 |
295 | if content_shape:
296 | text_frame = content_shape.text_frame
297 | text_frame.clear() # Clear any default text
298 |
299 | # If using structured bullets
300 | if new_slide.bullets:
301 | for i, bullet in enumerate(new_slide.bullets):
302 | if i == 0:
303 | p = text_frame.paragraphs[0]
304 | else:
305 | p = text_frame.add_paragraph()
306 |
307 | p.text = bullet.text
308 | p.level = bullet.level
309 |
310 | # Apply individual bullet formatting if specified
311 | if bullet.formatting:
312 | for run in p.runs:
313 | if bullet.formatting.font_name:
314 | run.font.name = bullet.formatting.font_name
315 | if bullet.formatting.font_size:
316 | run.font.size = Pt(bullet.formatting.font_size)
317 | if bullet.formatting.bold is not None:
318 | run.font.bold = bullet.formatting.bold
319 | if bullet.formatting.italic is not None:
320 | run.font.italic = bullet.formatting.italic
321 |
322 | # If using simple content
323 | elif new_slide.content:
324 | lines = new_slide.content.strip().split('\n')
325 | for i, line in enumerate(lines):
326 | if i == 0:
327 | p = text_frame.paragraphs[0]
328 | else:
329 | p = text_frame.add_paragraph()
330 | p.text = line
331 |
332 | # Apply overall content formatting
333 | if new_slide.content_formatting:
334 | apply_text_formatting(text_frame, new_slide.content_formatting)
335 |
336 | # Add speaker notes if provided
337 | if new_slide.notes:
338 | notes_slide = slide.notes_slide
339 | notes_slide.notes_text_frame.text = new_slide.notes
340 |
341 | # Save the presentation
342 | prs.save(filename)
343 | return f"Updated presentation at: {filename}"
344 |
345 |
346 | @mcp.tool()
347 | def list_available_layouts() -> str:
348 | """
349 | List all available slide layouts with descriptions to help create professional slides.
350 | """
351 | from pptx import Presentation
352 |
353 | # Create a temporary presentation to inspect layouts
354 | prs = Presentation()
355 |
356 | layouts = []
357 | layouts.append("# Available Slide Layouts\n")
358 | layouts.append("Use these layouts when creating slides for professional presentations:\n")
359 |
360 | layout_descriptions = {
361 | "Title Slide": "Main title slide with title and subtitle",
362 | "Title and Content": "Standard slide with title and content area (bullets, text, etc.)",
363 | "Section Header": "Section divider slide with large title",
364 | "Two Content": "Side-by-side content areas with title",
365 | "Comparison": "Side-by-side content with title and headings for comparison",
366 | "Title Only": "Just a title, rest of slide is blank for custom content",
367 | "Blank": "Completely blank slide with no placeholders",
368 | "Content with Caption": "Content with side caption text",
369 | "Picture with Caption": "Picture with caption text below"
370 | }
371 |
372 | for i, layout in enumerate(prs.slide_layouts):
373 | layout_name = layout.name
374 | description = layout_descriptions.get(layout_name, "Standard slide layout")
375 | layouts.append(f"## {layout_name}")
376 | layouts.append(f"{description}")
377 |
378 | # Count placeholders to help understand what's available
379 | placeholder_counts = {}
380 | for shape in layout.placeholders:
381 | ph_type = shape.placeholder_format.type
382 | name = {1: "Title", 2: "Subtitle/Body", 3: "Date", 4: "Slide Number",
383 | 5: "Footer", 7: "Content", 18: "Picture"}.get(ph_type, f"Type {ph_type}")
384 |
385 | placeholder_counts[name] = placeholder_counts.get(name, 0) + 1
386 |
387 | if placeholder_counts:
388 | layouts.append("Contains: " + ", ".join(f"{count} {name}" for name, count in placeholder_counts.items()))
389 |
390 | layouts.append("") # Empty line between layouts
391 |
392 | return "\n".join(layouts)
393 |
394 |
395 | @mcp.tool()
396 | def get_slide_preview(filename: str, slide_index: int = 0) -> Image:
397 | """
398 | Generate a preview image of a specific slide from the presentation.
399 | Returns the slide as an image.
400 | """
401 | from pptx import Presentation
402 | from PIL import Image as PILImage, ImageDraw, ImageFont
403 | import io
404 | import os
405 |
406 | # If filename doesn't include path, assume it's in the user directory
407 | if not os.path.dirname(filename):
408 | user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
409 | filename = os.path.join(user_docs, filename)
410 |
411 | try:
412 | # Open the presentation
413 | prs = Presentation(filename)
414 |
415 | # Make sure slide index is valid
416 | if slide_index < 0 or slide_index >= len(prs.slides):
417 | slide_index = 0
418 |
419 | # Create a representation of the slide
420 | img = PILImage.new('RGB', (960, 540), color='white') # 16:9 aspect ratio
421 | draw = ImageDraw.Draw(img)
422 |
423 | # Try to get a system font
424 | try:
425 | font = ImageFont.truetype("Arial", 24)
426 | small_font = ImageFont.truetype("Arial", 16)
427 | except:
428 | font = ImageFont.load_default()
429 | small_font = ImageFont.load_default()
430 |
431 | # Draw slide frame
432 | draw.rectangle([20, 20, 940, 520], outline=(200, 200, 200))
433 |
434 | # Get slide info
435 | slide = prs.slides[slide_index]
436 |
437 | # Draw title if present
438 | if slide.shapes.title:
439 | title_text = slide.shapes.title.text
440 | draw.text((60, 40), title_text, fill=(0, 0, 0), font=font)
441 |
442 | # Draw content placeholder representation
443 | y_pos = 100
444 | for shape in slide.placeholders:
445 | if hasattr(shape, 'text') and shape.text and shape.placeholder_format.type != 1: # Not title
446 | lines = shape.text.split('\n')
447 | for line in lines:
448 | draw.text((60, y_pos), line, fill=(0, 0, 0), font=small_font)
449 | y_pos += 30
450 |
451 | # Draw slide number
452 | draw.text((880, 500), f"Slide {slide_index + 1}", fill=(100, 100, 100), font=small_font)
453 |
454 | # Save to buffer with high quality
455 | buffer = io.BytesIO()
456 | img.save(buffer, format="PNG")
457 |
458 | return Image(data=buffer.getvalue(), format="png")
459 |
460 | except Exception as e:
461 | # Create an error image
462 | img = PILImage.new('RGB', (960, 540), color=(245, 245, 245))
463 | draw = ImageDraw.Draw(img)
464 |
465 | try:
466 | font = ImageFont.truetype("Arial", 24)
467 | small_font = ImageFont.truetype("Arial", 16)
468 | except:
469 | font = ImageFont.load_default()
470 | small_font = ImageFont.load_default()
471 |
472 | draw.text((100, 100), f"Error loading slide preview:", fill=(200, 0, 0), font=font)
473 | draw.text((100, 150), str(e), fill=(100, 0, 0), font=small_font)
474 | draw.text((100, 230), f"File: {filename}", fill=(0, 0, 0), font=small_font)
475 | draw.text((100, 260), f"Slide index: {slide_index}", fill=(0, 0, 0), font=small_font)
476 |
477 | buffer = io.BytesIO()
478 | img.save(buffer, format="PNG")
479 |
480 | return Image(data=buffer.getvalue(), format="png")
481 |
482 |
483 | @mcp.tool()
484 | def apply_professional_theme(filename: str, theme_name: str = "professional") -> str:
485 | """
486 | Apply a professional theme to an existing presentation.
487 | Available themes: professional, minimal, bold, corporate, creative
488 | Returns the filename of the updated presentation.
489 | """
490 | from pptx import Presentation
491 | import os
492 |
493 | # If filename doesn't include path, assume it's in the user directory
494 | if not os.path.dirname(filename):
495 | user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
496 | filename = os.path.join(user_docs, filename)
497 |
498 | # Define professional themes
499 | themes = {
500 | "professional": PresentationTheme(
501 | title_font=TextFormatting(font_name="Calibri", font_size=32, bold=True, color="#1F497D"),
502 | body_font=TextFormatting(font_name="Calibri", font_size=18, color="#333333"),
503 | background_color="#FFFFFF"
504 | ),
505 | "minimal": PresentationTheme(
506 | title_font=TextFormatting(font_name="Arial", font_size=36, bold=True, color="#333333"),
507 | body_font=TextFormatting(font_name="Arial", font_size=20, color="#505050"),
508 | background_color="#F7F7F7"
509 | ),
510 | "bold": PresentationTheme(
511 | title_font=TextFormatting(font_name="Verdana", font_size=40, bold=True, color="#FFFFFF"),
512 | body_font=TextFormatting(font_name="Verdana", font_size=20, color="#FFFFFF"),
513 | background_color="#2D2D2D"
514 | ),
515 | "corporate": PresentationTheme(
516 | title_font=TextFormatting(font_name="Segoe UI", font_size=36, bold=True, color="#003366"),
517 | body_font=TextFormatting(font_name="Segoe UI", font_size=18, color="#333333"),
518 | background_color="#F2F2F2"
519 | ),
520 | "creative": PresentationTheme(
521 | title_font=TextFormatting(font_name="Georgia", font_size=38, bold=True, color="#663399"),
522 | body_font=TextFormatting(font_name="Georgia", font_size=20, color="#333333"),
523 | background_color="#FFF8E1"
524 | )
525 | }
526 |
527 | # Get the appropriate theme
528 | selected_theme = themes.get(theme_name.lower(), themes["professional"])
529 |
530 | # Open the existing presentation
531 | prs = Presentation(filename)
532 |
533 | # Apply the theme
534 | apply_theme_to_presentation(prs, selected_theme)
535 |
536 | # Save the presentation
537 | prs.save(filename)
538 | return f"Applied '{theme_name}' theme to presentation at: {filename}"
539 |
540 |
541 | @mcp.tool()
542 | def get_save_location() -> str:
543 | """
544 | Returns the directory where presentations are saved.
545 | """
546 | import os
547 | user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
548 | return f"Presentations are saved to: {user_docs}"
549 |
550 |
551 | @mcp.tool()
552 | def create_professional_presentation(title: str, subtitle: str, content_slides: List[Dict[str, Any]]) -> str:
553 | """
554 | Create a professionally formatted presentation with standard slides.
555 | This is a simplified interface for creating complete presentations.
556 |
557 | Parameters:
558 | - title: Presentation title
559 | - subtitle: Presentation subtitle
560 | - content_slides: List of dictionaries with 'title' and 'points' keys (points should be a list of strings)
561 |
562 | Returns the path to the created presentation.
563 | """
564 | from pptx import Presentation
565 | import os
566 |
567 | # Create user directory
568 | user_docs = os.path.expanduser("~/Documents/ClaudePresentations")
569 | os.makedirs(user_docs, exist_ok=True)
570 |
571 | # Create a list of properly formatted slides
572 | slides = []
573 |
574 | # Add title slide
575 | title_slide = Slide(
576 | title=title,
577 | subtitle=subtitle,
578 | layout="Title Slide",
579 | title_formatting=TextFormatting(font_size=40, bold=True, color="#1F497D"),
580 | )
581 | slides.append(title_slide)
582 |
583 | # Add content slides
584 | for slide_data in content_slides:
585 | # Convert string points to proper BulletPoint objects
586 | bullet_points = []
587 | if "points" in slide_data:
588 | for point in slide_data["points"]:
589 | # Check if it's a string or already has level info
590 | if isinstance(point, str):
591 | bullet_points.append(BulletPoint(text=point, level=0))
592 | elif isinstance(point, dict) and "text" in point:
593 | level = point.get("level", 0)
594 | bullet_points.append(BulletPoint(text=point["text"], level=level))
595 |
596 | # Create the slide
597 | content_slide = Slide(
598 | title=slide_data.get("title", ""),
599 | bullets=bullet_points,
600 | layout="Title and Content",
601 | title_formatting=TextFormatting(font_size=32, bold=True, color="#1F497D"),
602 | content_formatting=TextFormatting(font_size=20, color="#333333")
603 | )
604 | slides.append(content_slide)
605 |
606 | # Define a professional theme
607 | theme = PresentationTheme(
608 | title_font=TextFormatting(font_name="Calibri", font_size=32, bold=True, color="#1F497D"),
609 | body_font=TextFormatting(font_name="Calibri", font_size=20, color="#333333"),
610 | background_color="#FFFFFF"
611 | )
612 |
613 | # Create the presentation
614 | prs = Presentation()
615 |
616 | # Process each slide
617 | for slide_data in slides:
618 | # Get layout
619 | layout_name = slide_data.layout
620 | layout = None
621 | for lo in prs.slide_layouts:
622 | if lo.name.lower() == layout_name.lower():
623 | layout = lo
624 | break
625 |
626 | if not layout:
627 | layout = prs.slide_layouts[1] # Default to Title and Content
628 |
629 | # Add slide
630 | slide = prs.slides.add_slide(layout)
631 |
632 | # Set title
633 | if slide_data.title and slide.shapes.title:
634 | slide.shapes.title.text = slide_data.title
635 | apply_text_formatting(slide.shapes.title.text_frame, slide_data.title_formatting)
636 |
637 | # Set subtitle (for title slide)
638 | if slide_data.subtitle:
639 | for shape in slide.placeholders:
640 | if shape.placeholder_format.type == 2: # Subtitle
641 | shape.text = slide_data.subtitle
642 | break
643 |
644 | # Add bullets
645 | if slide_data.bullets:
646 | for shape in slide.placeholders:
647 | if shape.placeholder_format.type not in [1, 2]: # Not title or subtitle
648 | text_frame = shape.text_frame
649 | text_frame.clear()
650 |
651 | for i, bullet in enumerate(slide_data.bullets):
652 | if i == 0:
653 | p = text_frame.paragraphs[0]
654 | else:
655 | p = text_frame.add_paragraph()
656 |
657 | p.text = bullet.text
658 | p.level = bullet.level
659 |
660 | # Apply formatting
661 | if slide_data.content_formatting:
662 | apply_text_formatting(text_frame, slide_data.content_formatting)
663 |
664 | break
665 |
666 | # Apply theme
667 | apply_theme_to_presentation(prs, theme)
668 |
669 | # Save file
670 | filename = f"{title.replace(' ', '_')}.pptx"
671 | full_path = os.path.join(user_docs, filename)
672 | prs.save(full_path)
673 |
674 | return f"Created professional presentation at: {full_path}"
```