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.
@@ -0,0 +1,323 @@
1
+ """
2
+ Validation utilities for PowerPoint MCP Server.
3
+ Functions for validating and fixing slide content, text fit, and layouts.
4
+ """
5
+ from typing import Dict, List, Optional, Any
6
+
7
+
8
+ def validate_text_fit(shape, text_content: str = None, font_size: int = 12) -> Dict:
9
+ """
10
+ Validate if text content will fit in a shape container.
11
+
12
+ Args:
13
+ shape: The shape containing the text
14
+ text_content: The text to validate (if None, uses existing text)
15
+ font_size: The font size to check
16
+
17
+ Returns:
18
+ Dictionary with validation results and suggestions
19
+ """
20
+ result = {
21
+ 'fits': True,
22
+ 'estimated_overflow': False,
23
+ 'suggested_font_size': font_size,
24
+ 'suggested_dimensions': None,
25
+ 'warnings': [],
26
+ 'needs_optimization': False
27
+ }
28
+
29
+ try:
30
+ # Use existing text if not provided
31
+ if text_content is None and hasattr(shape, 'text_frame'):
32
+ text_content = shape.text_frame.text
33
+
34
+ if not text_content:
35
+ return result
36
+
37
+ # Basic heuristic: estimate if text will overflow
38
+ if hasattr(shape, 'width') and hasattr(shape, 'height'):
39
+ # Rough estimation: average character width is about 0.6 * font_size
40
+ avg_char_width = font_size * 0.6
41
+ estimated_width = len(text_content) * avg_char_width
42
+
43
+ # Convert shape dimensions to points (assuming they're in EMU)
44
+ shape_width_pt = shape.width / 12700 # EMU to points conversion
45
+ shape_height_pt = shape.height / 12700
46
+
47
+ if estimated_width > shape_width_pt:
48
+ result['fits'] = False
49
+ result['estimated_overflow'] = True
50
+ result['needs_optimization'] = True
51
+
52
+ # Suggest smaller font size
53
+ suggested_size = int((shape_width_pt / len(text_content)) * 0.8)
54
+ result['suggested_font_size'] = max(suggested_size, 8)
55
+
56
+ # Suggest larger dimensions
57
+ result['suggested_dimensions'] = {
58
+ 'width': estimated_width * 1.2,
59
+ 'height': shape_height_pt
60
+ }
61
+
62
+ result['warnings'].append(
63
+ f"Text may overflow. Consider font size {result['suggested_font_size']} "
64
+ f"or increase width to {result['suggested_dimensions']['width']:.1f} points"
65
+ )
66
+
67
+ # Check for very long lines that might cause formatting issues
68
+ lines = text_content.split('\n')
69
+ max_line_length = max(len(line) for line in lines) if lines else 0
70
+
71
+ if max_line_length > 100: # Arbitrary threshold
72
+ result['warnings'].append("Very long lines detected. Consider adding line breaks.")
73
+ result['needs_optimization'] = True
74
+
75
+ return result
76
+
77
+ except Exception as e:
78
+ result['fits'] = False
79
+ result['error'] = str(e)
80
+ return result
81
+
82
+
83
+ def validate_and_fix_slide(slide, auto_fix: bool = True, min_font_size: int = 8,
84
+ max_font_size: int = 72) -> Dict:
85
+ """
86
+ Comprehensively validate and automatically fix slide content issues.
87
+
88
+ Args:
89
+ slide: The slide object to validate
90
+ auto_fix: Whether to automatically apply fixes
91
+ min_font_size: Minimum allowed font size
92
+ max_font_size: Maximum allowed font size
93
+
94
+ Returns:
95
+ Dictionary with validation results and applied fixes
96
+ """
97
+ result = {
98
+ 'validation_passed': True,
99
+ 'issues_found': [],
100
+ 'fixes_applied': [],
101
+ 'warnings': [],
102
+ 'shapes_processed': 0,
103
+ 'text_shapes_optimized': 0
104
+ }
105
+
106
+ try:
107
+ shapes_with_text = []
108
+
109
+ # Find all shapes with text content
110
+ for i, shape in enumerate(slide.shapes):
111
+ result['shapes_processed'] += 1
112
+
113
+ if hasattr(shape, 'text_frame') and shape.text_frame.text.strip():
114
+ shapes_with_text.append((i, shape))
115
+
116
+ # Validate each text shape
117
+ for shape_index, shape in shapes_with_text:
118
+ shape_name = f"Shape {shape_index}"
119
+
120
+ # Validate text fit
121
+ text_validation = validate_text_fit(shape, font_size=12)
122
+
123
+ if not text_validation['fits'] or text_validation['needs_optimization']:
124
+ issue = f"{shape_name}: Text may not fit properly"
125
+ result['issues_found'].append(issue)
126
+ result['validation_passed'] = False
127
+
128
+ if auto_fix and text_validation['suggested_font_size']:
129
+ try:
130
+ # Apply suggested font size
131
+ suggested_size = max(min_font_size,
132
+ min(text_validation['suggested_font_size'], max_font_size))
133
+
134
+ # Apply font size to all runs in the text frame
135
+ for paragraph in shape.text_frame.paragraphs:
136
+ for run in paragraph.runs:
137
+ if hasattr(run, 'font'):
138
+ run.font.size = suggested_size * 12700 # Convert to EMU
139
+
140
+ fix = f"{shape_name}: Adjusted font size to {suggested_size}pt"
141
+ result['fixes_applied'].append(fix)
142
+ result['text_shapes_optimized'] += 1
143
+
144
+ except Exception as e:
145
+ warning = f"{shape_name}: Could not auto-fix font size: {str(e)}"
146
+ result['warnings'].append(warning)
147
+
148
+ # Check for other potential issues
149
+ if len(shape.text_frame.text) > 500: # Very long text
150
+ result['warnings'].append(f"{shape_name}: Contains very long text (>500 chars)")
151
+
152
+ # Check for empty paragraphs
153
+ empty_paragraphs = sum(1 for p in shape.text_frame.paragraphs if not p.text.strip())
154
+ if empty_paragraphs > 2:
155
+ result['warnings'].append(f"{shape_name}: Contains {empty_paragraphs} empty paragraphs")
156
+
157
+ # Check slide-level issues
158
+ if len(slide.shapes) > 20:
159
+ result['warnings'].append("Slide contains many shapes (>20), may affect performance")
160
+
161
+ # Summary
162
+ if result['validation_passed']:
163
+ result['summary'] = "Slide validation passed successfully"
164
+ else:
165
+ result['summary'] = f"Found {len(result['issues_found'])} issues"
166
+ if auto_fix:
167
+ result['summary'] += f", applied {len(result['fixes_applied'])} fixes"
168
+
169
+ return result
170
+
171
+ except Exception as e:
172
+ result['validation_passed'] = False
173
+ result['error'] = str(e)
174
+ return result
175
+
176
+
177
+ def validate_slide_layout(slide) -> Dict:
178
+ """
179
+ Validate slide layout for common issues.
180
+
181
+ Args:
182
+ slide: The slide object
183
+
184
+ Returns:
185
+ Dictionary with layout validation results
186
+ """
187
+ result = {
188
+ 'layout_valid': True,
189
+ 'issues': [],
190
+ 'suggestions': [],
191
+ 'shape_count': len(slide.shapes),
192
+ 'overlapping_shapes': []
193
+ }
194
+
195
+ try:
196
+ shapes = list(slide.shapes)
197
+
198
+ # Check for overlapping shapes
199
+ for i, shape1 in enumerate(shapes):
200
+ for j, shape2 in enumerate(shapes[i+1:], i+1):
201
+ if shapes_overlap(shape1, shape2):
202
+ result['overlapping_shapes'].append({
203
+ 'shape1_index': i,
204
+ 'shape2_index': j,
205
+ 'shape1_name': getattr(shape1, 'name', f'Shape {i}'),
206
+ 'shape2_name': getattr(shape2, 'name', f'Shape {j}')
207
+ })
208
+
209
+ if result['overlapping_shapes']:
210
+ result['layout_valid'] = False
211
+ result['issues'].append(f"Found {len(result['overlapping_shapes'])} overlapping shapes")
212
+ result['suggestions'].append("Consider repositioning overlapping shapes")
213
+
214
+ # Check for shapes outside slide boundaries
215
+ slide_width = 10 * 914400 # Standard slide width in EMU
216
+ slide_height = 7.5 * 914400 # Standard slide height in EMU
217
+
218
+ shapes_outside = []
219
+ for i, shape in enumerate(shapes):
220
+ if (shape.left < 0 or shape.top < 0 or
221
+ shape.left + shape.width > slide_width or
222
+ shape.top + shape.height > slide_height):
223
+ shapes_outside.append(i)
224
+
225
+ if shapes_outside:
226
+ result['layout_valid'] = False
227
+ result['issues'].append(f"Found {len(shapes_outside)} shapes outside slide boundaries")
228
+ result['suggestions'].append("Reposition shapes to fit within slide boundaries")
229
+
230
+ # Check shape spacing
231
+ if len(shapes) > 1:
232
+ min_spacing = check_minimum_spacing(shapes)
233
+ if min_spacing < 0.1 * 914400: # Less than 0.1 inch spacing
234
+ result['suggestions'].append("Consider increasing spacing between shapes")
235
+
236
+ return result
237
+
238
+ except Exception as e:
239
+ result['layout_valid'] = False
240
+ result['error'] = str(e)
241
+ return result
242
+
243
+
244
+ def shapes_overlap(shape1, shape2) -> bool:
245
+ """
246
+ Check if two shapes overlap.
247
+
248
+ Args:
249
+ shape1: First shape
250
+ shape2: Second shape
251
+
252
+ Returns:
253
+ True if shapes overlap, False otherwise
254
+ """
255
+ try:
256
+ # Get boundaries
257
+ left1, top1 = shape1.left, shape1.top
258
+ right1, bottom1 = left1 + shape1.width, top1 + shape1.height
259
+
260
+ left2, top2 = shape2.left, shape2.top
261
+ right2, bottom2 = left2 + shape2.width, top2 + shape2.height
262
+
263
+ # Check for overlap
264
+ return not (right1 <= left2 or right2 <= left1 or bottom1 <= top2 or bottom2 <= top1)
265
+ except:
266
+ return False
267
+
268
+
269
+ def check_minimum_spacing(shapes: List) -> float:
270
+ """
271
+ Check minimum spacing between shapes.
272
+
273
+ Args:
274
+ shapes: List of shapes
275
+
276
+ Returns:
277
+ Minimum spacing found between shapes (in EMU)
278
+ """
279
+ min_spacing = float('inf')
280
+
281
+ try:
282
+ for i, shape1 in enumerate(shapes):
283
+ for shape2 in shapes[i+1:]:
284
+ # Calculate distance between shape edges
285
+ distance = calculate_shape_distance(shape1, shape2)
286
+ min_spacing = min(min_spacing, distance)
287
+
288
+ return min_spacing if min_spacing != float('inf') else 0
289
+ except:
290
+ return 0
291
+
292
+
293
+ def calculate_shape_distance(shape1, shape2) -> float:
294
+ """
295
+ Calculate distance between two shapes.
296
+
297
+ Args:
298
+ shape1: First shape
299
+ shape2: Second shape
300
+
301
+ Returns:
302
+ Distance between shape edges (in EMU)
303
+ """
304
+ try:
305
+ # Get centers
306
+ center1_x = shape1.left + shape1.width / 2
307
+ center1_y = shape1.top + shape1.height / 2
308
+
309
+ center2_x = shape2.left + shape2.width / 2
310
+ center2_y = shape2.top + shape2.height / 2
311
+
312
+ # Calculate center-to-center distance
313
+ dx = abs(center2_x - center1_x)
314
+ dy = abs(center2_y - center1_y)
315
+
316
+ # Subtract half-widths and half-heights to get edge distance
317
+ edge_distance_x = max(0, dx - (shape1.width + shape2.width) / 2)
318
+ edge_distance_y = max(0, dy - (shape1.height + shape2.height) / 2)
319
+
320
+ # Return minimum edge distance
321
+ return min(edge_distance_x, edge_distance_y)
322
+ except:
323
+ return 0