content_utils.pyā¢19.7 kB
"""
Content management utilities for PowerPoint MCP Server.
Functions for slides, text, images, tables, charts, and shapes.
"""
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from typing import Dict, List, Tuple, Optional, Any
import tempfile
import os
import base64
def add_slide(presentation: Presentation, layout_index: int = 1) -> Tuple:
"""
Add a slide to the presentation.
Args:
presentation: The Presentation object
layout_index: Index of the slide layout to use
Returns:
A tuple containing the slide and its layout
"""
layout = presentation.slide_layouts[layout_index]
slide = presentation.slides.add_slide(layout)
return slide, layout
def get_slide_info(slide, slide_index: int) -> Dict:
"""
Get information about a specific slide.
Args:
slide: The slide object
slide_index: Index of the slide
Returns:
Dictionary containing slide information
"""
try:
placeholders = []
for placeholder in slide.placeholders:
placeholder_info = {
"idx": placeholder.placeholder_format.idx,
"type": str(placeholder.placeholder_format.type),
"name": placeholder.name
}
placeholders.append(placeholder_info)
shapes = []
for i, shape in enumerate(slide.shapes):
shape_info = {
"index": i,
"name": shape.name,
"shape_type": str(shape.shape_type),
"left": shape.left,
"top": shape.top,
"width": shape.width,
"height": shape.height
}
shapes.append(shape_info)
return {
"slide_index": slide_index,
"layout_name": slide.slide_layout.name,
"placeholder_count": len(placeholders),
"placeholders": placeholders,
"shape_count": len(shapes),
"shapes": shapes
}
except Exception as e:
raise Exception(f"Failed to get slide info: {str(e)}")
def set_title(slide, title: str) -> None:
"""
Set the title of a slide.
Args:
slide: The slide object
title: The title text
"""
if slide.shapes.title:
slide.shapes.title.text = title
def populate_placeholder(slide, placeholder_idx: int, text: str) -> None:
"""
Populate a placeholder with text.
Args:
slide: The slide object
placeholder_idx: The index of the placeholder
text: The text to add
"""
placeholder = slide.placeholders[placeholder_idx]
placeholder.text = text
def add_bullet_points(placeholder, bullet_points: List[str]) -> None:
"""
Add bullet points to a placeholder.
Args:
placeholder: The placeholder object
bullet_points: List of bullet point texts
"""
text_frame = placeholder.text_frame
text_frame.clear()
for i, point in enumerate(bullet_points):
p = text_frame.add_paragraph()
p.text = point
p.level = 0
def add_textbox(slide, left: float, top: float, width: float, height: float, text: str,
font_size: int = None, font_name: str = None, bold: bool = None,
italic: bool = None, underline: bool = None,
color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
alignment: str = None, vertical_alignment: str = None,
auto_fit: bool = True) -> Any:
"""
Add a textbox to a slide with formatting options.
Args:
slide: The slide object
left: Left position in inches
top: Top position in inches
width: Width in inches
height: Height in inches
text: Text content
font_size: Font size in points
font_name: Font name
bold: Whether text should be bold
italic: Whether text should be italic
underline: Whether text should be underlined
color: RGB color tuple (r, g, b)
bg_color: Background RGB color tuple (r, g, b)
alignment: Text alignment ('left', 'center', 'right', 'justify')
vertical_alignment: Vertical alignment ('top', 'middle', 'bottom')
auto_fit: Whether to auto-fit text
Returns:
The created textbox shape
"""
textbox = slide.shapes.add_textbox(
Inches(left), Inches(top), Inches(width), Inches(height)
)
textbox.text_frame.text = text
# Apply formatting if provided
if any([font_size, font_name, bold, italic, underline, color, bg_color, alignment, vertical_alignment]):
format_text_advanced(
textbox.text_frame,
font_size=font_size,
font_name=font_name,
bold=bold,
italic=italic,
underline=underline,
color=color,
bg_color=bg_color,
alignment=alignment,
vertical_alignment=vertical_alignment
)
return textbox
def format_text(text_frame, font_size: int = None, font_name: str = None,
bold: bool = None, italic: bool = None, color: Tuple[int, int, int] = None,
alignment: str = None) -> None:
"""
Format text in a text frame.
Args:
text_frame: The text frame to format
font_size: Font size in points
font_name: Font name
bold: Whether text should be bold
italic: Whether text should be italic
color: RGB color tuple (r, g, b)
alignment: Text alignment ('left', 'center', 'right', 'justify')
"""
alignment_map = {
'left': PP_ALIGN.LEFT,
'center': PP_ALIGN.CENTER,
'right': PP_ALIGN.RIGHT,
'justify': PP_ALIGN.JUSTIFY
}
for paragraph in text_frame.paragraphs:
if alignment and alignment in alignment_map:
paragraph.alignment = alignment_map[alignment]
for run in paragraph.runs:
font = run.font
if font_size is not None:
font.size = Pt(font_size)
if font_name is not None:
font.name = font_name
if bold is not None:
font.bold = bold
if italic is not None:
font.italic = italic
if color is not None:
r, g, b = color
font.color.rgb = RGBColor(r, g, b)
def format_text_advanced(text_frame, font_size: int = None, font_name: str = None,
bold: bool = None, italic: bool = None, underline: bool = None,
color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
alignment: str = None, vertical_alignment: str = None) -> Dict:
"""
Advanced text formatting with comprehensive options.
Args:
text_frame: The text frame to format
font_size: Font size in points
font_name: Font name
bold: Whether text should be bold
italic: Whether text should be italic
underline: Whether text should be underlined
color: RGB color tuple (r, g, b)
bg_color: Background RGB color tuple (r, g, b)
alignment: Text alignment ('left', 'center', 'right', 'justify')
vertical_alignment: Vertical alignment ('top', 'middle', 'bottom')
Returns:
Dictionary with formatting results
"""
result = {
'success': True,
'warnings': []
}
try:
alignment_map = {
'left': PP_ALIGN.LEFT,
'center': PP_ALIGN.CENTER,
'right': PP_ALIGN.RIGHT,
'justify': PP_ALIGN.JUSTIFY
}
# Enable text wrapping
text_frame.word_wrap = True
# Apply formatting to all paragraphs and runs
for paragraph in text_frame.paragraphs:
if alignment and alignment in alignment_map:
paragraph.alignment = alignment_map[alignment]
for run in paragraph.runs:
font = run.font
if font_size is not None:
font.size = Pt(font_size)
if font_name is not None:
font.name = font_name
if bold is not None:
font.bold = bold
if italic is not None:
font.italic = italic
if underline is not None:
font.underline = underline
if color is not None:
r, g, b = color
font.color.rgb = RGBColor(r, g, b)
return result
except Exception as e:
result['success'] = False
result['error'] = str(e)
return result
def add_image(slide, image_path: str, left: float, top: float, width: float = None, height: float = None) -> Any:
"""
Add an image to a slide.
Args:
slide: The slide object
image_path: Path to the image file
left: Left position in inches
top: Top position in inches
width: Width in inches (optional)
height: Height in inches (optional)
Returns:
The created image shape
"""
if width is not None and height is not None:
return slide.shapes.add_picture(
image_path, Inches(left), Inches(top), Inches(width), Inches(height)
)
elif width is not None:
return slide.shapes.add_picture(
image_path, Inches(left), Inches(top), Inches(width)
)
elif height is not None:
return slide.shapes.add_picture(
image_path, Inches(left), Inches(top), height=Inches(height)
)
else:
return slide.shapes.add_picture(
image_path, Inches(left), Inches(top)
)
def add_table(slide, rows: int, cols: int, left: float, top: float, width: float, height: float) -> Any:
"""
Add a table to a slide.
Args:
slide: The slide object
rows: Number of rows
cols: Number of columns
left: Left position in inches
top: Top position in inches
width: Width in inches
height: Height in inches
Returns:
The created table shape
"""
return slide.shapes.add_table(
rows, cols, Inches(left), Inches(top), Inches(width), Inches(height)
)
def format_table_cell(cell, font_size: int = None, font_name: str = None,
bold: bool = None, italic: bool = None,
color: Tuple[int, int, int] = None, bg_color: Tuple[int, int, int] = None,
alignment: str = None, vertical_alignment: str = None) -> None:
"""
Format a table cell.
Args:
cell: The table cell object
font_size: Font size in points
font_name: Font name
bold: Whether text should be bold
italic: Whether text should be italic
color: RGB color tuple (r, g, b)
bg_color: Background RGB color tuple (r, g, b)
alignment: Text alignment
vertical_alignment: Vertical alignment
"""
# Format text
if any([font_size, font_name, bold, italic, color, alignment]):
format_text_advanced(
cell.text_frame,
font_size=font_size,
font_name=font_name,
bold=bold,
italic=italic,
color=color,
alignment=alignment
)
# Set background color
if bg_color:
cell.fill.solid()
cell.fill.fore_color.rgb = RGBColor(*bg_color)
def add_chart(slide, chart_type: str, left: float, top: float, width: float, height: float,
categories: List[str], series_names: List[str], series_values: List[List[float]]) -> Any:
"""
Add a chart to a slide.
Args:
slide: The slide object
chart_type: Type of chart ('column', 'bar', 'line', 'pie', etc.)
left: Left position in inches
top: Top position in inches
width: Width in inches
height: Height in inches
categories: List of category names
series_names: List of series names
series_values: List of value lists for each series
Returns:
The created chart object
"""
# Map chart type names to enum values
chart_type_map = {
'column': XL_CHART_TYPE.COLUMN_CLUSTERED,
'stacked_column': XL_CHART_TYPE.COLUMN_STACKED,
'bar': XL_CHART_TYPE.BAR_CLUSTERED,
'stacked_bar': XL_CHART_TYPE.BAR_STACKED,
'line': XL_CHART_TYPE.LINE,
'line_markers': XL_CHART_TYPE.LINE_MARKERS,
'pie': XL_CHART_TYPE.PIE,
'doughnut': XL_CHART_TYPE.DOUGHNUT,
'area': XL_CHART_TYPE.AREA,
'stacked_area': XL_CHART_TYPE.AREA_STACKED,
'scatter': XL_CHART_TYPE.XY_SCATTER,
'radar': XL_CHART_TYPE.RADAR,
'radar_markers': XL_CHART_TYPE.RADAR_MARKERS
}
xl_chart_type = chart_type_map.get(chart_type.lower(), XL_CHART_TYPE.COLUMN_CLUSTERED)
# Create chart data
chart_data = CategoryChartData()
chart_data.categories = categories
for i, series_name in enumerate(series_names):
if i < len(series_values):
chart_data.add_series(series_name, series_values[i])
# Add chart to slide
chart_shape = slide.shapes.add_chart(
xl_chart_type, Inches(left), Inches(top), Inches(width), Inches(height), chart_data
)
return chart_shape.chart
def format_chart(chart, has_legend: bool = True, legend_position: str = 'right',
has_data_labels: bool = False, title: str = None,
x_axis_title: str = None, y_axis_title: str = None,
color_scheme: str = None) -> None:
"""
Format a chart with various options.
Args:
chart: The chart object
has_legend: Whether to show legend
legend_position: Position of legend ('right', 'top', 'bottom', 'left')
has_data_labels: Whether to show data labels
title: Chart title
x_axis_title: X-axis title
y_axis_title: Y-axis title
color_scheme: Color scheme to apply
"""
try:
# Set chart title
if title:
chart.chart_title.text_frame.text = title
# Configure legend
if has_legend:
chart.has_legend = True
# Note: Legend position setting may vary by chart type
else:
chart.has_legend = False
# Configure data labels
if has_data_labels:
for series in chart.series:
series.has_data_labels = True
# Set axis titles if available
try:
if x_axis_title and hasattr(chart, 'category_axis'):
chart.category_axis.axis_title.text_frame.text = x_axis_title
if y_axis_title and hasattr(chart, 'value_axis'):
chart.value_axis.axis_title.text_frame.text = y_axis_title
except:
pass # Axis titles may not be available for all chart types
except Exception:
pass # Graceful degradation for chart formatting
def extract_slide_text_content(slide) -> Dict:
"""
Extract all text content from a slide including placeholders and text shapes.
Args:
slide: The slide object to extract text from
Returns:
Dictionary containing all text content organized by source type
"""
try:
text_content = {
"slide_title": "",
"placeholders": [],
"text_shapes": [],
"table_text": [],
"all_text_combined": ""
}
all_texts = []
# Extract title from slide if available
if hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title:
try:
title_text = slide.shapes.title.text_frame.text.strip()
if title_text:
text_content["slide_title"] = title_text
all_texts.append(title_text)
except:
pass
# Extract text from all shapes
for i, shape in enumerate(slide.shapes):
shape_text_info = {
"shape_index": i,
"shape_name": shape.name,
"shape_type": str(shape.shape_type),
"text": ""
}
try:
# Check if shape has text frame
if hasattr(shape, 'text_frame') and shape.text_frame:
text = shape.text_frame.text.strip()
if text:
shape_text_info["text"] = text
all_texts.append(text)
# Categorize by shape type
if hasattr(shape, 'placeholder_format'):
# This is a placeholder
placeholder_info = shape_text_info.copy()
placeholder_info["placeholder_type"] = str(shape.placeholder_format.type)
placeholder_info["placeholder_idx"] = shape.placeholder_format.idx
text_content["placeholders"].append(placeholder_info)
else:
# This is a regular text shape
text_content["text_shapes"].append(shape_text_info)
# Extract text from tables
elif hasattr(shape, 'table'):
table_texts = []
table = shape.table
for row_idx, row in enumerate(table.rows):
row_texts = []
for col_idx, cell in enumerate(row.cells):
cell_text = cell.text_frame.text.strip()
if cell_text:
row_texts.append(cell_text)
all_texts.append(cell_text)
if row_texts:
table_texts.append({
"row": row_idx,
"cells": row_texts
})
if table_texts:
text_content["table_text"].append({
"shape_index": i,
"shape_name": shape.name,
"table_content": table_texts
})
except Exception as e:
# Skip shapes that can't be processed
continue
# Combine all text
text_content["all_text_combined"] = "\n".join(all_texts)
return {
"success": True,
"text_content": text_content,
"total_text_shapes": len(text_content["placeholders"]) + len(text_content["text_shapes"]),
"has_title": bool(text_content["slide_title"]),
"has_tables": len(text_content["table_text"]) > 0
}
except Exception as e:
return {
"success": False,
"error": f"Failed to extract text content: {str(e)}",
"text_content": None
}