mcpcn-office-powerpoint-mcp-server 2.1.0__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.
- {mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/METADATA +82 -30
- mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info/RECORD +25 -0
- {mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/WHEEL +1 -1
- {mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/licenses/LICENSE +20 -20
- ppt_mcp_server.py +474 -474
- slide_layout_templates.json +3689 -3689
- tools/__init__.py +27 -27
- tools/chart_tools.py +81 -81
- tools/connector_tools.py +90 -90
- tools/content_tools.py +966 -777
- tools/hyperlink_tools.py +137 -137
- tools/master_tools.py +113 -113
- tools/presentation_tools.py +211 -211
- tools/professional_tools.py +289 -289
- tools/structural_tools.py +372 -372
- tools/template_tools.py +520 -520
- tools/transition_tools.py +74 -74
- utils/__init__.py +69 -68
- utils/content_utils.py +633 -578
- utils/core_utils.py +54 -54
- utils/design_utils.py +688 -688
- utils/presentation_utils.py +216 -216
- utils/template_utils.py +1142 -1142
- utils/validation_utils.py +322 -322
- mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info/RECORD +0 -25
- {mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/entry_points.txt +0 -0
utils/validation_utils.py
CHANGED
|
@@ -1,323 +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:
|
|
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
323
|
return 0
|