|
| 1 | +--- |
| 2 | +name: pptx-from-template |
| 3 | +description: "Creates a new PowerPoint (.pptx) presentation based on an existing template PPTX, reusing its slide layouts, styles, fonts, colors, and backgrounds. Use this skill when the user asks to generate slides, create a new presentation from an existing one, or produce a PPTX with consistent branding from a template." |
| 4 | +argument-hint: "[template.pptx] [content description or plan]" |
| 5 | +--- |
| 6 | + |
| 7 | +# Create PPTX from Template |
| 8 | + |
| 9 | +This skill creates a new PowerPoint presentation that inherits all styles, slide layouts, backgrounds, fonts, and color schemes from an existing PPTX template. It uses the `python-pptx` package. |
| 10 | + |
| 11 | +## When to use |
| 12 | + |
| 13 | +- User asks to create a new PPTX based on an existing presentation's style |
| 14 | +- User wants to generate slides with consistent branding from a template |
| 15 | +- User asks to produce a presentation using a previous session's look and feel |
| 16 | +- User has ASCII/markdown slide content and wants it turned into a styled PPTX |
| 17 | + |
| 18 | +## Prerequisites |
| 19 | + |
| 20 | +`python-pptx` is declared as an inline script dependency, so no manual install is needed when using `uv run`. |
| 21 | + |
| 22 | +## Helper module |
| 23 | + |
| 24 | +This skill includes a reusable Python module at `.github/skills/pptx-from-template/pptx_from_template.py`. |
| 25 | + |
| 26 | +To inspect a template directly: |
| 27 | +```bash |
| 28 | +uv run .github/skills/pptx-from-template/pptx_from_template.py template.pptx |
| 29 | +``` |
| 30 | + |
| 31 | +Available functions: |
| 32 | + |
| 33 | +| Function | Purpose | |
| 34 | +|---|---| |
| 35 | +| `inspect_template(path)` | Returns a dict with dimensions, layouts, placeholders, and existing slides | |
| 36 | +| `print_template_info(path)` | Prints a human-readable summary of the template | |
| 37 | +| `create_pptx_from_template(template, output, slides)` | Creates a new PPTX from a template with a list of slide content dicts | |
| 38 | +| `set_body_bullets(placeholder, lines)` | Sets placeholder text as bullet points | |
| 39 | +| `add_code_block(slide, code, ...)` | Adds a monospace code block with dark background | |
| 40 | +| `copy_slide_background(source, target)` | Copies background XML from one slide to another | |
| 41 | + |
| 42 | +## How it works |
| 43 | + |
| 44 | +The approach uses the template PPTX as the base file, which preserves: |
| 45 | +- **Slide master & layouts**: All layouts (title, content, section header, blank, etc.) with their formatting |
| 46 | +- **Theme**: Colors, fonts, effects |
| 47 | +- **Slide backgrounds**: Solid fills, gradient fills, and background images |
| 48 | +- **Placeholder styles**: Font sizes, positions, bullet styles |
| 49 | + |
| 50 | +### Step 1: Inspect the template |
| 51 | + |
| 52 | +Before generating slides, inspect the template to discover available slide layouts and their placeholders. Run this Python snippet to list them: |
| 53 | + |
| 54 | +```python |
| 55 | +from pptx import Presentation |
| 56 | + |
| 57 | +template = Presentation("template.pptx") |
| 58 | + |
| 59 | +print(f"Slide dimensions: {template.slide_width} x {template.slide_height} EMUs") |
| 60 | +print(f" ({template.slide_width / 914400:.1f}\" x {template.slide_height / 914400:.1f}\")\n") |
| 61 | + |
| 62 | +for i, layout in enumerate(template.slide_layouts): |
| 63 | + print(f"Layout [{i}]: '{layout.name}'") |
| 64 | + for ph in layout.placeholders: |
| 65 | + print(f" Placeholder idx={ph.placeholder_format.idx}, " |
| 66 | + f"name='{ph.name}', " |
| 67 | + f"type={ph.placeholder_format.type}, " |
| 68 | + f"size=({ph.left}, {ph.top}, {ph.width}, {ph.height})") |
| 69 | + print() |
| 70 | +``` |
| 71 | + |
| 72 | +Also inspect existing slides to see which layouts are used and how content is structured: |
| 73 | + |
| 74 | +```python |
| 75 | +for i, slide in enumerate(template.slides): |
| 76 | + layout_name = slide.slide_layout.name |
| 77 | + print(f"Slide {i+1}: layout='{layout_name}'") |
| 78 | + for shape in slide.shapes: |
| 79 | + if shape.has_text_frame: |
| 80 | + text = shape.text_frame.text[:80] |
| 81 | + print(f" Shape '{shape.name}': \"{text}\"") |
| 82 | + elif shape.shape_type == 13: # Picture |
| 83 | + print(f" Picture '{shape.name}': {shape.width}x{shape.height}") |
| 84 | + print() |
| 85 | +``` |
| 86 | + |
| 87 | +### Step 2: Generate the new presentation |
| 88 | + |
| 89 | +Use the template as the starting point. Delete existing slides (keeping the slide master/layouts), then add new slides using the discovered layouts. |
| 90 | + |
| 91 | +```python |
| 92 | +from pptx import Presentation |
| 93 | +from pptx.util import Inches, Pt, Emu |
| 94 | +from pptx.enum.text import PP_ALIGN |
| 95 | +from lxml import etree |
| 96 | +import copy |
| 97 | + |
| 98 | +def create_pptx_from_template(template_path, output_path, slides_content): |
| 99 | + """Create a new PPTX from a template, preserving all styles. |
| 100 | +
|
| 101 | + Args: |
| 102 | + template_path: Path to the template .pptx file |
| 103 | + output_path: Path for the output .pptx file |
| 104 | + slides_content: List of dicts, each with: |
| 105 | + - layout_index (int): Index of the slide layout to use |
| 106 | + - title (str, optional): Title text |
| 107 | + - body (str, optional): Body/content text |
| 108 | + - notes (str, optional): Speaker notes |
| 109 | + """ |
| 110 | + prs = Presentation(template_path) |
| 111 | + |
| 112 | + # Delete all existing slides (keep layouts/masters) |
| 113 | + while len(prs.slides) > 0: |
| 114 | + rId = prs.slides._sldIdLst[0].get('r:id') |
| 115 | + prs.part.drop_rel(rId) |
| 116 | + prs.slides._sldIdLst.remove(prs.slides._sldIdLst[0]) |
| 117 | + |
| 118 | + # Add new slides from content |
| 119 | + for slide_data in slides_content: |
| 120 | + layout_idx = slide_data.get("layout_index", 1) |
| 121 | + layout = prs.slide_layouts[layout_idx] |
| 122 | + slide = prs.slides.add_slide(layout) |
| 123 | + |
| 124 | + # Set title if present and placeholder exists |
| 125 | + title_text = slide_data.get("title") |
| 126 | + if title_text and slide.shapes.title: |
| 127 | + slide.shapes.title.text = title_text |
| 128 | + |
| 129 | + # Set body content if present |
| 130 | + body_text = slide_data.get("body") |
| 131 | + if body_text: |
| 132 | + for ph in slide.placeholders: |
| 133 | + if ph.placeholder_format.idx == 1: # Content placeholder |
| 134 | + ph.text = body_text |
| 135 | + break |
| 136 | + |
| 137 | + # Add speaker notes if present |
| 138 | + notes_text = slide_data.get("notes") |
| 139 | + if notes_text: |
| 140 | + notes_slide = slide.notes_slide |
| 141 | + notes_slide.notes_text_frame.text = notes_text |
| 142 | + |
| 143 | + prs.save(output_path) |
| 144 | + print(f"Created {output_path} with {len(slides_content)} slides") |
| 145 | +``` |
| 146 | + |
| 147 | +### Step 3: Advanced — Preserve a background from a template slide |
| 148 | + |
| 149 | +If the template has slides with custom backgrounds (images, gradients) that you want to reuse on new slides, copy the background XML from the template slide: |
| 150 | + |
| 151 | +```python |
| 152 | +from lxml import etree |
| 153 | +import copy |
| 154 | + |
| 155 | +def copy_slide_background(source_slide, target_slide): |
| 156 | + """Copy background properties from a source slide to a target slide.""" |
| 157 | + source_bg = source_slide._element.find( |
| 158 | + '{http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing}bg', |
| 159 | + ) |
| 160 | + if source_bg is None: |
| 161 | + # Try the presentation ML namespace |
| 162 | + source_bg = source_slide._element.find( |
| 163 | + '{http://schemas.openxmlformats.org/presentationml/2006/main}bg', |
| 164 | + ) |
| 165 | + if source_bg is not None: |
| 166 | + # Remove existing background on target |
| 167 | + nsmap = {'p': 'http://schemas.openxmlformats.org/presentationml/2006/main'} |
| 168 | + existing_bg = target_slide._element.find('p:bg', nsmap) |
| 169 | + if existing_bg is not None: |
| 170 | + target_slide._element.remove(existing_bg) |
| 171 | + # Insert copy of source background |
| 172 | + new_bg = copy.deepcopy(source_bg) |
| 173 | + # Background should be the first child after cSld opening |
| 174 | + target_slide._element.insert(0, new_bg) |
| 175 | +``` |
| 176 | + |
| 177 | +### Step 4: Advanced — Multi-line body with bullet formatting |
| 178 | + |
| 179 | +To add bullet points that inherit the template's bullet styling: |
| 180 | + |
| 181 | +```python |
| 182 | +from pptx.util import Pt |
| 183 | + |
| 184 | +def set_body_bullets(placeholder, lines): |
| 185 | + """Set body text as bullet points, one per line. |
| 186 | +
|
| 187 | + Clears existing text and adds each line as a separate paragraph, |
| 188 | + inheriting the placeholder's default paragraph formatting. |
| 189 | + """ |
| 190 | + tf = placeholder.text_frame |
| 191 | + tf.clear() |
| 192 | + |
| 193 | + for i, line in enumerate(lines): |
| 194 | + if i == 0: |
| 195 | + para = tf.paragraphs[0] |
| 196 | + else: |
| 197 | + para = tf.add_paragraph() |
| 198 | + para.text = line |
| 199 | + # Inherit level from template; set level 0 for top-level bullets |
| 200 | + para.level = 0 |
| 201 | +``` |
| 202 | + |
| 203 | +### Step 5: Advanced — Add code blocks |
| 204 | + |
| 205 | +For technical presentations, add code blocks as a text box with monospace font: |
| 206 | + |
| 207 | +```python |
| 208 | +from pptx.util import Inches, Pt, Emu |
| 209 | +from pptx.dml.color import RGBColor |
| 210 | + |
| 211 | +def add_code_block(slide, code_text, left=Inches(0.5), top=Inches(2), |
| 212 | + width=Inches(9), height=Inches(4.5), |
| 213 | + font_size=Pt(11), font_name="Consolas"): |
| 214 | + """Add a code block as a text box with monospace font and dark background.""" |
| 215 | + txBox = slide.shapes.add_textbox(left, top, width, height) |
| 216 | + tf = txBox.text_frame |
| 217 | + tf.word_wrap = True |
| 218 | + |
| 219 | + # Set background fill on the shape |
| 220 | + fill = txBox.fill |
| 221 | + fill.solid() |
| 222 | + fill.fore_color.rgb = RGBColor(0x1E, 0x1E, 0x1E) # Dark background |
| 223 | + |
| 224 | + para = tf.paragraphs[0] |
| 225 | + para.text = code_text |
| 226 | + run = para.runs[0] |
| 227 | + run.font.name = font_name |
| 228 | + run.font.size = font_size |
| 229 | + run.font.color.rgb = RGBColor(0xD4, 0xD4, 0xD4) # Light text |
| 230 | +``` |
| 231 | + |
| 232 | +## Typical workflow |
| 233 | + |
| 234 | +1. User provides a template PPTX and slide content (ASCII plan, markdown, or structured data) |
| 235 | +2. **Inspect** the template to discover layouts and placeholders |
| 236 | +3. **Map** each piece of content to the appropriate layout |
| 237 | +4. **Generate** the PPTX using the template as the base |
| 238 | +5. **Review** the output and adjust as needed |
| 239 | + |
| 240 | +## Layout mapping guide |
| 241 | + |
| 242 | +Common layout conventions (indices vary per template — always inspect first): |
| 243 | + |
| 244 | +| Layout name | Typical use | Key placeholders | |
| 245 | +|---|---|---| |
| 246 | +| Title Slide | First/last slide, section dividers | title (0), subtitle (1) | |
| 247 | +| Title and Content | Most content slides | title (0), body (1) | |
| 248 | +| Section Header | Topic transitions | title (0), subtitle (1) | |
| 249 | +| Two Content | Side-by-side comparisons | title (0), left (1), right (2) | |
| 250 | +| Blank | Diagrams, full-bleed images | none | |
| 251 | +| Title Only | Slides with custom content below title | title (0) | |
| 252 | + |
| 253 | +## Important notes |
| 254 | + |
| 255 | +- Always inspect the template first — layout indices and placeholder indices vary between templates |
| 256 | +- The template's slide master determines all default formatting; you rarely need to set fonts/colors explicitly |
| 257 | +- If a template slide has images or shapes you want to preserve, keep that slide and modify its text rather than deleting and recreating |
| 258 | +- Background images tied to the slide master (not individual slides) are automatically inherited by all new slides |
| 259 | +- For complex slides (diagrams, tables, charts), consider keeping the template slide and modifying specific shapes by name |
0 commit comments