deckbuilder 1.0.0b1__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.
- deckbuilder/__init__.py +22 -0
- deckbuilder/cli.py +544 -0
- deckbuilder/cli_tools.py +739 -0
- deckbuilder/engine.py +1546 -0
- deckbuilder/image_handler.py +291 -0
- deckbuilder/layout_intelligence.json +288 -0
- deckbuilder/layout_intelligence.py +398 -0
- deckbuilder/naming_conventions.py +541 -0
- deckbuilder/placeholder_types.py +101 -0
- deckbuilder/placekitten_integration.py +280 -0
- deckbuilder/structured_frontmatter.py +862 -0
- deckbuilder/table_styles.py +37 -0
- deckbuilder-1.0.0b1.dist-info/METADATA +378 -0
- deckbuilder-1.0.0b1.dist-info/RECORD +37 -0
- deckbuilder-1.0.0b1.dist-info/WHEEL +5 -0
- deckbuilder-1.0.0b1.dist-info/entry_points.txt +3 -0
- deckbuilder-1.0.0b1.dist-info/licenses/LICENSE +201 -0
- deckbuilder-1.0.0b1.dist-info/top_level.txt +4 -0
- mcp_server/__init__.py +9 -0
- mcp_server/content_analysis.py +436 -0
- mcp_server/content_optimization.py +822 -0
- mcp_server/layout_recommendations.py +595 -0
- mcp_server/main.py +550 -0
- mcp_server/tools.py +492 -0
- placekitten/README.md +561 -0
- placekitten/__init__.py +44 -0
- placekitten/core.py +184 -0
- placekitten/filters.py +183 -0
- placekitten/images/ACuteKitten-1.png +0 -0
- placekitten/images/ACuteKitten-2.png +0 -0
- placekitten/images/ACuteKitten-3.png +0 -0
- placekitten/images/TwoKitttens Playing-1.png +0 -0
- placekitten/images/TwoKitttens Playing-2.png +0 -0
- placekitten/images/TwoKitttensSleeping-1.png +0 -0
- placekitten/processor.py +262 -0
- placekitten/smart_crop.py +314 -0
- shared/__init__.py +9 -0
deckbuilder/cli_tools.py
ADDED
@@ -0,0 +1,739 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Deckbuilder Command Line Tools
|
4
|
+
|
5
|
+
Standalone utilities for template analysis, documentation generation, and validation.
|
6
|
+
These tools are designed to be run independently for template management tasks.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import argparse
|
10
|
+
import json
|
11
|
+
import os
|
12
|
+
import sys
|
13
|
+
from datetime import datetime
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
# Add parent directory to path for imports
|
17
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
18
|
+
|
19
|
+
# Import after path modification - this is intentional to find the mcp_server module
|
20
|
+
from mcp_server.tools import TemplateAnalyzer # noqa: E402
|
21
|
+
|
22
|
+
try:
|
23
|
+
from .naming_conventions import NamingConvention, PlaceholderContext # noqa: E402
|
24
|
+
except ImportError:
|
25
|
+
# Handle direct script execution
|
26
|
+
from naming_conventions import NamingConvention, PlaceholderContext # noqa: E402
|
27
|
+
|
28
|
+
|
29
|
+
class TemplateManager:
|
30
|
+
"""Command-line template management utilities"""
|
31
|
+
|
32
|
+
def __init__(self, template_folder=None, output_folder=None):
|
33
|
+
# Use command-line arguments or sensible defaults
|
34
|
+
if template_folder:
|
35
|
+
self.template_folder = template_folder
|
36
|
+
else:
|
37
|
+
# Default: look for assets/templates relative to current directory
|
38
|
+
current_dir = Path.cwd()
|
39
|
+
if (current_dir / "assets" / "templates").exists():
|
40
|
+
self.template_folder = str(current_dir / "assets" / "templates")
|
41
|
+
else:
|
42
|
+
# Try from project root
|
43
|
+
project_root = Path(__file__).parent.parent.parent
|
44
|
+
self.template_folder = str(project_root / "assets" / "templates")
|
45
|
+
|
46
|
+
if output_folder:
|
47
|
+
self.output_folder = output_folder
|
48
|
+
else:
|
49
|
+
# Default: create output folder in current directory
|
50
|
+
self.output_folder = str(Path.cwd() / "template_output")
|
51
|
+
|
52
|
+
# Ensure folders exist
|
53
|
+
os.makedirs(self.output_folder, exist_ok=True)
|
54
|
+
|
55
|
+
# Don't create analyzer yet - wait until we need it
|
56
|
+
|
57
|
+
def analyze_template(self, template_name: str, verbose: bool = False) -> dict:
|
58
|
+
"""
|
59
|
+
Analyze a PowerPoint template and generate JSON mapping.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
template_name: Name of template (e.g., 'default')
|
63
|
+
verbose: Print detailed analysis information
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
Template analysis results
|
67
|
+
"""
|
68
|
+
print(f"🔍 Analyzing template: {template_name}")
|
69
|
+
print(f"📁 Template folder: {self.template_folder}")
|
70
|
+
print(f"📂 Output folder: {self.output_folder}")
|
71
|
+
|
72
|
+
try:
|
73
|
+
# Set environment variables for the analyzer
|
74
|
+
os.environ["DECK_TEMPLATE_FOLDER"] = self.template_folder
|
75
|
+
os.environ["DECK_OUTPUT_FOLDER"] = self.output_folder
|
76
|
+
|
77
|
+
# Create analyzer instance with current environment
|
78
|
+
analyzer = TemplateAnalyzer()
|
79
|
+
|
80
|
+
# Run analysis
|
81
|
+
result = analyzer.analyze_pptx_template(template_name)
|
82
|
+
|
83
|
+
# Print summary
|
84
|
+
layouts_count = len(result.get("layouts", {}))
|
85
|
+
print("✅ Analysis complete!")
|
86
|
+
print(f" 📊 Found {layouts_count} layouts")
|
87
|
+
|
88
|
+
# Print layout summary
|
89
|
+
if verbose and "layouts" in result:
|
90
|
+
print("\n📋 Layout Summary:")
|
91
|
+
for layout_name, layout_info in result["layouts"].items():
|
92
|
+
placeholder_count = len(layout_info.get("placeholders", {}))
|
93
|
+
index = layout_info.get("index", "?")
|
94
|
+
print(f" {index:2d}: {layout_name} ({placeholder_count} placeholders)")
|
95
|
+
|
96
|
+
# Check for generated file
|
97
|
+
base_name = template_name.replace(".pptx", "")
|
98
|
+
output_file = os.path.join(self.output_folder, f"{base_name}.g.json")
|
99
|
+
if os.path.exists(output_file):
|
100
|
+
print(f"📄 Generated: {output_file}")
|
101
|
+
print(" ✏️ Edit this file with semantic placeholder names")
|
102
|
+
print(f" 📝 Rename to '{base_name}.json' when ready")
|
103
|
+
|
104
|
+
return result
|
105
|
+
|
106
|
+
except Exception as e:
|
107
|
+
print(f"❌ Error analyzing template: {str(e)}")
|
108
|
+
return {}
|
109
|
+
|
110
|
+
def document_template(self, template_name: str, output_path: str = None) -> str:
|
111
|
+
"""
|
112
|
+
Generate comprehensive documentation for a template.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
template_name: Name of template to document
|
116
|
+
output_path: Custom output path (optional)
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
Path to generated documentation
|
120
|
+
"""
|
121
|
+
print(f"📝 Generating documentation for: {template_name}")
|
122
|
+
|
123
|
+
# Analyze template first
|
124
|
+
analysis = self.analyze_template(template_name, verbose=False)
|
125
|
+
if not analysis:
|
126
|
+
return ""
|
127
|
+
|
128
|
+
# Load JSON mapping if available
|
129
|
+
base_name = template_name.replace(".pptx", "")
|
130
|
+
mapping_file = os.path.join(self.template_folder, f"{base_name}.json")
|
131
|
+
mapping = {}
|
132
|
+
|
133
|
+
if os.path.exists(mapping_file):
|
134
|
+
try:
|
135
|
+
with open(mapping_file, "r", encoding="utf-8") as f:
|
136
|
+
mapping = json.load(f)
|
137
|
+
print(f"📄 Using mapping: {mapping_file}")
|
138
|
+
except Exception as e:
|
139
|
+
print(f"⚠️ Could not load mapping file: {e}")
|
140
|
+
|
141
|
+
# Generate documentation
|
142
|
+
doc_content = self._generate_template_documentation(template_name, analysis, mapping)
|
143
|
+
|
144
|
+
# Save documentation
|
145
|
+
if not output_path:
|
146
|
+
project_root = Path(__file__).parent.parent.parent
|
147
|
+
docs_folder = project_root / "docs" / "Features"
|
148
|
+
docs_folder.mkdir(parents=True, exist_ok=True)
|
149
|
+
output_path = str(docs_folder / f"{base_name.title()}Template.md")
|
150
|
+
|
151
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
152
|
+
f.write(doc_content)
|
153
|
+
|
154
|
+
print(f"✅ Documentation generated: {output_path}")
|
155
|
+
return output_path
|
156
|
+
|
157
|
+
def _generate_template_documentation(
|
158
|
+
self, template_name: str, analysis: dict, mapping: dict
|
159
|
+
) -> str:
|
160
|
+
"""Generate markdown documentation for template"""
|
161
|
+
base_name = template_name.replace(".pptx", "")
|
162
|
+
layouts = analysis.get("layouts", {})
|
163
|
+
|
164
|
+
# Generate layout summary table
|
165
|
+
table_rows = []
|
166
|
+
for layout_name, layout_info in layouts.items():
|
167
|
+
index = layout_info.get("index", "?")
|
168
|
+
placeholders = layout_info.get("placeholders", {})
|
169
|
+
placeholder_count = len(placeholders)
|
170
|
+
|
171
|
+
# Check if mapping exists
|
172
|
+
mapping_status = "✅" if mapping.get("layouts", {}).get(layout_name) else "❌"
|
173
|
+
|
174
|
+
# Check structured frontmatter support (placeholder for now)
|
175
|
+
structured_status = "⏳" # Would check structured_frontmatter.py
|
176
|
+
|
177
|
+
table_rows.append(
|
178
|
+
f"| {layout_name} | {index} | {placeholder_count} | {structured_status} | {mapping_status} |"
|
179
|
+
)
|
180
|
+
|
181
|
+
# Generate detailed layout specifications
|
182
|
+
detailed_layouts = []
|
183
|
+
for layout_name, layout_info in layouts.items():
|
184
|
+
index = layout_info.get("index", "?")
|
185
|
+
placeholders = layout_info.get("placeholders", {})
|
186
|
+
|
187
|
+
layout_section = f"### {layout_name} (Index: {index})\\n\\n"
|
188
|
+
layout_section += "**PowerPoint Placeholders**:\\n"
|
189
|
+
|
190
|
+
for idx, placeholder_name in placeholders.items():
|
191
|
+
# Handle both string and object formats
|
192
|
+
if isinstance(placeholder_name, dict):
|
193
|
+
name = placeholder_name.get("name", "Unknown")
|
194
|
+
actual_idx = placeholder_name.get("idx", idx)
|
195
|
+
else:
|
196
|
+
name = placeholder_name
|
197
|
+
actual_idx = idx
|
198
|
+
layout_section += f'- `idx={actual_idx}`: "{name}"\\n'
|
199
|
+
|
200
|
+
# Add mapping info if available
|
201
|
+
layout_mapping = mapping.get("layouts", {}).get(layout_name)
|
202
|
+
if layout_mapping:
|
203
|
+
layout_section += "\\n**JSON Mapping**: ✅ Configured\\n"
|
204
|
+
else:
|
205
|
+
layout_section += "\\n**JSON Mapping**: ❌ Not configured\\n"
|
206
|
+
|
207
|
+
layout_section += "**Structured Frontmatter**: ⏳ To be implemented\\n"
|
208
|
+
|
209
|
+
detailed_layouts.append(layout_section)
|
210
|
+
|
211
|
+
# Generate full documentation
|
212
|
+
doc_content = f"""# {base_name.title()} Template Documentation
|
213
|
+
|
214
|
+
## Template Overview
|
215
|
+
- **Template Name**: {template_name}
|
216
|
+
- **Analysis Date**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
217
|
+
- **Total Layouts**: {len(layouts)}
|
218
|
+
- **Template Location**: `{self.template_folder}/{template_name}`
|
219
|
+
|
220
|
+
## Layout Summary
|
221
|
+
|
222
|
+
| Layout Name | Index | Placeholders | Structured Support | JSON Mapping |
|
223
|
+
|-------------|-------|--------------|-------------------|--------------|
|
224
|
+
{chr(10).join(table_rows)}
|
225
|
+
|
226
|
+
## Detailed Layout Specifications
|
227
|
+
|
228
|
+
{chr(10).join(detailed_layouts)}
|
229
|
+
|
230
|
+
## Template Management
|
231
|
+
|
232
|
+
### Adding JSON Mapping
|
233
|
+
1. **Analyze template**: Run `python -m deckbuilder.cli_tools analyze {base_name}`
|
234
|
+
2. **Edit generated file**: Customize `{base_name}.g.json` with semantic names
|
235
|
+
3. **Activate mapping**: Rename to `{base_name}.json` in templates folder
|
236
|
+
|
237
|
+
### Example JSON Mapping Structure
|
238
|
+
```json
|
239
|
+
{{
|
240
|
+
"template_info": {{
|
241
|
+
"name": "{base_name.title()}",
|
242
|
+
"version": "1.0"
|
243
|
+
}},
|
244
|
+
"layouts": {{
|
245
|
+
"Title Slide": {{
|
246
|
+
"index": 0,
|
247
|
+
"placeholders": {{
|
248
|
+
"0": "Title 1",
|
249
|
+
"1": "Subtitle 2"
|
250
|
+
}}
|
251
|
+
}}
|
252
|
+
}},
|
253
|
+
"aliases": {{
|
254
|
+
"title": "Title Slide",
|
255
|
+
"content": "Title and Content"
|
256
|
+
}}
|
257
|
+
}}
|
258
|
+
```
|
259
|
+
|
260
|
+
### Usage Examples
|
261
|
+
|
262
|
+
**JSON Format**:
|
263
|
+
```json
|
264
|
+
{{
|
265
|
+
"presentation": {{
|
266
|
+
"slides": [
|
267
|
+
{{
|
268
|
+
"type": "Title Slide",
|
269
|
+
"layout": "Title Slide",
|
270
|
+
"title": "My Presentation",
|
271
|
+
"subtitle": "Subtitle text"
|
272
|
+
}}
|
273
|
+
]
|
274
|
+
}}
|
275
|
+
}}
|
276
|
+
```
|
277
|
+
|
278
|
+
**Markdown with Frontmatter**:
|
279
|
+
```yaml
|
280
|
+
---
|
281
|
+
layout: Title Slide
|
282
|
+
---
|
283
|
+
# My Presentation
|
284
|
+
## Subtitle text
|
285
|
+
```
|
286
|
+
|
287
|
+
---
|
288
|
+
*Generated automatically by Deckbuilder Template Manager on {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}*
|
289
|
+
"""
|
290
|
+
|
291
|
+
return doc_content
|
292
|
+
|
293
|
+
def validate_template(self, template_name: str) -> dict:
|
294
|
+
"""
|
295
|
+
Validate template structure and mappings.
|
296
|
+
|
297
|
+
Args:
|
298
|
+
template_name: Name of template to validate
|
299
|
+
|
300
|
+
Returns:
|
301
|
+
Validation results
|
302
|
+
"""
|
303
|
+
print(f"🔍 Validating template: {template_name}")
|
304
|
+
|
305
|
+
validation_results = {
|
306
|
+
"template_file": self._validate_template_file(template_name),
|
307
|
+
"json_mapping": self._validate_json_mapping(template_name),
|
308
|
+
"placeholder_naming": self._validate_placeholder_naming(template_name),
|
309
|
+
}
|
310
|
+
|
311
|
+
# Print results
|
312
|
+
for category, results in validation_results.items():
|
313
|
+
status = results.get("status", "unknown")
|
314
|
+
if status == "valid":
|
315
|
+
print(f"✅ {category}: Valid")
|
316
|
+
elif status == "issues_found":
|
317
|
+
print(f"⚠️ {category}: Issues found")
|
318
|
+
for issue in results.get("issues", []):
|
319
|
+
print(f" - {issue}")
|
320
|
+
else:
|
321
|
+
print(f"❌ {category}: {results.get('error', 'Unknown error')}")
|
322
|
+
|
323
|
+
return validation_results
|
324
|
+
|
325
|
+
def _validate_template_file(self, template_name: str) -> dict:
|
326
|
+
"""Validate template file exists and is accessible"""
|
327
|
+
try:
|
328
|
+
if not template_name.endswith(".pptx"):
|
329
|
+
template_name += ".pptx"
|
330
|
+
|
331
|
+
template_path = os.path.join(self.template_folder, template_name)
|
332
|
+
|
333
|
+
if not os.path.exists(template_path):
|
334
|
+
return {"status": "error", "error": f"Template file not found: {template_path}"}
|
335
|
+
|
336
|
+
# Try to load with python-pptx
|
337
|
+
from pptx import Presentation
|
338
|
+
|
339
|
+
prs = Presentation(template_path)
|
340
|
+
|
341
|
+
return {
|
342
|
+
"status": "valid",
|
343
|
+
"layout_count": len(prs.slide_layouts),
|
344
|
+
"file_size": os.path.getsize(template_path),
|
345
|
+
}
|
346
|
+
|
347
|
+
except Exception as e:
|
348
|
+
return {"status": "error", "error": str(e)}
|
349
|
+
|
350
|
+
def _validate_json_mapping(self, template_name: str) -> dict:
|
351
|
+
"""Validate JSON mapping file"""
|
352
|
+
base_name = template_name.replace(".pptx", "")
|
353
|
+
mapping_file = os.path.join(self.template_folder, f"{base_name}.json")
|
354
|
+
|
355
|
+
if not os.path.exists(mapping_file):
|
356
|
+
return {"status": "missing", "message": "JSON mapping file not found"}
|
357
|
+
|
358
|
+
try:
|
359
|
+
with open(mapping_file, "r", encoding="utf-8") as f:
|
360
|
+
mapping = json.load(f)
|
361
|
+
|
362
|
+
# Basic structure validation
|
363
|
+
required_keys = ["template_info", "layouts"]
|
364
|
+
missing_keys = [key for key in required_keys if key not in mapping]
|
365
|
+
|
366
|
+
if missing_keys:
|
367
|
+
return {
|
368
|
+
"status": "issues_found",
|
369
|
+
"issues": [f"Missing required keys: {missing_keys}"],
|
370
|
+
}
|
371
|
+
|
372
|
+
return {"status": "valid", "layouts_count": len(mapping.get("layouts", {}))}
|
373
|
+
|
374
|
+
except json.JSONDecodeError as e:
|
375
|
+
return {"status": "error", "error": f"Invalid JSON: {str(e)}"}
|
376
|
+
except Exception as e:
|
377
|
+
return {"status": "error", "error": str(e)}
|
378
|
+
|
379
|
+
def _validate_placeholder_naming(self, template_name: str) -> dict:
|
380
|
+
"""Validate placeholder naming conventions"""
|
381
|
+
# Placeholder for naming convention validation
|
382
|
+
return {"status": "valid", "message": "Naming validation not yet implemented"}
|
383
|
+
|
384
|
+
def enhance_template(
|
385
|
+
self,
|
386
|
+
template_name: str,
|
387
|
+
mapping_file: str = None,
|
388
|
+
create_backup: bool = True,
|
389
|
+
use_conventions: bool = False,
|
390
|
+
) -> dict:
|
391
|
+
"""
|
392
|
+
Enhance template by updating master slide placeholder names using semantic mapping.
|
393
|
+
|
394
|
+
Args:
|
395
|
+
template_name: Name of template to enhance
|
396
|
+
mapping_file: Custom JSON mapping file (optional)
|
397
|
+
create_backup: Create backup before modification (default: True)
|
398
|
+
use_conventions: Use convention-based naming system (default: False)
|
399
|
+
|
400
|
+
Returns:
|
401
|
+
Enhancement results with success/failure information
|
402
|
+
"""
|
403
|
+
print(f"🔧 Enhancing template: {template_name}")
|
404
|
+
|
405
|
+
try:
|
406
|
+
# Ensure template name has .pptx extension
|
407
|
+
if not template_name.endswith(".pptx"):
|
408
|
+
template_name += ".pptx"
|
409
|
+
|
410
|
+
template_path = os.path.join(self.template_folder, template_name)
|
411
|
+
|
412
|
+
if not os.path.exists(template_path):
|
413
|
+
return {"status": "error", "error": f"Template file not found: {template_path}"}
|
414
|
+
|
415
|
+
# Determine mapping file path (only if not using conventions)
|
416
|
+
if not use_conventions:
|
417
|
+
base_name = template_name.replace(".pptx", "")
|
418
|
+
if not mapping_file:
|
419
|
+
mapping_file = os.path.join(self.template_folder, f"{base_name}.json")
|
420
|
+
|
421
|
+
if not os.path.exists(mapping_file):
|
422
|
+
return {
|
423
|
+
"status": "error",
|
424
|
+
"error": f"Mapping file not found: {mapping_file}. Run 'analyze' first to generate mapping.",
|
425
|
+
}
|
426
|
+
|
427
|
+
# Create backup if requested
|
428
|
+
if create_backup:
|
429
|
+
backup_path = self._create_template_backup(template_path)
|
430
|
+
print(f"📄 Backup created: {backup_path}")
|
431
|
+
|
432
|
+
# Load or generate mapping
|
433
|
+
if use_conventions:
|
434
|
+
print("🎯 Using convention-based naming system...")
|
435
|
+
# Generate convention-based mapping
|
436
|
+
mapping = self._generate_convention_mapping(template_path)
|
437
|
+
else:
|
438
|
+
# Load existing mapping
|
439
|
+
with open(mapping_file, "r", encoding="utf-8") as f:
|
440
|
+
mapping = json.load(f)
|
441
|
+
|
442
|
+
# Enhance template
|
443
|
+
modifications = self._modify_master_slide_placeholders(template_path, mapping)
|
444
|
+
|
445
|
+
if modifications["modified_count"] > 0:
|
446
|
+
print("✅ Enhancement complete!")
|
447
|
+
print(
|
448
|
+
f" 📊 Modified {modifications['modified_count']} placeholders across {modifications['layout_count']} layouts"
|
449
|
+
)
|
450
|
+
|
451
|
+
if "enhanced_template_path" in modifications:
|
452
|
+
print(
|
453
|
+
f" 📄 Enhanced template saved: {modifications['enhanced_template_path']}"
|
454
|
+
)
|
455
|
+
|
456
|
+
if modifications["issues"]:
|
457
|
+
print("⚠️ Issues encountered:")
|
458
|
+
for issue in modifications["issues"]:
|
459
|
+
print(f" - {issue}")
|
460
|
+
|
461
|
+
return {
|
462
|
+
"status": "success",
|
463
|
+
"modifications": modifications,
|
464
|
+
"backup_path": backup_path if create_backup else None,
|
465
|
+
}
|
466
|
+
else:
|
467
|
+
print("ℹ️ No modifications needed - all placeholders already have correct names")
|
468
|
+
return {
|
469
|
+
"status": "no_changes",
|
470
|
+
"message": "Template already has correct placeholder names",
|
471
|
+
}
|
472
|
+
|
473
|
+
except Exception as e:
|
474
|
+
return {"status": "error", "error": str(e)}
|
475
|
+
|
476
|
+
def _create_template_backup(self, template_path: str) -> str:
|
477
|
+
"""Create backup copy of template file in organized backups folder"""
|
478
|
+
import shutil
|
479
|
+
from datetime import datetime
|
480
|
+
|
481
|
+
# Generate backup filename with timestamp
|
482
|
+
path_obj = Path(template_path)
|
483
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
484
|
+
backup_name = f"{path_obj.stem}_backup_{timestamp}{path_obj.suffix}"
|
485
|
+
|
486
|
+
# Create backups folder within templates directory
|
487
|
+
backups_folder = path_obj.parent / "backups"
|
488
|
+
backups_folder.mkdir(exist_ok=True)
|
489
|
+
|
490
|
+
backup_path = backups_folder / backup_name
|
491
|
+
|
492
|
+
# Copy file
|
493
|
+
shutil.copy2(template_path, backup_path)
|
494
|
+
return str(backup_path)
|
495
|
+
|
496
|
+
def _modify_master_slide_placeholders(self, template_path: str, mapping: dict) -> dict:
|
497
|
+
"""
|
498
|
+
Modify master slide placeholder names using python-pptx.
|
499
|
+
|
500
|
+
Args:
|
501
|
+
template_path: Path to PowerPoint template
|
502
|
+
mapping: JSON mapping with placeholder names
|
503
|
+
|
504
|
+
Returns:
|
505
|
+
Dictionary with modification results
|
506
|
+
"""
|
507
|
+
from pptx import Presentation
|
508
|
+
|
509
|
+
modifications = {"modified_count": 0, "layout_count": 0, "issues": [], "changes": []}
|
510
|
+
|
511
|
+
# Load presentation
|
512
|
+
prs = Presentation(template_path)
|
513
|
+
layouts_mapping = mapping.get("layouts", {})
|
514
|
+
|
515
|
+
# Access the slide master (this is the key change!)
|
516
|
+
slide_master = prs.slide_master
|
517
|
+
|
518
|
+
# First, try to modify placeholders on the master slide itself
|
519
|
+
try:
|
520
|
+
for placeholder in slide_master.placeholders:
|
521
|
+
placeholder_idx = str(placeholder.placeholder_format.idx)
|
522
|
+
# Try to find this placeholder in any layout mapping
|
523
|
+
for layout_name, layout_info in layouts_mapping.items():
|
524
|
+
placeholder_mapping = layout_info.get("placeholders", {})
|
525
|
+
if placeholder_idx in placeholder_mapping:
|
526
|
+
new_name = placeholder_mapping[placeholder_idx]
|
527
|
+
old_name = placeholder.name
|
528
|
+
try:
|
529
|
+
placeholder.element.nvSpPr.cNvPr.name = new_name
|
530
|
+
modifications["modified_count"] += 1
|
531
|
+
modifications["changes"].append(
|
532
|
+
{
|
533
|
+
"location": "master_slide",
|
534
|
+
"placeholder_idx": placeholder_idx,
|
535
|
+
"old_name": old_name,
|
536
|
+
"new_name": new_name,
|
537
|
+
}
|
538
|
+
)
|
539
|
+
break
|
540
|
+
except Exception as e:
|
541
|
+
modifications["issues"].append(
|
542
|
+
f"Failed to modify master placeholder {placeholder_idx}: {str(e)}"
|
543
|
+
)
|
544
|
+
except Exception as e:
|
545
|
+
modifications["issues"].append(f"No direct master placeholders or error: {e}")
|
546
|
+
|
547
|
+
# Process each slide layout in the master
|
548
|
+
for layout in slide_master.slide_layouts:
|
549
|
+
layout_name = layout.name
|
550
|
+
modifications["layout_count"] += 1
|
551
|
+
|
552
|
+
if layout_name not in layouts_mapping:
|
553
|
+
modifications["issues"].append(f"Layout '{layout_name}' not found in mapping file")
|
554
|
+
continue
|
555
|
+
|
556
|
+
layout_mapping = layouts_mapping[layout_name]
|
557
|
+
placeholder_mapping = layout_mapping.get("placeholders", {})
|
558
|
+
|
559
|
+
# Modify placeholders in this master slide layout
|
560
|
+
for placeholder in layout.placeholders:
|
561
|
+
placeholder_idx = str(placeholder.placeholder_format.idx)
|
562
|
+
|
563
|
+
if placeholder_idx in placeholder_mapping:
|
564
|
+
new_name = placeholder_mapping[placeholder_idx]
|
565
|
+
old_name = placeholder.name
|
566
|
+
|
567
|
+
try:
|
568
|
+
# Update placeholder name on the master slide layout
|
569
|
+
if hasattr(placeholder, "element") and hasattr(
|
570
|
+
placeholder.element, "nvSpPr"
|
571
|
+
):
|
572
|
+
placeholder.element.nvSpPr.cNvPr.name = new_name
|
573
|
+
modifications["modified_count"] += 1
|
574
|
+
modifications["changes"].append(
|
575
|
+
{
|
576
|
+
"layout": layout_name,
|
577
|
+
"placeholder_idx": placeholder_idx,
|
578
|
+
"old_name": old_name,
|
579
|
+
"new_name": new_name,
|
580
|
+
}
|
581
|
+
)
|
582
|
+
else:
|
583
|
+
modifications["issues"].append(
|
584
|
+
f"Cannot modify placeholder {placeholder_idx} in {layout_name} - unsupported structure"
|
585
|
+
)
|
586
|
+
except Exception as e:
|
587
|
+
modifications["issues"].append(
|
588
|
+
f"Failed to modify placeholder {placeholder_idx} in {layout_name}: {str(e)}"
|
589
|
+
)
|
590
|
+
|
591
|
+
# Save modified presentation with .g.pptx extension
|
592
|
+
try:
|
593
|
+
# Generate enhanced template filename with .g.pptx convention
|
594
|
+
path_obj = Path(template_path)
|
595
|
+
enhanced_name = f"{path_obj.stem}.g{path_obj.suffix}"
|
596
|
+
enhanced_path = path_obj.parent / enhanced_name
|
597
|
+
|
598
|
+
prs.save(str(enhanced_path))
|
599
|
+
modifications["enhanced_template_path"] = str(enhanced_path)
|
600
|
+
except Exception as e:
|
601
|
+
modifications["issues"].append(f"Failed to save enhanced template: {str(e)}")
|
602
|
+
|
603
|
+
return modifications
|
604
|
+
|
605
|
+
def _generate_convention_mapping(self, template_path: str) -> dict:
|
606
|
+
"""
|
607
|
+
Generate convention-based mapping for template placeholders.
|
608
|
+
|
609
|
+
Args:
|
610
|
+
template_path: Path to PowerPoint template
|
611
|
+
|
612
|
+
Returns:
|
613
|
+
Convention-based mapping dictionary
|
614
|
+
"""
|
615
|
+
from pptx import Presentation
|
616
|
+
|
617
|
+
# Load presentation to analyze structure
|
618
|
+
prs = Presentation(template_path)
|
619
|
+
convention = NamingConvention()
|
620
|
+
|
621
|
+
# Build mapping using convention system
|
622
|
+
mapping = {"template_info": {"name": "Convention-Based", "version": "1.0"}, "layouts": {}}
|
623
|
+
|
624
|
+
# Process each slide layout
|
625
|
+
layout_index = 0
|
626
|
+
for layout in prs.slide_layouts:
|
627
|
+
layout_name = layout.name
|
628
|
+
layout_placeholders = {}
|
629
|
+
|
630
|
+
for placeholder in layout.placeholders:
|
631
|
+
placeholder_idx = str(placeholder.placeholder_format.idx)
|
632
|
+
|
633
|
+
# Create context for convention naming
|
634
|
+
context = PlaceholderContext(
|
635
|
+
layout_name=layout_name,
|
636
|
+
placeholder_idx=placeholder_idx,
|
637
|
+
total_placeholders=len(layout.placeholders),
|
638
|
+
)
|
639
|
+
|
640
|
+
# Generate convention-based name
|
641
|
+
convention_name = convention.generate_placeholder_name(context)
|
642
|
+
layout_placeholders[placeholder_idx] = convention_name
|
643
|
+
|
644
|
+
mapping["layouts"][layout_name] = {
|
645
|
+
"index": layout_index,
|
646
|
+
"placeholders": layout_placeholders,
|
647
|
+
}
|
648
|
+
|
649
|
+
layout_index += 1
|
650
|
+
|
651
|
+
return mapping
|
652
|
+
|
653
|
+
|
654
|
+
def main():
|
655
|
+
"""Command-line interface for template management tools"""
|
656
|
+
parser = argparse.ArgumentParser(
|
657
|
+
description="Deckbuilder Template Management Tools",
|
658
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
659
|
+
epilog="""
|
660
|
+
Examples:
|
661
|
+
python cli_tools.py analyze default
|
662
|
+
python cli_tools.py analyze default --verbose
|
663
|
+
python cli_tools.py document default
|
664
|
+
python cli_tools.py validate default
|
665
|
+
python cli_tools.py enhance default
|
666
|
+
python cli_tools.py enhance default --no-backup
|
667
|
+
python cli_tools.py enhance default --use-conventions
|
668
|
+
python cli_tools.py analyze default --template-folder ./templates --output-folder ./output
|
669
|
+
""",
|
670
|
+
)
|
671
|
+
|
672
|
+
# Global arguments
|
673
|
+
parser.add_argument(
|
674
|
+
"--template-folder", "-t", help="Path to templates folder (default: auto-detect)"
|
675
|
+
)
|
676
|
+
parser.add_argument(
|
677
|
+
"--output-folder", "-o", help="Path to output folder (default: ./template_output)"
|
678
|
+
)
|
679
|
+
|
680
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
681
|
+
|
682
|
+
# Analyze command
|
683
|
+
analyze_parser = subparsers.add_parser("analyze", help="Analyze PowerPoint template structure")
|
684
|
+
analyze_parser.add_argument("template", help="Template name (e.g., default)")
|
685
|
+
analyze_parser.add_argument(
|
686
|
+
"--verbose", "-v", action="store_true", help="Show detailed analysis"
|
687
|
+
)
|
688
|
+
|
689
|
+
# Document command
|
690
|
+
doc_parser = subparsers.add_parser("document", help="Generate template documentation")
|
691
|
+
doc_parser.add_argument("template", help="Template name to document")
|
692
|
+
doc_parser.add_argument("--doc-output", help="Documentation output file path")
|
693
|
+
|
694
|
+
# Validate command
|
695
|
+
validate_parser = subparsers.add_parser("validate", help="Validate template and mappings")
|
696
|
+
validate_parser.add_argument("template", help="Template name to validate")
|
697
|
+
|
698
|
+
# Enhance command
|
699
|
+
enhance_parser = subparsers.add_parser(
|
700
|
+
"enhance", help="Enhance template with corrected placeholder names (saves as .g.pptx)"
|
701
|
+
)
|
702
|
+
enhance_parser.add_argument("template", help="Template name to enhance")
|
703
|
+
enhance_parser.add_argument("--mapping-file", help="Custom JSON mapping file path")
|
704
|
+
enhance_parser.add_argument(
|
705
|
+
"--no-backup", action="store_true", help="Skip creating backup before modification"
|
706
|
+
)
|
707
|
+
enhance_parser.add_argument(
|
708
|
+
"--use-conventions",
|
709
|
+
action="store_true",
|
710
|
+
help="Use convention-based naming system instead of JSON mapping",
|
711
|
+
)
|
712
|
+
|
713
|
+
args = parser.parse_args()
|
714
|
+
|
715
|
+
if not args.command:
|
716
|
+
parser.print_help()
|
717
|
+
return
|
718
|
+
|
719
|
+
# Initialize template manager with command-line arguments
|
720
|
+
manager = TemplateManager(
|
721
|
+
template_folder=args.template_folder, output_folder=args.output_folder
|
722
|
+
)
|
723
|
+
|
724
|
+
# Execute command
|
725
|
+
if args.command == "analyze":
|
726
|
+
manager.analyze_template(args.template, verbose=args.verbose)
|
727
|
+
elif args.command == "document":
|
728
|
+
manager.document_template(args.template, getattr(args, "doc_output", None))
|
729
|
+
elif args.command == "validate":
|
730
|
+
manager.validate_template(args.template)
|
731
|
+
elif args.command == "enhance":
|
732
|
+
create_backup = not args.no_backup
|
733
|
+
manager.enhance_template(
|
734
|
+
args.template, args.mapping_file, create_backup, args.use_conventions
|
735
|
+
)
|
736
|
+
|
737
|
+
|
738
|
+
if __name__ == "__main__":
|
739
|
+
main()
|