--- name: powerpoint description: 'PowerPoint slide deck generation and management using python-pptx with YAML-driven content and styling' license: MIT compatibility: 'Requires uv, Python 3.11+, PowerShell 7+, and LibreOffice' metadata: authors: "microsoft/hve-core" spec_version: "1.0" last_updated: "2026-08-09" --- # PowerPoint Skill Generates, updates, and manages PowerPoint slide decks using `python-pptx` with YAML-driven content and styling definitions. ## Overview This skill provides Python scripts that consume YAML configuration files to produce PowerPoint slide decks. Each slide is defined by a `content.yaml` file describing its layout, text, and shapes. A `style.yaml` file defines dimensions, template configuration, layout mappings, metadata, and defaults. SKILL.md covers technical reference: prerequisites, commands, script architecture, API constraints, and troubleshooting. For conventions and design rules (element positioning, visual quality, color and contrast, contextual styling), follow `pptx.instructions.md`. ## Prerequisites ### PowerShell The `Invoke-PptxPipeline.ps1` script handles virtual environment creation and dependency installation automatically via `uv sync`. Requires `uv`, Python 3.11+, and PowerShell 7+. ### Installing uv If `uv` is not installed: ```bash # macOS / Linux curl -LsSf https://astral.sh/uv/install.sh | sh # Windows powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # Via pip (fallback) pip install uv ``` ### System Dependencies (Export and Validation) The Export and Validate actions require LibreOffice for PPTX-to-PDF conversion and optionally `pdftoppm` from poppler for PDF-to-JPG rendering. When `pdftoppm` is not available, PyMuPDF handles the image rendering. The Validate action's vision-based checks require the GitHub Copilot CLI for model access. ```bash # macOS brew install --cask libreoffice brew install poppler # optional, provides pdftoppm # Linux sudo apt-get install libreoffice poppler-utils # Windows (winget preferred, choco fallback) winget install TheDocumentFoundation.LibreOffice # choco install libreoffice-still # alternative # poppler: no winget package; use choco install poppler (optional, provides pdftoppm) ``` ### Copilot CLI (Vision Validation) The `validate_slides.py` script uses the GitHub Copilot SDK to send slide images to vision-capable models. The Copilot CLI must be installed and authenticated: ```bash # Install Copilot CLI npm install -g @github/copilot-cli # Authenticate (uses the same GitHub account as VS Code Copilot) copilot auth login # Verify copilot --version ``` ### Required Files * `style.yaml` — Dimensions, defaults, template configuration, and metadata * `content.yaml` — Per-slide content definition (text, shapes, images, layout) * (Optional) `content-extra.py` — Custom Python for complex slide drawings ## Content Directory Structure All slide content lives under the working directory's `content/` folder: ```text content/ ├── global/ │ ├── style.yaml # Dimensions, defaults, template config, and theme metadata │ └── voice-guide.md # Voice and tone guidelines ├── slide-001/ │ ├── content.yaml # Slide 1 content and layout │ └── images/ # Slide-specific images │ ├── background.png │ └── background.yaml # Image metadata sidecar ├── slide-002/ │ ├── content.yaml # Slide 2 content and layout │ ├── content-extra.py # Custom Python for complex drawings │ └── images/ │ └── screenshot.png ├── slide-003/ │ ├── content.yaml │ └── images/ │ ├── diagram.png │ └── diagram.yaml └── ... ``` ## Global Style Definition (`style.yaml`) The global `style.yaml` defines dimensions, template configuration, layout mappings, metadata, and defaults. Color and font choices are specified per-element in each slide's `content.yaml` rather than centralized in the style file. See the [style.yaml template](style-yaml-template.md) for the full template, field reference, and usage instructions. ## Per-Slide Content Definition (`content.yaml`) Each slide's `content.yaml` defines layout, text, shapes, and positioning. All position and size values are in inches. Color values use `#RRGGBB` hex format or `@theme_name` references. Text contract: markdown-like list lines in `textbox.text` and `shape.text` are interpreted as PowerPoint lists during rendering. Unordered markers (`-`, `+`, `*`) become bulleted paragraphs, ordered markers (`1.`, `1)`) become auto-numbered paragraphs, and leading indentation maps to paragraph level. See the [content.yaml template](content-yaml-template.md) for the full template, supported element types, supported shape types, and usage instructions. ## Complex Drawings (`content-extra.py`) When a slide requires complex drawings that cannot be expressed through `content.yaml` element definitions, create a `content-extra.py` file in the slide folder. The `render()` function signature is fixed. The build script calls it after placing standard `content.yaml` elements. See the [content-extra.py template](content-extra-py-template.md) for the full template, function parameters, and usage guidelines. ### Security Validation `content-extra.py` execution is disabled by default. When a slide folder contains one and `--allow-scripts` is not passed, the build fails with an error naming the file. Pass `--allow-scripts` to authorize execution after reviewing the script. When execution is authorized, the build script performs AST-based static analysis before running the file and rejects the patterns below. This analysis is a lint that catches obvious mistakes early. It is not a security boundary: a blocked module reached through an alias is not detected, and `pathlib` and `open` are permitted. Authorization is the control. **Allowed imports:** * `pptx` and all `pptx.*` submodules * Safe standard-library modules (e.g., `math`, `copy`, `json`, `re`, `pathlib`, `collections`, `itertools`, `functools`, `typing`, `enum`, `dataclasses`, `decimal`, `fractions`, `string`, `textwrap`) **Blocked imports:** * `subprocess`, `os`, `shutil`, `socket`, `ctypes`, `signal`, `multiprocessing`, `threading`, `http`, `urllib`, `ftplib`, `smtplib`, `imaplib`, `poplib`, `xmlrpc`, `webbrowser`, `code`, `codeop`, `compileall`, `py_compile`, `zipimport`, `pkgutil`, `runpy`, `ensurepip`, `venv`, `sqlite3`, `tempfile`, `shelve`, `dbm`, `pickle`, `marshal`, `importlib`, `sys`, `telnetlib` * Any third-party package not on the allowlist **Blocked builtins:** * Dangerous: `eval`, `exec`, `__import__`, `compile`, `breakpoint` * Indirect bypass: `getattr`, `setattr`, `delattr`, `globals`, `locals`, `vars` * Attribute-form calls onto `builtins`, `os`, `sys`, `subprocess`, and `importlib` (for example `builtins.eval(...)`) **`--allow-scripts` flag:** Pass `--allow-scripts` to authorize execution of `content-extra.py` files. Without it, a present script fails the build rather than being skipped, so a deck never silently loses custom drawings. The flag authorizes execution; it does not skip the lint. This is a behavior change. A deck that previously built with a `content-extra.py` present now requires `--allow-scripts`, and a script relying on a blocked import no longer has a bypass. ```bash python scripts/build_deck.py \ --content-dir content/ \ --style content/global/style.yaml \ --output slide-deck/presentation.pptx \ --allow-scripts ``` When validation fails, the build raises `ContentExtraError` with a message identifying the violation and file path. ## Script Reference The full command surface lives in [references/script-reference.md](references/script-reference.md): build a deck, build from a template, update specific slides, extract content from an existing PPTX, validate, export slides to images or SVG, dry-run validation, generate theme variants, and embed audio. ## Script Architecture The build and extraction scripts use shared modules in the `scripts/` directory: | Module | Purpose | |------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `pptx_utils.py` | Shared utilities: exit codes, logging configuration, slide filter parsing, unit conversion (`emu_to_inches()`), YAML loading | | `pptx_colors.py` | Color resolution (`#hex`, `@theme`, dict with brightness), theme color map (16 entries) | | `pptx_fonts.py` | Font resolution, family normalization, weight suffix handling, alignment mapping | | `pptx_shapes.py` | Shape constant map (29 entries + circle alias), auto-shape name mapping, rotation utilities | | `pptx_fills.py` | Solid, gradient, and pattern fill application/extraction; line/border styling with dash styles | | `pptx_text.py` | Text frame properties (margins, auto-size, vertical anchor), paragraph properties (spacing, level), run properties (underline, hyperlink), markdown-like list parsing to bullet/auto-number paragraphs | | `pptx_tables.py` | Table element creation and extraction with cell merging, banding, and per-cell styling | | `pptx_charts.py` | Chart element creation and extraction for 12 chart types (column, bar, line, pie, scatter, bubble, etc.) | | `validate_deck.py` | PPTX-only validation for speaker notes and slide count | | `validate_geometry.py` | Structural validation for element edge margins, adjacent gaps, boundary overflow, and title clearance | | `validate_slides.py` | Vision-based slide issue detection and quality validation via Copilot SDK with built-in checks and plain-text per-slide output | | `render_pdf_images.py` | PDF-to-JPG rendering via PyMuPDF with optional slide-number-based naming | | `generate_themes.py` | Theme variant generation from a base content directory using a color mapping YAML file | | `embed_audio.py` | WAV audio embedding into PPTX slides with per-slide file matching and off-screen audio icon placement | | `export_svg.py` | PPTX-to-SVG export via LibreOffice PDF conversion and PyMuPDF SVG rendering | ## python-pptx Constraints * python-pptx does NOT support SVG images. Always convert to PNG via `cairosvg` or `Pillow`. * python-pptx cannot create new slide masters or layouts programmatically. Use blank layouts or start from a template PPTX with the `--template` argument. * Transitions and animations are preserved when opening and saving existing files, but cannot be created or modified via the API. * When extracting content, slide master and layout inheritance means many text elements have no inline styling. Add explicit font properties in content YAML before rebuilding. * The Export and Validate actions require LibreOffice for PPTX-to-PDF conversion. The PowerShell orchestrator checks for LibreOffice availability before starting and provides platform-specific install instructions if missing. * Accessing `background.fill` on slides with inherited backgrounds replaces them with `NoFill`. Check `slide.follow_master_background` before accessing the fill property. * Gradient fills use the python-pptx `GradientFill` API with `GradientStop` objects. Each stop specifies a position (0–100) and a color. * Theme colors resolve via `MSO_THEME_COLOR` enum. Brightness adjustments apply through the color format's `brightness` property. * Template-based builds load layouts by name or index. Layout name resolution falls back to index 6 (blank) when no match is found. ## Security Considerations This skill processes PDF files via [PyMuPDF](https://pymupdf.readthedocs.io/), which wraps the MuPDF C library. MuPDF parses untrusted binary structures (cross-reference tables, stream objects, font definitions) and historical CVEs have shown that memory-safety bugs in C parsers can lead to crashes, memory disclosure, or in rare cases code execution. ### Mitigations in place * `scripts/pdf_safety.py` validates every PDF (existence, regular-file, size <= 100 MB, `%PDF-` magic bytes, page count <= 1000) before calling `fitz.open()`. * All `fitz` operations are wrapped in `safe_open_pdf()`, which converts MuPDF exceptions into typed `PdfSafetyError` subclasses (`PdfTooLargeError`, `PdfInvalidFormatError`, `PdfTooManyPagesError`, `PdfParseError`, `PdfRenderError`). * `pymupdf` is version-pinned in `pyproject.toml` (`>=1.27.1,<2.0`) to track security fixes without silently adopting a major-version API change. > **Defense-in-depth note**: the 5-byte `%PDF-` magic check is a necessary but not sufficient guarantee of structural validity. A crafted small file that begins with `%PDF-` still reaches the MuPDF parser, where memory-safety bugs may exist. Callers MUST keep PyMuPDF patched against the latest advisories (tracked via microsoft/hve-core#1020 / `pip-audit`) and continue to treat such inputs as untrusted. The `PdfSafetyError` hierarchy (`PdfTooLargeError`, `PdfInvalidFormatError`, `PdfTooManyPagesError`, `PdfParseError`, `PdfRenderError`) is one defense layer alongside the version pin and CVE monitoring; no single layer is sufficient on its own. ### Accepted risk These mitigations reduce but do not eliminate the C-extension attack surface. Consumers SHOULD treat PDF inputs from outside this skill's PPTX-to-PDF pipeline as untrusted and apply additional sandboxing (subprocess, container) before feeding adversarial input. ### Update policy Re-check [NVD](https://nvd.nist.gov) and [OSV](https://osv.dev) advisories for MuPDF and PyMuPDF quarterly and on every pip-audit alert. ### Cross-references * microsoft/hve-core#1018 — original hardening request * microsoft/hve-core#1020 — pip-audit CI for ongoing CVE monitoring (separate effort) ## Troubleshooting | Issue | Cause | Solution | |----------------------------------------|----------------------------------------------------|--------------------------------------------------------------------------------------------------| | SVG runtime error | python-pptx cannot embed SVG | Convert to PNG via `cairosvg` before adding | | Text overlay between elements | Insufficient vertical spacing | Follow element positioning conventions in `pptx.instructions.md` | | Width overflow off-slide | Element extends beyond slide boundary | Follow element positioning conventions in `pptx.instructions.md` | | Bright accent color unreadable as fill | White text on bright background | Darken accent to ~60% saturation for box fills | | Background fill replaced with NoFill | Accessed `background.fill` on inherited background | Check `slide.follow_master_background` before accessing | | Missing speaker notes | Notes not specified in `content.yaml` | Add `speaker_notes` field to every content slide | | LibreOffice not found during Validate | Validate exports slides to images first | Install LibreOffice: `brew install --cask libreoffice` (macOS) | | `uv` not found | uv package manager not installed | Install uv: `curl -LsSf https://astral.sh/uv/install.sh \| sh` (macOS/Linux) or `pip install uv` | | Python not found by uv | No Python 3.11+ on PATH | Install via `uv python install 3.11` or `pyenv install 3.11` | | `uv sync` fails | Missing or corrupt `.venv` | Delete `.venv/` at the skill root and re-run `uv sync` | | Import errors in scripts | Dependencies not installed or stale venv | Run `uv sync` from the skill root to recreate the environment | ## Environment Recovery When scripts fail due to missing modules, import errors, or a corrupt virtual environment, recover with: ```bash cd .github/skills/experimental/powerpoint rm -rf .venv uv sync ``` This recreates the virtual environment from scratch using `pyproject.toml` as the single source of truth. The `Invoke-PptxPipeline.ps1` orchestrator runs `uv sync` automatically on each invocation unless `-SkipVenvSetup` is passed. When `uv` itself is not available, install it first (see Installing uv above), then retry. When Python 3.11+ is not available, run `uv python install 3.11` to have uv fetch and manage the interpreter.