mcpcn-office-powerpoint-mcp-server 2.1.1__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.1.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.1.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/WHEEL +1 -1
- {mcpcn_office_powerpoint_mcp_server-2.1.1.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 -778
- 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.1.dist-info/RECORD +0 -25
- {mcpcn_office_powerpoint_mcp_server-2.1.1.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.2.dist-info}/entry_points.txt +0 -0
tools/content_tools.py
CHANGED
|
@@ -1,779 +1,967 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Content management tools for PowerPoint MCP Server.
|
|
3
|
-
Handles slides, text, images, and content manipulation.
|
|
4
|
-
"""
|
|
5
|
-
from typing import Dict, List, Optional, Any, Union
|
|
6
|
-
from mcp.server.fastmcp import FastMCP
|
|
7
|
-
import utils as ppt_utils
|
|
8
|
-
import tempfile
|
|
9
|
-
import base64
|
|
10
|
-
import os
|
|
11
|
-
import urllib.request
|
|
12
|
-
import urllib.parse
|
|
13
|
-
|
|
14
|
-
# Optional: requests for better HTTP handling
|
|
15
|
-
try:
|
|
16
|
-
import requests # type: ignore
|
|
17
|
-
except Exception:
|
|
18
|
-
requests = None
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def register_content_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb):
|
|
22
|
-
"""Register content management tools with the FastMCP app"""
|
|
23
|
-
|
|
24
|
-
@app.tool()
|
|
25
|
-
def add_slide(
|
|
26
|
-
layout_index: int = 1,
|
|
27
|
-
title: Optional[str] = None,
|
|
28
|
-
background_type: Optional[str] = None, # "solid", "gradient", "professional_gradient"
|
|
29
|
-
background_colors: Optional[List[List[int]]] = None, # For gradient: [[start_rgb], [end_rgb]]
|
|
30
|
-
gradient_direction: str = "horizontal",
|
|
31
|
-
color_scheme: str = "modern_blue",
|
|
32
|
-
presentation_id: Optional[str] = None
|
|
33
|
-
) -> Dict:
|
|
34
|
-
"""Add a new slide to the presentation with optional background styling."""
|
|
35
|
-
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
36
|
-
|
|
37
|
-
if pres_id is None or pres_id not in presentations:
|
|
38
|
-
return {
|
|
39
|
-
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
pres = presentations[pres_id]
|
|
43
|
-
|
|
44
|
-
# Validate layout index
|
|
45
|
-
if layout_index < 0 or layout_index >= len(pres.slide_layouts):
|
|
46
|
-
return {
|
|
47
|
-
"error": f"Invalid layout index: {layout_index}. Available layouts: 0-{len(pres.slide_layouts) - 1}"
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
try:
|
|
51
|
-
# Add the slide
|
|
52
|
-
slide, layout = ppt_utils.add_slide(pres, layout_index)
|
|
53
|
-
slide_index = len(pres.slides) - 1
|
|
54
|
-
|
|
55
|
-
# Set title if provided
|
|
56
|
-
if title:
|
|
57
|
-
ppt_utils.set_title(slide, title)
|
|
58
|
-
|
|
59
|
-
# Apply background if specified
|
|
60
|
-
if background_type == "gradient" and background_colors and len(background_colors) >= 2:
|
|
61
|
-
ppt_utils.set_slide_gradient_background(
|
|
62
|
-
slide, background_colors[0], background_colors[1], gradient_direction
|
|
63
|
-
)
|
|
64
|
-
elif background_type == "professional_gradient":
|
|
65
|
-
ppt_utils.create_professional_gradient_background(
|
|
66
|
-
slide, color_scheme, "subtle", gradient_direction
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
"message": f"Added slide {slide_index} with layout {layout_index}",
|
|
71
|
-
"slide_index": slide_index,
|
|
72
|
-
"layout_name": layout.name if hasattr(layout, 'name') else f"Layout {layout_index}"
|
|
73
|
-
}
|
|
74
|
-
except Exception as e:
|
|
75
|
-
return {
|
|
76
|
-
"error": f"Failed to add slide: {str(e)}"
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
@app.tool()
|
|
80
|
-
def get_slide_info(slide_index: int, presentation_id: Optional[str] = None) -> Dict:
|
|
81
|
-
"""Get information about a specific slide."""
|
|
82
|
-
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
83
|
-
|
|
84
|
-
if pres_id is None or pres_id not in presentations:
|
|
85
|
-
return {
|
|
86
|
-
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
pres = presentations[pres_id]
|
|
90
|
-
|
|
91
|
-
if slide_index < 0 or slide_index >= len(pres.slides):
|
|
92
|
-
return {
|
|
93
|
-
"error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
slide = pres.slides[slide_index]
|
|
97
|
-
|
|
98
|
-
try:
|
|
99
|
-
return ppt_utils.get_slide_info(slide, slide_index)
|
|
100
|
-
except Exception as e:
|
|
101
|
-
return {
|
|
102
|
-
"error": f"Failed to get slide info: {str(e)}"
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
@app.tool()
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if
|
|
325
|
-
|
|
326
|
-
if
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
return {
|
|
778
|
-
"error": f"
|
|
1
|
+
"""
|
|
2
|
+
Content management tools for PowerPoint MCP Server.
|
|
3
|
+
Handles slides, text, images, and content manipulation.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Dict, List, Optional, Any, Union
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
import utils as ppt_utils
|
|
8
|
+
import tempfile
|
|
9
|
+
import base64
|
|
10
|
+
import os
|
|
11
|
+
import urllib.request
|
|
12
|
+
import urllib.parse
|
|
13
|
+
|
|
14
|
+
# Optional: requests for better HTTP handling
|
|
15
|
+
try:
|
|
16
|
+
import requests # type: ignore
|
|
17
|
+
except Exception:
|
|
18
|
+
requests = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_content_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb):
|
|
22
|
+
"""Register content management tools with the FastMCP app"""
|
|
23
|
+
|
|
24
|
+
@app.tool()
|
|
25
|
+
def add_slide(
|
|
26
|
+
layout_index: int = 1,
|
|
27
|
+
title: Optional[str] = None,
|
|
28
|
+
background_type: Optional[str] = None, # "solid", "gradient", "professional_gradient"
|
|
29
|
+
background_colors: Optional[List[List[int]]] = None, # For gradient: [[start_rgb], [end_rgb]]
|
|
30
|
+
gradient_direction: str = "horizontal",
|
|
31
|
+
color_scheme: str = "modern_blue",
|
|
32
|
+
presentation_id: Optional[str] = None
|
|
33
|
+
) -> Dict:
|
|
34
|
+
"""Add a new slide to the presentation with optional background styling."""
|
|
35
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
36
|
+
|
|
37
|
+
if pres_id is None or pres_id not in presentations:
|
|
38
|
+
return {
|
|
39
|
+
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pres = presentations[pres_id]
|
|
43
|
+
|
|
44
|
+
# Validate layout index
|
|
45
|
+
if layout_index < 0 or layout_index >= len(pres.slide_layouts):
|
|
46
|
+
return {
|
|
47
|
+
"error": f"Invalid layout index: {layout_index}. Available layouts: 0-{len(pres.slide_layouts) - 1}"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Add the slide
|
|
52
|
+
slide, layout = ppt_utils.add_slide(pres, layout_index)
|
|
53
|
+
slide_index = len(pres.slides) - 1
|
|
54
|
+
|
|
55
|
+
# Set title if provided
|
|
56
|
+
if title:
|
|
57
|
+
ppt_utils.set_title(slide, title)
|
|
58
|
+
|
|
59
|
+
# Apply background if specified
|
|
60
|
+
if background_type == "gradient" and background_colors and len(background_colors) >= 2:
|
|
61
|
+
ppt_utils.set_slide_gradient_background(
|
|
62
|
+
slide, background_colors[0], background_colors[1], gradient_direction
|
|
63
|
+
)
|
|
64
|
+
elif background_type == "professional_gradient":
|
|
65
|
+
ppt_utils.create_professional_gradient_background(
|
|
66
|
+
slide, color_scheme, "subtle", gradient_direction
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"message": f"Added slide {slide_index} with layout {layout_index}",
|
|
71
|
+
"slide_index": slide_index,
|
|
72
|
+
"layout_name": layout.name if hasattr(layout, 'name') else f"Layout {layout_index}"
|
|
73
|
+
}
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return {
|
|
76
|
+
"error": f"Failed to add slide: {str(e)}"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@app.tool()
|
|
80
|
+
def get_slide_info(slide_index: int, presentation_id: Optional[str] = None) -> Dict:
|
|
81
|
+
"""Get information about a specific slide."""
|
|
82
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
83
|
+
|
|
84
|
+
if pres_id is None or pres_id not in presentations:
|
|
85
|
+
return {
|
|
86
|
+
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pres = presentations[pres_id]
|
|
90
|
+
|
|
91
|
+
if slide_index < 0 or slide_index >= len(pres.slides):
|
|
92
|
+
return {
|
|
93
|
+
"error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
slide = pres.slides[slide_index]
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
return ppt_utils.get_slide_info(slide, slide_index)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return {
|
|
102
|
+
"error": f"Failed to get slide info: {str(e)}"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@app.tool()
|
|
106
|
+
def move_slide(
|
|
107
|
+
from_page: int,
|
|
108
|
+
to_page: Optional[int] = None,
|
|
109
|
+
to_position: Optional[str] = None,
|
|
110
|
+
file_path: Optional[str] = None,
|
|
111
|
+
save_path: Optional[str] = None,
|
|
112
|
+
presentation_id: Optional[str] = None
|
|
113
|
+
) -> Dict:
|
|
114
|
+
"""
|
|
115
|
+
移动幻灯片到新位置。
|
|
116
|
+
|
|
117
|
+
将指定页码的幻灯片移动到目标位置。
|
|
118
|
+
|
|
119
|
+
【重要】页码从 1 开始计数,与用户日常习惯一致:
|
|
120
|
+
- 第1页 = from_page 填 1
|
|
121
|
+
- 第2页 = from_page 填 2
|
|
122
|
+
- 以此类推...
|
|
123
|
+
|
|
124
|
+
目标位置有两种指定方式(二选一):
|
|
125
|
+
1. to_page: 具体页码数字(如:移动到第5页,填 5)
|
|
126
|
+
2. to_position: 相对位置关键字
|
|
127
|
+
- "first" 或 "首页" — 移动到第一页
|
|
128
|
+
- "last" 或 "末页" 或 "最后" — 移动到最后一页
|
|
129
|
+
|
|
130
|
+
参数:
|
|
131
|
+
- from_page: int — 要移动的幻灯片页码(从1开始)。例如:用户说"第2页",则传入 2
|
|
132
|
+
- to_page: Optional[int] — 目标位置页码(从1开始)。例如:用户说"移动到第5页",则传入 5
|
|
133
|
+
- to_position: Optional[str] — 目标位置关键字。可选值:"first"/"首页" 或 "last"/"末页"/"最后"
|
|
134
|
+
- file_path: Optional[str] — PPT 文件的完整路径。传入后会自动打开、操作并保存
|
|
135
|
+
- save_path: Optional[str] — 保存路径。不传则保存到原文件
|
|
136
|
+
- presentation_id: Optional[str] — 已打开的演示文稿 ID
|
|
137
|
+
|
|
138
|
+
注意:to_page 和 to_position 二选一,如果都传入,优先使用 to_position
|
|
139
|
+
|
|
140
|
+
返回:
|
|
141
|
+
- success: bool — 操作是否成功
|
|
142
|
+
- message: str — 操作结果消息
|
|
143
|
+
- from_page: int — 原页码
|
|
144
|
+
- to_page: int — 新页码
|
|
145
|
+
- total_slides: int — 幻灯片总数
|
|
146
|
+
- saved_to: str — 文件保存路径(仅 file_path 模式)
|
|
147
|
+
|
|
148
|
+
示例:
|
|
149
|
+
- 将第2页移动到第5页:
|
|
150
|
+
move_slide(from_page=2, to_page=5, file_path="D:/文档/演示文稿.pptx")
|
|
151
|
+
- 将第2页移动到最后一页:
|
|
152
|
+
move_slide(from_page=2, to_position="last", file_path="D:/文档/演示文稿.pptx")
|
|
153
|
+
- 将第5页移动到首页:
|
|
154
|
+
move_slide(from_page=5, to_position="first", file_path="D:/文档/演示文稿.pptx")
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def resolve_target_page(slide_count: int) -> int:
|
|
158
|
+
"""Resolve to_position keyword or to_page to actual page number"""
|
|
159
|
+
if to_position is not None:
|
|
160
|
+
pos_lower = to_position.lower().strip()
|
|
161
|
+
if pos_lower in ("first", "首页", "第一页", "开头"):
|
|
162
|
+
return 1
|
|
163
|
+
elif pos_lower in ("last", "末页", "最后", "最后一页", "结尾", "末尾"):
|
|
164
|
+
return slide_count
|
|
165
|
+
else:
|
|
166
|
+
raise ValueError(f"无效的 to_position: {to_position}。可选值: 'first'/'首页' 或 'last'/'末页'/'最后'")
|
|
167
|
+
elif to_page is not None:
|
|
168
|
+
return to_page
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError("必须提供 to_page(目标页码)或 to_position(目标位置如 'first'/'last')")
|
|
171
|
+
|
|
172
|
+
# Mode 1: Direct file operation
|
|
173
|
+
if file_path is not None:
|
|
174
|
+
if not os.path.exists(file_path):
|
|
175
|
+
return {
|
|
176
|
+
"error": f"文件不存在: {file_path}"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Open the presentation
|
|
181
|
+
pres = ppt_utils.open_presentation(file_path)
|
|
182
|
+
slide_count = len(pres.slides)
|
|
183
|
+
|
|
184
|
+
# Validate from_page
|
|
185
|
+
if from_page < 1 or from_page > slide_count:
|
|
186
|
+
return {
|
|
187
|
+
"error": f"无效的 from_page: {from_page}。有效范围: 1-{slide_count}(共 {slide_count} 张幻灯片)"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Resolve target page
|
|
191
|
+
try:
|
|
192
|
+
actual_to_page = resolve_target_page(slide_count)
|
|
193
|
+
except ValueError as e:
|
|
194
|
+
return {"error": str(e)}
|
|
195
|
+
|
|
196
|
+
# Validate resolved to_page
|
|
197
|
+
if actual_to_page < 1 or actual_to_page > slide_count:
|
|
198
|
+
return {
|
|
199
|
+
"error": f"无效的目标页码: {actual_to_page}。有效范围: 1-{slide_count}(共 {slide_count} 张幻灯片)"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Convert to 0-based indices
|
|
203
|
+
from_index = from_page - 1
|
|
204
|
+
to_index = actual_to_page - 1
|
|
205
|
+
|
|
206
|
+
# Move the slide
|
|
207
|
+
result = ppt_utils.move_slide(pres, from_index, to_index)
|
|
208
|
+
|
|
209
|
+
# Save the presentation
|
|
210
|
+
final_save_path = save_path if save_path else file_path
|
|
211
|
+
ppt_utils.save_presentation(pres, final_save_path)
|
|
212
|
+
|
|
213
|
+
# Build descriptive message
|
|
214
|
+
position_desc = f"第 {actual_to_page} 页"
|
|
215
|
+
if to_position:
|
|
216
|
+
pos_lower = to_position.lower().strip()
|
|
217
|
+
if pos_lower in ("first", "首页", "第一页", "开头"):
|
|
218
|
+
position_desc = "首页"
|
|
219
|
+
elif pos_lower in ("last", "末页", "最后", "最后一页", "结尾", "末尾"):
|
|
220
|
+
position_desc = f"末页(第 {actual_to_page} 页)"
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"success": True,
|
|
224
|
+
"message": f"成功将第 {from_page} 页移动到{position_desc}的位置,并已保存到 {final_save_path}",
|
|
225
|
+
"from_page": from_page,
|
|
226
|
+
"to_page": actual_to_page,
|
|
227
|
+
"total_slides": slide_count,
|
|
228
|
+
"saved_to": final_save_path
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
return {
|
|
233
|
+
"error": f"操作失败: {str(e)}"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Mode 2: Use existing presentation_id
|
|
237
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
238
|
+
|
|
239
|
+
if pres_id is None or pres_id not in presentations:
|
|
240
|
+
return {
|
|
241
|
+
"error": "请提供 file_path(文件路径)或有效的 presentation_id。如果没有打开的演示文稿,请使用 file_path 参数直接指定文件路径。"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pres = presentations[pres_id]
|
|
245
|
+
slide_count = len(pres.slides)
|
|
246
|
+
|
|
247
|
+
# Validate from_page
|
|
248
|
+
if from_page < 1 or from_page > slide_count:
|
|
249
|
+
return {
|
|
250
|
+
"error": f"无效的 from_page: {from_page}。有效范围: 1-{slide_count}(共 {slide_count} 张幻灯片)"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Resolve target page
|
|
254
|
+
try:
|
|
255
|
+
actual_to_page = resolve_target_page(slide_count)
|
|
256
|
+
except ValueError as e:
|
|
257
|
+
return {"error": str(e)}
|
|
258
|
+
|
|
259
|
+
# Validate resolved to_page
|
|
260
|
+
if actual_to_page < 1 or actual_to_page > slide_count:
|
|
261
|
+
return {
|
|
262
|
+
"error": f"无效的目标页码: {actual_to_page}。有效范围: 1-{slide_count}(共 {slide_count} 张幻灯片)"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Convert to 0-based indices
|
|
266
|
+
from_index = from_page - 1
|
|
267
|
+
to_index = actual_to_page - 1
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
result = ppt_utils.move_slide(pres, from_index, to_index)
|
|
271
|
+
|
|
272
|
+
# Build descriptive message
|
|
273
|
+
position_desc = f"第 {actual_to_page} 页"
|
|
274
|
+
if to_position:
|
|
275
|
+
pos_lower = to_position.lower().strip()
|
|
276
|
+
if pos_lower in ("first", "首页", "第一页", "开头"):
|
|
277
|
+
position_desc = "首页"
|
|
278
|
+
elif pos_lower in ("last", "末页", "最后", "最后一页", "结尾", "末尾"):
|
|
279
|
+
position_desc = f"末页(第 {actual_to_page} 页)"
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"success": True,
|
|
283
|
+
"message": f"成功将第 {from_page} 页移动到{position_desc}的位置。请记得使用 save_presentation 保存文件。",
|
|
284
|
+
"from_page": from_page,
|
|
285
|
+
"to_page": actual_to_page,
|
|
286
|
+
"total_slides": slide_count
|
|
287
|
+
}
|
|
288
|
+
except Exception as e:
|
|
289
|
+
return {
|
|
290
|
+
"error": f"移动幻灯片失败: {str(e)}"
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@app.tool()
|
|
294
|
+
def extract_slide_text(slide_index: int, presentation_id: Optional[str] = None) -> Dict:
|
|
295
|
+
"""Extract all text content from a specific slide."""
|
|
296
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
297
|
+
|
|
298
|
+
if pres_id is None or pres_id not in presentations:
|
|
299
|
+
return {
|
|
300
|
+
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
pres = presentations[pres_id]
|
|
304
|
+
|
|
305
|
+
if slide_index < 0 or slide_index >= len(pres.slides):
|
|
306
|
+
return {
|
|
307
|
+
"error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
slide = pres.slides[slide_index]
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
result = ppt_utils.extract_slide_text_content(slide)
|
|
314
|
+
result["slide_index"] = slide_index
|
|
315
|
+
return result
|
|
316
|
+
except Exception as e:
|
|
317
|
+
return {
|
|
318
|
+
"error": f"Failed to extract slide text: {str(e)}"
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@app.tool()
|
|
322
|
+
def extract_presentation_text(presentation_id: Optional[str] = None, include_slide_info: bool = True) -> Dict:
|
|
323
|
+
"""Extract all text content from all slides in the presentation."""
|
|
324
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
325
|
+
|
|
326
|
+
if pres_id is None or pres_id not in presentations:
|
|
327
|
+
return {
|
|
328
|
+
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
pres = presentations[pres_id]
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
slides_text = []
|
|
335
|
+
total_text_shapes = 0
|
|
336
|
+
slides_with_tables = 0
|
|
337
|
+
slides_with_titles = 0
|
|
338
|
+
all_presentation_text = []
|
|
339
|
+
|
|
340
|
+
for slide_index, slide in enumerate(pres.slides):
|
|
341
|
+
slide_text_result = ppt_utils.extract_slide_text_content(slide)
|
|
342
|
+
|
|
343
|
+
if slide_text_result["success"]:
|
|
344
|
+
slide_data = {
|
|
345
|
+
"slide_index": slide_index,
|
|
346
|
+
"text_content": slide_text_result["text_content"]
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if include_slide_info:
|
|
350
|
+
# Add basic slide info
|
|
351
|
+
slide_data["layout_name"] = slide.slide_layout.name
|
|
352
|
+
slide_data["total_text_shapes"] = slide_text_result["total_text_shapes"]
|
|
353
|
+
slide_data["has_title"] = slide_text_result["has_title"]
|
|
354
|
+
slide_data["has_tables"] = slide_text_result["has_tables"]
|
|
355
|
+
|
|
356
|
+
slides_text.append(slide_data)
|
|
357
|
+
|
|
358
|
+
# Accumulate statistics
|
|
359
|
+
total_text_shapes += slide_text_result["total_text_shapes"]
|
|
360
|
+
if slide_text_result["has_tables"]:
|
|
361
|
+
slides_with_tables += 1
|
|
362
|
+
if slide_text_result["has_title"]:
|
|
363
|
+
slides_with_titles += 1
|
|
364
|
+
|
|
365
|
+
# Collect all text for combined output
|
|
366
|
+
if slide_text_result["text_content"]["all_text_combined"]:
|
|
367
|
+
all_presentation_text.append(f"=== SLIDE {slide_index + 1} ===")
|
|
368
|
+
all_presentation_text.append(slide_text_result["text_content"]["all_text_combined"])
|
|
369
|
+
all_presentation_text.append("") # Empty line separator
|
|
370
|
+
else:
|
|
371
|
+
slides_text.append({
|
|
372
|
+
"slide_index": slide_index,
|
|
373
|
+
"error": slide_text_result.get("error", "Unknown error"),
|
|
374
|
+
"text_content": None
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
"success": True,
|
|
379
|
+
"presentation_id": pres_id,
|
|
380
|
+
"total_slides": len(pres.slides),
|
|
381
|
+
"slides_with_text": len([s for s in slides_text if s.get("text_content") is not None]),
|
|
382
|
+
"total_text_shapes": total_text_shapes,
|
|
383
|
+
"slides_with_titles": slides_with_titles,
|
|
384
|
+
"slides_with_tables": slides_with_tables,
|
|
385
|
+
"slides_text": slides_text,
|
|
386
|
+
"all_presentation_text_combined": "\n".join(all_presentation_text)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
except Exception as e:
|
|
390
|
+
return {
|
|
391
|
+
"error": f"Failed to extract presentation text: {str(e)}"
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@app.tool()
|
|
395
|
+
def populate_placeholder(
|
|
396
|
+
slide_index: int,
|
|
397
|
+
placeholder_idx: int,
|
|
398
|
+
text: str,
|
|
399
|
+
presentation_id: Optional[str] = None
|
|
400
|
+
) -> Dict:
|
|
401
|
+
"""Populate a placeholder with text."""
|
|
402
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
403
|
+
|
|
404
|
+
if pres_id is None or pres_id not in presentations:
|
|
405
|
+
return {
|
|
406
|
+
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
pres = presentations[pres_id]
|
|
410
|
+
|
|
411
|
+
if slide_index < 0 or slide_index >= len(pres.slides):
|
|
412
|
+
return {
|
|
413
|
+
"error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
slide = pres.slides[slide_index]
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
ppt_utils.populate_placeholder(slide, placeholder_idx, text)
|
|
420
|
+
return {
|
|
421
|
+
"message": f"Populated placeholder {placeholder_idx} on slide {slide_index}"
|
|
422
|
+
}
|
|
423
|
+
except Exception as e:
|
|
424
|
+
return {
|
|
425
|
+
"error": f"Failed to populate placeholder: {str(e)}"
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@app.tool()
|
|
429
|
+
def add_bullet_points(
|
|
430
|
+
slide_index: int,
|
|
431
|
+
placeholder_idx: int,
|
|
432
|
+
bullet_points: List[str],
|
|
433
|
+
presentation_id: Optional[str] = None
|
|
434
|
+
) -> Dict:
|
|
435
|
+
"""Add bullet points to a placeholder."""
|
|
436
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
437
|
+
|
|
438
|
+
if pres_id is None or pres_id not in presentations:
|
|
439
|
+
return {
|
|
440
|
+
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
pres = presentations[pres_id]
|
|
444
|
+
|
|
445
|
+
if slide_index < 0 or slide_index >= len(pres.slides):
|
|
446
|
+
return {
|
|
447
|
+
"error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
slide = pres.slides[slide_index]
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
placeholder = slide.placeholders[placeholder_idx]
|
|
454
|
+
ppt_utils.add_bullet_points(placeholder, bullet_points)
|
|
455
|
+
return {
|
|
456
|
+
"message": f"Added {len(bullet_points)} bullet points to placeholder {placeholder_idx} on slide {slide_index}"
|
|
457
|
+
}
|
|
458
|
+
except Exception as e:
|
|
459
|
+
return {
|
|
460
|
+
"error": f"Failed to add bullet points: {str(e)}"
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
@app.tool()
|
|
464
|
+
def manage_text(
|
|
465
|
+
slide_index: int,
|
|
466
|
+
operation: str, # "add", "format", "validate", "format_runs"
|
|
467
|
+
left: float = 1.0,
|
|
468
|
+
top: float = 1.0,
|
|
469
|
+
width: float = 4.0,
|
|
470
|
+
height: float = 2.0,
|
|
471
|
+
text: str = "",
|
|
472
|
+
shape_index: Optional[int] = None, # For format/validate operations
|
|
473
|
+
text_runs: Optional[List[Dict]] = None, # For format_runs operation
|
|
474
|
+
# Formatting options
|
|
475
|
+
font_size: Optional[int] = None,
|
|
476
|
+
font_name: Optional[str] = None,
|
|
477
|
+
bold: Optional[bool] = None,
|
|
478
|
+
italic: Optional[bool] = None,
|
|
479
|
+
underline: Optional[bool] = None,
|
|
480
|
+
color: Optional[List[int]] = None,
|
|
481
|
+
bg_color: Optional[List[int]] = None,
|
|
482
|
+
alignment: Optional[str] = None,
|
|
483
|
+
vertical_alignment: Optional[str] = None,
|
|
484
|
+
# Advanced options
|
|
485
|
+
auto_fit: bool = True,
|
|
486
|
+
validation_only: bool = False,
|
|
487
|
+
min_font_size: int = 8,
|
|
488
|
+
max_font_size: int = 72,
|
|
489
|
+
presentation_id: Optional[str] = None
|
|
490
|
+
) -> Dict:
|
|
491
|
+
"""Unified text management tool for adding, formatting, validating text, and formatting multiple text runs."""
|
|
492
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
493
|
+
|
|
494
|
+
if pres_id is None or pres_id not in presentations:
|
|
495
|
+
return {
|
|
496
|
+
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
pres = presentations[pres_id]
|
|
500
|
+
|
|
501
|
+
if slide_index < 0 or slide_index >= len(pres.slides):
|
|
502
|
+
return {
|
|
503
|
+
"error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
slide = pres.slides[slide_index]
|
|
507
|
+
|
|
508
|
+
# Validate parameters
|
|
509
|
+
validations = {}
|
|
510
|
+
if font_size is not None:
|
|
511
|
+
validations["font_size"] = (font_size, [(is_positive, "must be a positive integer")])
|
|
512
|
+
if color is not None:
|
|
513
|
+
validations["color"] = (color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")])
|
|
514
|
+
if bg_color is not None:
|
|
515
|
+
validations["bg_color"] = (bg_color, [(is_valid_rgb, "must be a valid RGB list [R, G, B] with values 0-255")])
|
|
516
|
+
|
|
517
|
+
if validations:
|
|
518
|
+
valid, error = validate_parameters(validations)
|
|
519
|
+
if not valid:
|
|
520
|
+
return {"error": error}
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
if operation == "add":
|
|
524
|
+
# Auto-detect URL even if source_type is not explicitly "url"
|
|
525
|
+
if isinstance(image_source, str) and (image_source.startswith("http://") or image_source.startswith("https://")):
|
|
526
|
+
source_type = "url"
|
|
527
|
+
# Add new textbox
|
|
528
|
+
shape = ppt_utils.add_textbox(
|
|
529
|
+
slide, left, top, width, height, text,
|
|
530
|
+
font_size=font_size,
|
|
531
|
+
font_name=font_name,
|
|
532
|
+
bold=bold,
|
|
533
|
+
italic=italic,
|
|
534
|
+
underline=underline,
|
|
535
|
+
color=tuple(color) if color else None,
|
|
536
|
+
bg_color=tuple(bg_color) if bg_color else None,
|
|
537
|
+
alignment=alignment,
|
|
538
|
+
vertical_alignment=vertical_alignment,
|
|
539
|
+
auto_fit=auto_fit
|
|
540
|
+
)
|
|
541
|
+
return {
|
|
542
|
+
"message": f"Added text box to slide {slide_index}",
|
|
543
|
+
"shape_index": len(slide.shapes) - 1,
|
|
544
|
+
"text": text
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
elif operation == "format":
|
|
548
|
+
# Format existing text shape
|
|
549
|
+
if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes):
|
|
550
|
+
return {
|
|
551
|
+
"error": f"Invalid shape index for formatting: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}"
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
shape = slide.shapes[shape_index]
|
|
555
|
+
ppt_utils.format_text_advanced(
|
|
556
|
+
shape,
|
|
557
|
+
font_size=font_size,
|
|
558
|
+
font_name=font_name,
|
|
559
|
+
bold=bold,
|
|
560
|
+
italic=italic,
|
|
561
|
+
underline=underline,
|
|
562
|
+
color=tuple(color) if color else None,
|
|
563
|
+
bg_color=tuple(bg_color) if bg_color else None,
|
|
564
|
+
alignment=alignment,
|
|
565
|
+
vertical_alignment=vertical_alignment
|
|
566
|
+
)
|
|
567
|
+
return {
|
|
568
|
+
"message": f"Formatted text shape {shape_index} on slide {slide_index}"
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
elif operation == "validate":
|
|
572
|
+
# Validate text fit
|
|
573
|
+
if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes):
|
|
574
|
+
return {
|
|
575
|
+
"error": f"Invalid shape index for validation: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}"
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
validation_result = ppt_utils.validate_text_fit(
|
|
579
|
+
slide.shapes[shape_index],
|
|
580
|
+
text_content=text or None,
|
|
581
|
+
font_size=font_size or 12
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if not validation_only and validation_result.get("needs_optimization"):
|
|
585
|
+
# Apply automatic fixes
|
|
586
|
+
fix_result = ppt_utils.validate_and_fix_slide(
|
|
587
|
+
slide,
|
|
588
|
+
auto_fix=True,
|
|
589
|
+
min_font_size=min_font_size,
|
|
590
|
+
max_font_size=max_font_size
|
|
591
|
+
)
|
|
592
|
+
validation_result.update(fix_result)
|
|
593
|
+
|
|
594
|
+
return validation_result
|
|
595
|
+
|
|
596
|
+
elif operation == "format_runs":
|
|
597
|
+
# Format multiple text runs with different formatting
|
|
598
|
+
if shape_index is None or shape_index < 0 or shape_index >= len(slide.shapes):
|
|
599
|
+
return {
|
|
600
|
+
"error": f"Invalid shape index for format_runs: {shape_index}. Available shapes: 0-{len(slide.shapes) - 1}"
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if not text_runs:
|
|
604
|
+
return {"error": "text_runs parameter is required for format_runs operation"}
|
|
605
|
+
|
|
606
|
+
shape = slide.shapes[shape_index]
|
|
607
|
+
|
|
608
|
+
# Check if shape has text
|
|
609
|
+
if not hasattr(shape, 'text_frame') or not shape.text_frame:
|
|
610
|
+
return {"error": "Shape does not contain text"}
|
|
611
|
+
|
|
612
|
+
# Clear existing text and rebuild with formatted runs
|
|
613
|
+
text_frame = shape.text_frame
|
|
614
|
+
text_frame.clear()
|
|
615
|
+
|
|
616
|
+
formatted_runs = []
|
|
617
|
+
|
|
618
|
+
for run_data in text_runs:
|
|
619
|
+
if 'text' not in run_data:
|
|
620
|
+
continue
|
|
621
|
+
|
|
622
|
+
# Add paragraph if needed
|
|
623
|
+
if not text_frame.paragraphs:
|
|
624
|
+
paragraph = text_frame.paragraphs[0]
|
|
625
|
+
else:
|
|
626
|
+
paragraph = text_frame.add_paragraph()
|
|
627
|
+
|
|
628
|
+
# Add run with text
|
|
629
|
+
run = paragraph.add_run()
|
|
630
|
+
run.text = run_data['text']
|
|
631
|
+
|
|
632
|
+
# Apply formatting using pptx imports
|
|
633
|
+
from pptx.util import Pt
|
|
634
|
+
from pptx.dml.color import RGBColor
|
|
635
|
+
|
|
636
|
+
if 'bold' in run_data:
|
|
637
|
+
run.font.bold = run_data['bold']
|
|
638
|
+
if 'italic' in run_data:
|
|
639
|
+
run.font.italic = run_data['italic']
|
|
640
|
+
if 'underline' in run_data:
|
|
641
|
+
run.font.underline = run_data['underline']
|
|
642
|
+
if 'font_size' in run_data:
|
|
643
|
+
run.font.size = Pt(run_data['font_size'])
|
|
644
|
+
if 'font_name' in run_data:
|
|
645
|
+
run.font.name = run_data['font_name']
|
|
646
|
+
if 'color' in run_data and is_valid_rgb(run_data['color']):
|
|
647
|
+
run.font.color.rgb = RGBColor(*run_data['color'])
|
|
648
|
+
if 'hyperlink' in run_data:
|
|
649
|
+
run.hyperlink.address = run_data['hyperlink']
|
|
650
|
+
|
|
651
|
+
formatted_runs.append({
|
|
652
|
+
"text": run_data['text'],
|
|
653
|
+
"formatting_applied": {k: v for k, v in run_data.items() if k != 'text'}
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
"message": f"Applied formatting to {len(formatted_runs)} text runs on shape {shape_index}",
|
|
658
|
+
"slide_index": slide_index,
|
|
659
|
+
"shape_index": shape_index,
|
|
660
|
+
"formatted_runs": formatted_runs
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
else:
|
|
664
|
+
return {
|
|
665
|
+
"error": f"Invalid operation: {operation}. Must be 'add', 'format', 'validate', or 'format_runs'"
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
except Exception as e:
|
|
669
|
+
return {
|
|
670
|
+
"error": f"Failed to {operation} text: {str(e)}"
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
@app.tool()
|
|
674
|
+
def manage_image(
|
|
675
|
+
slide_index: int,
|
|
676
|
+
operation: str, # "add", "enhance"
|
|
677
|
+
image_source: str, # file path or base64 string
|
|
678
|
+
source_type: str = "file", # "file" or "base64"
|
|
679
|
+
left: float = 1.0,
|
|
680
|
+
top: float = 1.0,
|
|
681
|
+
width: Optional[float] = None,
|
|
682
|
+
height: Optional[float] = None,
|
|
683
|
+
# Enhancement options
|
|
684
|
+
enhancement_style: Optional[str] = None, # "presentation", "custom"
|
|
685
|
+
brightness: float = 1.0,
|
|
686
|
+
contrast: float = 1.0,
|
|
687
|
+
saturation: float = 1.0,
|
|
688
|
+
sharpness: float = 1.0,
|
|
689
|
+
blur_radius: float = 0,
|
|
690
|
+
filter_type: Optional[str] = None,
|
|
691
|
+
output_path: Optional[str] = None,
|
|
692
|
+
presentation_id: Optional[str] = None
|
|
693
|
+
) -> Dict:
|
|
694
|
+
"""
|
|
695
|
+
统一的图片处理工具(添加/增强)。
|
|
696
|
+
|
|
697
|
+
功能
|
|
698
|
+
- operation="add":将图片插入到指定幻灯片位置,支持本地文件或 Base64 图片源或图片地址。
|
|
699
|
+
- operation="enhance":对已有图片文件进行画质增强与风格化处理,输出增强后的图片路径。
|
|
700
|
+
|
|
701
|
+
参数
|
|
702
|
+
- slide_index: int — 目标幻灯片索引(从 0 开始)。
|
|
703
|
+
- operation: str — "add" 或 "enhance"。
|
|
704
|
+
- image_source: str —
|
|
705
|
+
* 当 source_type="file":本地图片文件路径。
|
|
706
|
+
* 当 source_type="base64":图片的 Base64 字符串。
|
|
707
|
+
* 当 source_type="url":图片的 http/https 地址。
|
|
708
|
+
- source_type: str — "file"、"base64" 或 "url"。
|
|
709
|
+
* add 支持 "file"、"base64"、"url"(仅允许 http/https)。
|
|
710
|
+
* enhance 仅支持 "file"(不接受 base64 或 url)。
|
|
711
|
+
- left, top: float — 插入位置(英寸)。
|
|
712
|
+
- width, height: Optional[float] — 插入尺寸(英寸)。可只提供一项以按比例缩放;都不提供则按图片原始尺寸。
|
|
713
|
+
- enhancement_style: Optional[str] — "presentation" 或 "custom"。当 operation="add" 且需要自动增强时可用;"presentation" 走预设的专业增强流程。
|
|
714
|
+
- brightness, contrast, saturation, sharpness: float — 亮度/对比度/饱和度/锐度(默认 1.0,>1 增强,<1 减弱)。
|
|
715
|
+
- blur_radius: float — 模糊半径(默认 0)。
|
|
716
|
+
- filter_type: Optional[str] — 过滤器类型(如 "DETAIL"、"SMOOTH" 等,取决于 Pillow 支持)。
|
|
717
|
+
- output_path: Optional[str] — 增强后图片的输出路径(不传则生成临时文件)。
|
|
718
|
+
- presentation_id: Optional[str] — 指定演示文稿 ID;不传则使用当前打开的演示文稿。
|
|
719
|
+
注:在某些部署环境(如你当前环境)中,必须显式传入 presentation_id 才会对目标文档生效。
|
|
720
|
+
|
|
721
|
+
返回
|
|
722
|
+
- operation="add":
|
|
723
|
+
* message: str
|
|
724
|
+
* shape_index: int — 新增形状索引
|
|
725
|
+
* image_path: str(当 source_type="file" 时返回)
|
|
726
|
+
- operation="enhance":
|
|
727
|
+
* message: str
|
|
728
|
+
* enhanced_path: str — 增强后图片文件路径
|
|
729
|
+
- 失败时返回 {"error": str}
|
|
730
|
+
|
|
731
|
+
注意事项
|
|
732
|
+
- 现在支持通过 URL 插入图片(仅 operation="add"):source_type="url",仅允许 http/https 协议。
|
|
733
|
+
会在内部下载到临时文件后插入并自动清理。若返回的 Content-Type 非 image/* 将返回错误。
|
|
734
|
+
enhance 仍然仅支持本地文件路径。
|
|
735
|
+
- 在某些部署环境(如你当前环境)中,需显式提供 presentation_id 参数,否则可能插入到非预期文档或不生效。
|
|
736
|
+
- operation="enhance" 不接受 Base64,必须提供可访问的本地文件路径。
|
|
737
|
+
- 插入 Base64 图片时,内部会写入临时文件后再插入,操作完成后临时文件会被清理。
|
|
738
|
+
- slide_index 必须在当前演示文稿的有效范围内,否则将返回错误。
|
|
739
|
+
|
|
740
|
+
示例
|
|
741
|
+
- 通过 URL 插入图片(仅 add):
|
|
742
|
+
manage_image(slide_index=0, operation="add",
|
|
743
|
+
image_source="https://example.com/logo.png", source_type="url",
|
|
744
|
+
left=1.0, top=1.0, width=3.0, presentation_id="YOUR_PRESENTATION_ID")
|
|
745
|
+
- 插入本地图片:
|
|
746
|
+
manage_image(slide_index=0, operation="add",
|
|
747
|
+
image_source="D:/images/logo.png", source_type="file",
|
|
748
|
+
left=1.0, top=1.0, width=3.0, presentation_id="YOUR_PRESENTATION_ID")
|
|
749
|
+
|
|
750
|
+
- 插入 Base64 图片:
|
|
751
|
+
manage_image(slide_index=1, operation="add",
|
|
752
|
+
image_source="<BASE64字符串>", source_type="base64",
|
|
753
|
+
left=2.0, top=1.5, width=4.0, height=2.5, presentation_id="YOUR_PRESENTATION_ID")
|
|
754
|
+
|
|
755
|
+
- 插入并应用专业增强(演示风格):
|
|
756
|
+
manage_image(slide_index=2, operation="add",
|
|
757
|
+
image_source="assets/photo.jpg", source_type="file",
|
|
758
|
+
enhancement_style="presentation", left=1.0, top=2.0, presentation_id="YOUR_PRESENTATION_ID")
|
|
759
|
+
|
|
760
|
+
- 增强已有图片文件(自定义参数):
|
|
761
|
+
manage_image(slide_index=0, operation="enhance",
|
|
762
|
+
image_source="assets/photo.jpg", source_type="file",
|
|
763
|
+
brightness=1.2, contrast=1.1, saturation=1.3,
|
|
764
|
+
sharpness=1.1, blur_radius=0, filter_type=None,
|
|
765
|
+
output_path="assets/photo_enhanced.jpg", presentation_id="YOUR_PRESENTATION_ID")
|
|
766
|
+
"""
|
|
767
|
+
pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
|
|
768
|
+
|
|
769
|
+
if pres_id is None or pres_id not in presentations:
|
|
770
|
+
return {
|
|
771
|
+
"error": "No presentation is currently loaded or the specified ID is invalid"
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
pres = presentations[pres_id]
|
|
775
|
+
|
|
776
|
+
if slide_index < 0 or slide_index >= len(pres.slides):
|
|
777
|
+
return {
|
|
778
|
+
"error": f"Invalid slide index: {slide_index}. Available slides: 0-{len(pres.slides) - 1}"
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
slide = pres.slides[slide_index]
|
|
782
|
+
|
|
783
|
+
try:
|
|
784
|
+
if operation == "add":
|
|
785
|
+
if source_type == "base64":
|
|
786
|
+
# Handle base64 image
|
|
787
|
+
try:
|
|
788
|
+
image_data = base64.b64decode(image_source)
|
|
789
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
|
|
790
|
+
temp_file.write(image_data)
|
|
791
|
+
temp_path = temp_file.name
|
|
792
|
+
|
|
793
|
+
# Add image from temporary file
|
|
794
|
+
shape = ppt_utils.add_image(slide, temp_path, left, top, width, height)
|
|
795
|
+
|
|
796
|
+
# Clean up temporary file
|
|
797
|
+
os.unlink(temp_path)
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
"message": f"Added image from base64 to slide {slide_index}",
|
|
801
|
+
"shape_index": len(slide.shapes) - 1
|
|
802
|
+
}
|
|
803
|
+
except Exception as e:
|
|
804
|
+
return {
|
|
805
|
+
"error": f"Failed to process base64 image: {str(e)}"
|
|
806
|
+
}
|
|
807
|
+
elif source_type == "url":
|
|
808
|
+
# Handle image URL (http/https)
|
|
809
|
+
try:
|
|
810
|
+
# Normalize and percent-encode URL path/query to support spaces and non-ASCII characters
|
|
811
|
+
parsed = urllib.parse.urlsplit(image_source)
|
|
812
|
+
if parsed.scheme not in ("http", "https"):
|
|
813
|
+
return {"error": f"Unsupported URL scheme: {parsed.scheme}. Only http/https allowed."}
|
|
814
|
+
encoded_path = urllib.parse.quote(parsed.path or "", safe="/%")
|
|
815
|
+
# Re-encode query preserving keys and multiple values
|
|
816
|
+
qsl = urllib.parse.parse_qsl(parsed.query or "", keep_blank_values=True)
|
|
817
|
+
encoded_query = urllib.parse.urlencode(qsl, doseq=True)
|
|
818
|
+
encoded_url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, encoded_path, encoded_query, parsed.fragment))
|
|
819
|
+
|
|
820
|
+
# Download helper using requests if available, else urllib
|
|
821
|
+
content_type = None
|
|
822
|
+
temp_path = None
|
|
823
|
+
image_exts = (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tif", ".tiff")
|
|
824
|
+
|
|
825
|
+
if requests is not None:
|
|
826
|
+
with requests.get(encoded_url, stream=True) as resp:
|
|
827
|
+
if resp.status_code != 200:
|
|
828
|
+
return {"error": f"Failed to download image. HTTP {resp.status_code}"}
|
|
829
|
+
content_type = resp.headers.get("Content-Type", "") or ""
|
|
830
|
+
|
|
831
|
+
# Determine suffix and allow fallback by URL extension if Content-Type missing or not image/*
|
|
832
|
+
suffix = ".png"
|
|
833
|
+
is_image = content_type.startswith("image/")
|
|
834
|
+
try:
|
|
835
|
+
main_type = (content_type.split(";")[0].strip() if content_type else "")
|
|
836
|
+
if "/" in main_type:
|
|
837
|
+
ext = main_type.split("/")[1].lower()
|
|
838
|
+
if ext in ("jpeg", "pjpeg"):
|
|
839
|
+
suffix = ".jpg"
|
|
840
|
+
elif ext in ("png", "gif", "bmp", "webp", "tiff"):
|
|
841
|
+
suffix = f".{ext}"
|
|
842
|
+
except Exception:
|
|
843
|
+
pass
|
|
844
|
+
if suffix == ".png":
|
|
845
|
+
path_ext = os.path.splitext(parsed.path or "")[1].lower()
|
|
846
|
+
if path_ext in image_exts:
|
|
847
|
+
suffix = path_ext
|
|
848
|
+
|
|
849
|
+
if not is_image and suffix not in image_exts:
|
|
850
|
+
return {"error": f"URL content is not an image (Content-Type: {content_type or 'unknown'})"}
|
|
851
|
+
|
|
852
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
|
|
853
|
+
temp_path = temp_file.name
|
|
854
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
855
|
+
if not chunk:
|
|
856
|
+
continue
|
|
857
|
+
temp_file.write(chunk)
|
|
858
|
+
else:
|
|
859
|
+
req = urllib.request.Request(encoded_url, headers={"User-Agent": "Mozilla/5.0"})
|
|
860
|
+
with urllib.request.urlopen(req) as resp:
|
|
861
|
+
content_type = resp.headers.get("Content-Type", "") or ""
|
|
862
|
+
|
|
863
|
+
suffix = ".png"
|
|
864
|
+
is_image = content_type.startswith("image/")
|
|
865
|
+
try:
|
|
866
|
+
main_type = (content_type.split(";")[0].strip() if content_type else "")
|
|
867
|
+
if "/" in main_type:
|
|
868
|
+
ext = main_type.split("/")[1].lower()
|
|
869
|
+
if ext in ("jpeg", "pjpeg"):
|
|
870
|
+
suffix = ".jpg"
|
|
871
|
+
elif ext in ("png", "gif", "bmp", "webp", "tiff"):
|
|
872
|
+
suffix = f".{ext}"
|
|
873
|
+
except Exception:
|
|
874
|
+
pass
|
|
875
|
+
if suffix == ".png":
|
|
876
|
+
path_ext = os.path.splitext(parsed.path or "")[1].lower()
|
|
877
|
+
if path_ext in image_exts:
|
|
878
|
+
suffix = path_ext
|
|
879
|
+
|
|
880
|
+
if not is_image and suffix not in image_exts:
|
|
881
|
+
return {"error": f"URL content is not an image (Content-Type: {content_type or 'unknown'})"}
|
|
882
|
+
|
|
883
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
|
|
884
|
+
temp_path = temp_file.name
|
|
885
|
+
while True:
|
|
886
|
+
chunk = resp.read(8192)
|
|
887
|
+
if not chunk:
|
|
888
|
+
break
|
|
889
|
+
temp_file.write(chunk)
|
|
890
|
+
|
|
891
|
+
# Add image from temporary file
|
|
892
|
+
shape = ppt_utils.add_image(slide, temp_path, left, top, width, height)
|
|
893
|
+
|
|
894
|
+
# Clean up temporary file
|
|
895
|
+
if temp_path and os.path.exists(temp_path):
|
|
896
|
+
os.unlink(temp_path)
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
"message": f"Added image from URL to slide {slide_index}",
|
|
900
|
+
"shape_index": len(slide.shapes) - 1
|
|
901
|
+
}
|
|
902
|
+
except Exception as e:
|
|
903
|
+
# Best-effort cleanup if temp_path was created
|
|
904
|
+
try:
|
|
905
|
+
if temp_path and os.path.exists(temp_path):
|
|
906
|
+
os.unlink(temp_path)
|
|
907
|
+
except Exception:
|
|
908
|
+
pass
|
|
909
|
+
return {"error": f"Failed to process image URL: {str(e)}"}
|
|
910
|
+
else:
|
|
911
|
+
# Handle file path
|
|
912
|
+
if not os.path.exists(image_source):
|
|
913
|
+
return {
|
|
914
|
+
"error": f"Image file not found: {image_source}"
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
shape = ppt_utils.add_image(slide, image_source, left, top, width, height)
|
|
918
|
+
return {
|
|
919
|
+
"message": f"Added image to slide {slide_index}",
|
|
920
|
+
"shape_index": len(slide.shapes) - 1,
|
|
921
|
+
"image_path": image_source
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
elif operation == "enhance":
|
|
925
|
+
# Enhance existing image file
|
|
926
|
+
if source_type == "base64":
|
|
927
|
+
return {
|
|
928
|
+
"error": "Enhancement operation requires file path, not base64 data"
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if not os.path.exists(image_source):
|
|
932
|
+
return {
|
|
933
|
+
"error": f"Image file not found: {image_source}"
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if enhancement_style == "presentation":
|
|
937
|
+
# Apply professional enhancement
|
|
938
|
+
enhanced_path = ppt_utils.apply_professional_image_enhancement(
|
|
939
|
+
image_source, style="presentation", output_path=output_path
|
|
940
|
+
)
|
|
941
|
+
else:
|
|
942
|
+
# Apply custom enhancement
|
|
943
|
+
enhanced_path = ppt_utils.enhance_image_with_pillow(
|
|
944
|
+
image_source,
|
|
945
|
+
brightness=brightness,
|
|
946
|
+
contrast=contrast,
|
|
947
|
+
saturation=saturation,
|
|
948
|
+
sharpness=sharpness,
|
|
949
|
+
blur_radius=blur_radius,
|
|
950
|
+
filter_type=filter_type,
|
|
951
|
+
output_path=output_path
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
return {
|
|
955
|
+
"message": f"Enhanced image: {image_source}",
|
|
956
|
+
"enhanced_path": enhanced_path
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
else:
|
|
960
|
+
return {
|
|
961
|
+
"error": f"Invalid operation: {operation}. Must be 'add' or 'enhance'"
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
except Exception as e:
|
|
965
|
+
return {
|
|
966
|
+
"error": f"Failed to {operation} image: {str(e)}"
|
|
779
967
|
}
|