validation_utils.py•11.8 kB
"""
Validation utilities for PowerPoint MCP Server.
Functions for validating and fixing slide content, text fit, and layouts.
"""
from typing import Dict, List, Optional, Any
def validate_text_fit(shape, text_content: str = None, font_size: int = 12) -> Dict:
"""
Validate if text content will fit in a shape container.
Args:
shape: The shape containing the text
text_content: The text to validate (if None, uses existing text)
font_size: The font size to check
Returns:
Dictionary with validation results and suggestions
"""
result = {
'fits': True,
'estimated_overflow': False,
'suggested_font_size': font_size,
'suggested_dimensions': None,
'warnings': [],
'needs_optimization': False
}
try:
# Use existing text if not provided
if text_content is None and hasattr(shape, 'text_frame'):
text_content = shape.text_frame.text
if not text_content:
return result
# Basic heuristic: estimate if text will overflow
if hasattr(shape, 'width') and hasattr(shape, 'height'):
# Rough estimation: average character width is about 0.6 * font_size
avg_char_width = font_size * 0.6
estimated_width = len(text_content) * avg_char_width
# Convert shape dimensions to points (assuming they're in EMU)
shape_width_pt = shape.width / 12700 # EMU to points conversion
shape_height_pt = shape.height / 12700
if estimated_width > shape_width_pt:
result['fits'] = False
result['estimated_overflow'] = True
result['needs_optimization'] = True
# Suggest smaller font size
suggested_size = int((shape_width_pt / len(text_content)) * 0.8)
result['suggested_font_size'] = max(suggested_size, 8)
# Suggest larger dimensions
result['suggested_dimensions'] = {
'width': estimated_width * 1.2,
'height': shape_height_pt
}
result['warnings'].append(
f"Text may overflow. Consider font size {result['suggested_font_size']} "
f"or increase width to {result['suggested_dimensions']['width']:.1f} points"
)
# Check for very long lines that might cause formatting issues
lines = text_content.split('\n')
max_line_length = max(len(line) for line in lines) if lines else 0
if max_line_length > 100: # Arbitrary threshold
result['warnings'].append("Very long lines detected. Consider adding line breaks.")
result['needs_optimization'] = True
return result
except Exception as e:
result['fits'] = False
result['error'] = str(e)
return result
def validate_and_fix_slide(slide, auto_fix: bool = True, min_font_size: int = 8,
max_font_size: int = 72) -> Dict:
"""
Comprehensively validate and automatically fix slide content issues.
Args:
slide: The slide object to validate
auto_fix: Whether to automatically apply fixes
min_font_size: Minimum allowed font size
max_font_size: Maximum allowed font size
Returns:
Dictionary with validation results and applied fixes
"""
result = {
'validation_passed': True,
'issues_found': [],
'fixes_applied': [],
'warnings': [],
'shapes_processed': 0,
'text_shapes_optimized': 0
}
try:
shapes_with_text = []
# Find all shapes with text content
for i, shape in enumerate(slide.shapes):
result['shapes_processed'] += 1
if hasattr(shape, 'text_frame') and shape.text_frame.text.strip():
shapes_with_text.append((i, shape))
# Validate each text shape
for shape_index, shape in shapes_with_text:
shape_name = f"Shape {shape_index}"
# Validate text fit
text_validation = validate_text_fit(shape, font_size=12)
if not text_validation['fits'] or text_validation['needs_optimization']:
issue = f"{shape_name}: Text may not fit properly"
result['issues_found'].append(issue)
result['validation_passed'] = False
if auto_fix and text_validation['suggested_font_size']:
try:
# Apply suggested font size
suggested_size = max(min_font_size,
min(text_validation['suggested_font_size'], max_font_size))
# Apply font size to all runs in the text frame
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
if hasattr(run, 'font'):
run.font.size = suggested_size * 12700 # Convert to EMU
fix = f"{shape_name}: Adjusted font size to {suggested_size}pt"
result['fixes_applied'].append(fix)
result['text_shapes_optimized'] += 1
except Exception as e:
warning = f"{shape_name}: Could not auto-fix font size: {str(e)}"
result['warnings'].append(warning)
# Check for other potential issues
if len(shape.text_frame.text) > 500: # Very long text
result['warnings'].append(f"{shape_name}: Contains very long text (>500 chars)")
# Check for empty paragraphs
empty_paragraphs = sum(1 for p in shape.text_frame.paragraphs if not p.text.strip())
if empty_paragraphs > 2:
result['warnings'].append(f"{shape_name}: Contains {empty_paragraphs} empty paragraphs")
# Check slide-level issues
if len(slide.shapes) > 20:
result['warnings'].append("Slide contains many shapes (>20), may affect performance")
# Summary
if result['validation_passed']:
result['summary'] = "Slide validation passed successfully"
else:
result['summary'] = f"Found {len(result['issues_found'])} issues"
if auto_fix:
result['summary'] += f", applied {len(result['fixes_applied'])} fixes"
return result
except Exception as e:
result['validation_passed'] = False
result['error'] = str(e)
return result
def validate_slide_layout(slide) -> Dict:
"""
Validate slide layout for common issues.
Args:
slide: The slide object
Returns:
Dictionary with layout validation results
"""
result = {
'layout_valid': True,
'issues': [],
'suggestions': [],
'shape_count': len(slide.shapes),
'overlapping_shapes': []
}
try:
shapes = list(slide.shapes)
# Check for overlapping shapes
for i, shape1 in enumerate(shapes):
for j, shape2 in enumerate(shapes[i+1:], i+1):
if shapes_overlap(shape1, shape2):
result['overlapping_shapes'].append({
'shape1_index': i,
'shape2_index': j,
'shape1_name': getattr(shape1, 'name', f'Shape {i}'),
'shape2_name': getattr(shape2, 'name', f'Shape {j}')
})
if result['overlapping_shapes']:
result['layout_valid'] = False
result['issues'].append(f"Found {len(result['overlapping_shapes'])} overlapping shapes")
result['suggestions'].append("Consider repositioning overlapping shapes")
# Check for shapes outside slide boundaries
slide_width = 10 * 914400 # Standard slide width in EMU
slide_height = 7.5 * 914400 # Standard slide height in EMU
shapes_outside = []
for i, shape in enumerate(shapes):
if (shape.left < 0 or shape.top < 0 or
shape.left + shape.width > slide_width or
shape.top + shape.height > slide_height):
shapes_outside.append(i)
if shapes_outside:
result['layout_valid'] = False
result['issues'].append(f"Found {len(shapes_outside)} shapes outside slide boundaries")
result['suggestions'].append("Reposition shapes to fit within slide boundaries")
# Check shape spacing
if len(shapes) > 1:
min_spacing = check_minimum_spacing(shapes)
if min_spacing < 0.1 * 914400: # Less than 0.1 inch spacing
result['suggestions'].append("Consider increasing spacing between shapes")
return result
except Exception as e:
result['layout_valid'] = False
result['error'] = str(e)
return result
def shapes_overlap(shape1, shape2) -> bool:
"""
Check if two shapes overlap.
Args:
shape1: First shape
shape2: Second shape
Returns:
True if shapes overlap, False otherwise
"""
try:
# Get boundaries
left1, top1 = shape1.left, shape1.top
right1, bottom1 = left1 + shape1.width, top1 + shape1.height
left2, top2 = shape2.left, shape2.top
right2, bottom2 = left2 + shape2.width, top2 + shape2.height
# Check for overlap
return not (right1 <= left2 or right2 <= left1 or bottom1 <= top2 or bottom2 <= top1)
except:
return False
def check_minimum_spacing(shapes: List) -> float:
"""
Check minimum spacing between shapes.
Args:
shapes: List of shapes
Returns:
Minimum spacing found between shapes (in EMU)
"""
min_spacing = float('inf')
try:
for i, shape1 in enumerate(shapes):
for shape2 in shapes[i+1:]:
# Calculate distance between shape edges
distance = calculate_shape_distance(shape1, shape2)
min_spacing = min(min_spacing, distance)
return min_spacing if min_spacing != float('inf') else 0
except:
return 0
def calculate_shape_distance(shape1, shape2) -> float:
"""
Calculate distance between two shapes.
Args:
shape1: First shape
shape2: Second shape
Returns:
Distance between shape edges (in EMU)
"""
try:
# Get centers
center1_x = shape1.left + shape1.width / 2
center1_y = shape1.top + shape1.height / 2
center2_x = shape2.left + shape2.width / 2
center2_y = shape2.top + shape2.height / 2
# Calculate center-to-center distance
dx = abs(center2_x - center1_x)
dy = abs(center2_y - center1_y)
# Subtract half-widths and half-heights to get edge distance
edge_distance_x = max(0, dx - (shape1.width + shape2.width) / 2)
edge_distance_y = max(0, dy - (shape1.height + shape2.height) / 2)
# Return minimum edge distance
return min(edge_distance_x, edge_distance_y)
except:
return 0