
Pptx
- 413 installs
- 70 repo stars
- Updated July 26, 2026
- rysweet/amplihack
pptx is an amplihack agent skill that builds or updates PowerPoint decks with slides, layouts, and speaker notes for developers who need pitch, status, or stakeholder presentation artifacts.
About
pptx is an amplihack skill for creating and revising PowerPoint presentation files programmatically through an agent. It structures slides, applies layouts, and adds speaker notes suited to pitches, status reports, and stakeholder documentation. Developers reach for pptx when narrative content exists but the deliverable must be a .pptx deck instead of markdown or a web page, and when slide order, layout consistency, and presenter notes need agent-assisted drafting or batch updates across recurring reports.
- Creates and edits .pptx slide decks programmatically
- Applies layouts, speaker notes, and branding
- Updates existing presentations from new content
- Supports pitch, roadmap, and status templates
- Fits agent-driven document generation pipelines
Pptx by the numbers
- 413 all-time installs (skills.sh)
- +1 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #164 of 687 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rysweet/amplihack --skill pptxAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 413 |
|---|---|
| repo stars | ★ 70 |
| Last updated | July 26, 2026 |
| Repository | rysweet/amplihack ↗ |
How do you generate PowerPoint slides from project content?
Build or update PowerPoint decks—slides, layouts, speaker notes—for pitches, status reports, and stakeholder documentation.
Who is it for?
Developers or technical leads who must ship recurring pitch decks, sprint reviews, or executive status slides as PowerPoint files.
Skip if: Developers who only need markdown READMEs or web-based slide tools, because pptx targets native PowerPoint .pptx deliverables.
When should I use this skill?
The user asks to create, update, or restructure a PowerPoint deck with slides, layouts, or speaker notes.
What you get
A .pptx deck with structured slides, applied layouts, and speaker notes ready for stakeholder presentation.
- .pptx presentation file
- Speaker notes
- Structured slide layouts
Files
PPTX creation, editing, and analysis
Overview
A user may ask you to create, edit, or analyze the contents of a .pptx file. A .pptx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks.
Reading and analyzing content
Text extraction
If you just need to read the text contents of a presentation, you should convert the document to markdown:
# Convert document to markdown
python -m markitdown path-to-file.pptxRaw XML access
You need raw XML access for: comments, speaker notes, slide layouts, animations, design elements, and complex formatting. For any of these features, you'll need to unpack a presentation and read its raw XML contents.
Unpacking a file
python ooxml/scripts/unpack.py <office_file> <output_dir>
Note: The unpack.py script is located at skills/pptx/ooxml/scripts/unpack.py relative to the project root. If the script doesn't exist at this path, use find . -name "unpack.py" to locate it.
Key file structures
ppt/presentation.xml- Main presentation metadata and slide referencesppt/slides/slide{N}.xml- Individual slide contents (slide1.xml, slide2.xml, etc.)ppt/notesSlides/notesSlide{N}.xml- Speaker notes for each slideppt/comments/modernComment_*.xml- Comments for specific slidesppt/slideLayouts/- Layout templates for slidesppt/slideMasters/- Master slide templatesppt/theme/- Theme and styling informationppt/media/- Images and other media files
Typography and color extraction
When given an example design to emulate: Always analyze the presentation's typography and colors first using the methods below:
1. Read theme file: Check ppt/theme/theme1.xml for colors (<a:clrScheme>) and fonts (<a:fontScheme>) 2. Sample slide content: Examine ppt/slides/slide1.xml for actual font usage (<a:rPr>) and colors 3. Search for patterns: Use grep to find color (<a:solidFill>, <a:srgbClr>) and font references across all XML files
Creating a new PowerPoint presentation without a template
When creating a new PowerPoint presentation from scratch, use the html2pptx workflow to convert HTML slides to PowerPoint with accurate positioning.
Design Principles
CRITICAL: Before creating any presentation, analyze the content and choose appropriate design elements:
1. Consider the subject matter: What is this presentation about? What tone, industry, or mood does it suggest? 2. Check for branding: If the user mentions a company/organization, consider their brand colors and identity 3. Match palette to content: Select colors that reflect the subject 4. State your approach: Explain your design choices before writing code
Requirements:
- ✅ State your content-informed design approach BEFORE writing code
- ✅ Use web-safe fonts only: Arial, Helvetica, Times New Roman, Georgia, Courier New, Verdana, Tahoma, Trebuchet MS, Impact
- ✅ Create clear visual hierarchy through size, weight, and color
- ✅ Ensure readability: strong contrast, appropriately sized text, clean alignment
- ✅ Be consistent: repeat patterns, spacing, and visual language across slides
Color Palette Selection
Choosing colors creatively:
- Think beyond defaults: What colors genuinely match this specific topic? Avoid autopilot choices.
- Consider multiple angles: Topic, industry, mood, energy level, target audience, brand identity (if mentioned)
- Be adventurous: Try unexpected combinations - a healthcare presentation doesn't have to be green, finance doesn't have to be navy
- Build your palette: Pick 3-5 colors that work together (dominant colors + supporting tones + accent)
- Ensure contrast: Text must be clearly readable on backgrounds
Example color palettes (use these to spark creativity - choose one, adapt it, or create your own):
1. Classic Blue: Deep navy (#1C2833), slate gray (#2E4053), silver (#AAB7B8), off-white (#F4F6F6) 2. Teal & Coral: Teal (#5EA8A7), deep teal (#277884), coral (#FE4447), white (#FFFFFF) 3. Bold Red: Red (#C0392B), bright red (#E74C3C), orange (#F39C12), yellow (#F1C40F), green (#2ECC71) 4. Warm Blush: Mauve (#A49393), blush (#EED6D3), rose (#E8B4B8), cream (#FAF7F2) 5. Burgundy Luxury: Burgundy (#5D1D2E), crimson (#951233), rust (#C15937), gold (#997929) 6. Deep Purple & Emerald: Purple (#B165FB), dark blue (#181B24), emerald (#40695B), white (#FFFFFF) 7. Cream & Forest Green: Cream (#FFE1C7), forest green (#40695B), white (#FCFCFC) 8. Pink & Purple: Pink (#F8275B), coral (#FF574A), rose (#FF737D), purple (#3D2F68) 9. Lime & Plum: Lime (#C5DE82), plum (#7C3A5F), coral (#FD8C6E), blue-gray (#98ACB5) 10. Black & Gold: Gold (#BF9A4A), black (#000000), cream (#F4F6F6) 11. Sage & Terracotta: Sage (#87A96B), terracotta (#E07A5F), cream (#F4F1DE), charcoal (#2C2C2C) 12. Charcoal & Red: Charcoal (#292929), red (#E33737), light gray (#CCCBCB) 13. Vibrant Orange: Orange (#F96D00), light gray (#F2F2F2), charcoal (#222831) 14. Forest Green: Black (#191A19), green (#4E9F3D), dark green (#1E5128), white (#FFFFFF) 15. Retro Rainbow: Purple (#722880), pink (#D72D51), orange (#EB5C18), amber (#F08800), gold (#DEB600) 16. Vintage Earthy: Mustard (#E3B448), sage (#CBD18F), forest green (#3A6B35), cream (#F4F1DE) 17. Coastal Rose: Old rose (#AD7670), beaver (#B49886), eggshell (#F3ECDC), ash gray (#BFD5BE) 18. Orange & Turquoise: Light orange (#FC993E), grayish turquoise (#667C6F), white (#FCFCFC)
Visual Details Options
Geometric Patterns:
- Diagonal section dividers instead of horizontal
- Asymmetric column widths (30/70, 40/60, 25/75)
- Rotated text headers at 90° or 270°
- Circular/hexagonal frames for images
- Triangular accent shapes in corners
- Overlapping shapes for depth
Border & Frame Treatments:
- Thick single-color borders (10-20pt) on one side only
- Double-line borders with contrasting colors
- Corner brackets instead of full frames
- L-shaped borders (top+left or bottom+right)
- Underline accents beneath headers (3-5pt thick)
Typography Treatments:
- Extreme size contrast (72pt headlines vs 11pt body)
- All-caps headers with wide letter spacing
- Numbered sections in oversized display type
- Monospace (Courier New) for data/stats/technical content
- Condensed fonts (Arial Narrow) for dense information
- Outlined text for emphasis
Chart & Data Styling:
- Monochrome charts with single accent color for key data
- Horizontal bar charts instead of vertical
- Dot plots instead of bar charts
- Minimal gridlines or none at all
- Data labels directly on elements (no legends)
- Oversized numbers for key metrics
Layout Innovations:
- Full-bleed images with text overlays
- Sidebar column (20-30% width) for navigation/context
- Modular grid systems (3×3, 4×4 blocks)
- Z-pattern or F-pattern content flow
- Floating text boxes over colored shapes
- Magazine-style multi-column layouts
Background Treatments:
- Solid color blocks occupying 40-60% of slide
- Gradient fills (vertical or diagonal only)
- Split backgrounds (two colors, diagonal or vertical)
- Edge-to-edge color bands
- Negative space as a design element
Layout Tips
When creating slides with charts or tables:
- Two-column layout (PREFERRED): Use a header spanning the full width, then two columns below - text/bullets in one column and the featured content in the other. This provides better balance and makes charts/tables more readable. Use flexbox with unequal column widths (e.g., 40%/60% split) to optimize space for each content type.
- Full-slide layout: Let the featured content (chart/table) take up the entire slide for maximum impact and readability
- NEVER vertically stack: Do not place charts/tables below text in a single column - this causes poor readability and layout issues
Workflow
1. MANDATORY - READ ENTIRE FILE: Read `html2pptx.md` completely from start to finish. NEVER set any range limits when reading this file. Read the full file content for detailed syntax, critical formatting rules, and best practices before proceeding with presentation creation. 2. Create an HTML file for each slide with proper dimensions (e.g., 720pt × 405pt for 16:9)
- Use
<p>,<h1>-<h6>,<ul>,<ol>for all text content - Use
class="placeholder"for areas where charts/tables will be added (render with gray background for visibility) - CRITICAL: Rasterize gradients and icons as PNG images FIRST using Sharp, then reference in HTML
- LAYOUT: For slides with charts/tables/images, use either full-slide layout or two-column layout for better readability
3. Create and run a JavaScript file using the `html2pptx.js` library to convert HTML slides to PowerPoint and save the presentation
- Use the
html2pptx()function to process each HTML file - Add charts and tables to placeholder areas using PptxGenJS API
- Save the presentation using
pptx.writeFile()
4. Visual validation: Generate thumbnails and inspect for layout issues
- Create thumbnail grid:
python scripts/thumbnail.py output.pptx workspace/thumbnails --cols 4 - Read and carefully examine the thumbnail image for:
- Text cutoff: Text being cut off by header bars, shapes, or slide edges
- Text overlap: Text overlapping with other text or shapes
- Positioning issues: Content too close to slide boundaries or other elements
- Contrast issues: Insufficient contrast between text and backgrounds
- If issues found, adjust HTML margins/spacing/colors and regenerate the presentation
- Repeat until all slides are visually correct
Editing an existing PowerPoint presentation
When edit slides in an existing PowerPoint presentation, you need to work with the raw Office Open XML (OOXML) format. This involves unpacking the .pptx file, editing the XML content, and repacking it.
Workflow
1. MANDATORY - READ ENTIRE FILE: Read `ooxml.md` (~500 lines) completely from start to finish. NEVER set any range limits when reading this file. Read the full file content for detailed guidance on OOXML structure and editing workflows before any presentation editing. 2. Unpack the presentation: python ooxml/scripts/unpack.py <office_file> <output_dir> 3. Edit the XML files (primarily ppt/slides/slide{N}.xml and related files) 4. CRITICAL: Validate immediately after each edit and fix any validation errors before proceeding: python ooxml/scripts/validate.py <dir> --original <file> 5. Pack the final presentation: python ooxml/scripts/pack.py <input_directory> <office_file>
Creating a new PowerPoint presentation using a template
When you need to create a presentation that follows an existing template's design, you'll need to duplicate and re-arrange template slides before then replacing placeholder context.
Workflow
1. Extract template text AND create visual thumbnail grid:
- Extract text:
python -m markitdown template.pptx > template-content.md - Read
template-content.md: Read the entire file to understand the contents of the template presentation. NEVER set any range limits when reading this file. - Create thumbnail grids:
python scripts/thumbnail.py template.pptx - See Creating Thumbnail Grids section for more details
2. Analyze template and save inventory to a file:
- Visual Analysis: Review thumbnail grid(s) to understand slide layouts, design patterns, and visual structure
- Create and save a template inventory file at
template-inventory.mdcontaining:
# Template Inventory Analysis
**Total Slides: [count]**
**IMPORTANT: Slides are 0-indexed (first slide = 0, last slide = count-1)**
## [Category Name]
- Slide 0: [Layout code if available] - Description/purpose
- Slide 1: [Layout code] - Description/purpose
- Slide 2: [Layout code] - Description/purpose
[... EVERY slide must be listed individually with its index ...]- Using the thumbnail grid: Reference the visual thumbnails to identify:
- Layout patterns (title slides, content layouts, section dividers)
- Image placeholder locations and counts
- Design consistency across slide groups
- Visual hierarchy and structure
- This inventory file is REQUIRED for selecting appropriate templates in the next step
3. Create presentation outline based on template inventory:
- Review available templates from step 2.
- Choose an intro or title template for the first slide. This should be one of the first templates.
- Choose safe, text-based layouts for the other slides.
- CRITICAL: Match layout structure to actual content:
- Single-column layouts: Use for unified narrative or single topic
- Two-column layouts: Use ONLY when you have exactly 2 distinct items/concepts
- Three-column layouts: Use ONLY when you have exactly 3 distinct items/concepts
- Image + text layouts: Use ONLY when you have actual images to insert
- Quote layouts: Use ONLY for actual quotes from people (with attribution), never for emphasis
- Never use layouts with more placeholders than you have content
- If you have 2 items, don't force them into a 3-column layout
- If you have 4+ items, consider breaking into multiple slides or using a list format
- Count your actual content pieces BEFORE selecting the layout
- Verify each placeholder in the chosen layout will be filled with meaningful content
- Select one option representing the best layout for each content section.
- Save
outline.mdwith content AND template mapping that leverages available designs - Example template mapping:
# Template slides to use (0-based indexing)
# WARNING: Verify indices are within range! Template with 73 slides has indices 0-72
# Mapping: slide numbers from outline -> template slide indices
template_mapping = [
0, # Use slide 0 (Title/Cover)
34, # Use slide 34 (B1: Title and body)
34, # Use slide 34 again (duplicate for second B1)
50, # Use slide 50 (E1: Quote)
54, # Use slide 54 (F2: Closing + Text)
]4. Duplicate, reorder, and delete slides using `rearrange.py`:
- Use the
scripts/rearrange.pyscript to create a new presentation with slides in the desired order:
python scripts/rearrange.py template.pptx working.pptx 0,34,34,50,52- The script handles duplicating repeated slides, deleting unused slides, and reordering automatically
- Slide indices are 0-based (first slide is 0, second is 1, etc.)
- The same slide index can appear multiple times to duplicate that slide
5. Extract ALL text using the `inventory.py` script:
- Run inventory extraction:
python scripts/inventory.py working.pptx text-inventory.json- Read text-inventory.json: Read the entire text-inventory.json file to understand all shapes and their properties. NEVER set any range limits when reading this file.
- The inventory JSON structure:
{
"slide-0": {
"shape-0": {
"placeholder_type": "TITLE", // or null for non-placeholders
"left": 1.5, // position in inches
"top": 2.0,
"width": 7.5,
"height": 1.2,
"paragraphs": [
{
"text": "Paragraph text",
// Optional properties (only included when non-default):
"bullet": true, // explicit bullet detected
"level": 0, // only included when bullet is true
"alignment": "CENTER", // CENTER, RIGHT (not LEFT)
"space_before": 10.0, // space before paragraph in points
"space_after": 6.0, // space after paragraph in points
"line_spacing": 22.4, // line spacing in points
"font_name": "Arial", // from first run
"font_size": 14.0, // in points
"bold": true,
"italic": false,
"underline": false,
"color": "FF0000" // RGB color
}
]
}
}
}- Key features:
- Slides: Named as "slide-0", "slide-1", etc.
- Shapes: Ordered by visual position (top-to-bottom, left-to-right) as "shape-0", "shape-1", etc.
- Placeholder types: TITLE, CENTER_TITLE, SUBTITLE, BODY, OBJECT, or null
- Default font size:
default_font_sizein points extracted from layout placeholders (when available) - Slide numbers are filtered: Shapes with SLIDE_NUMBER placeholder type are automatically excluded from inventory
- Bullets: When
bullet: true,levelis always included (even if 0) - Spacing:
space_before,space_after, andline_spacingin points (only included when set) - Colors:
colorfor RGB (e.g., "FF0000"),theme_colorfor theme colors (e.g., "DARK_1") - Properties: Only non-default values are included in the output
6. Generate replacement text and save the data to a JSON file Based on the text inventory from the previous step:
- CRITICAL: First verify which shapes exist in the inventory - only reference shapes that are actually present
- VALIDATION: The replace.py script will validate that all shapes in your replacement JSON exist in the inventory
- If you reference a non-existent shape, you'll get an error showing available shapes
- If you reference a non-existent slide, you'll get an error indicating the slide doesn't exist
- All validation errors are shown at once before the script exits
- IMPORTANT: The replace.py script uses inventory.py internally to identify ALL text shapes
- AUTOMATIC CLEARING: ALL text shapes from the inventory will be cleared unless you provide "paragraphs" for them
- Add a "paragraphs" field to shapes that need content (not "replacement_paragraphs")
- Shapes without "paragraphs" in the replacement JSON will have their text cleared automatically
- Paragraphs with bullets will be automatically left aligned. Don't set the
alignmentproperty on when"bullet": true - Generate appropriate replacement content for placeholder text
- Use shape size to determine appropriate content length
- CRITICAL: Include paragraph properties from the original inventory - don't just provide text
- IMPORTANT: When bullet: true, do NOT include bullet symbols (•, -, \*) in text - they're added automatically
- ESSENTIAL FORMATTING RULES:
- Headers/titles should typically have
"bold": true - List items should have
"bullet": true, "level": 0(level is required when bullet is true) - Preserve any alignment properties (e.g.,
"alignment": "CENTER"for centered text) - Include font properties when different from default (e.g.,
"font_size": 14.0,"font_name": "Lora") - Colors: Use
"color": "FF0000"for RGB or"theme_color": "DARK_1"for theme colors - The replacement script expects properly formatted paragraphs, not just text strings
- Overlapping shapes: Prefer shapes with larger default_font_size or more appropriate placeholder_type
- Save the updated inventory with replacements to
replacement-text.json - WARNING: Different template layouts have different shape counts - always check the actual inventory before creating replacements
Example paragraphs field showing proper formatting:
"paragraphs": [
{
"text": "New presentation title text",
"alignment": "CENTER",
"bold": true
},
{
"text": "Section Header",
"bold": true
},
{
"text": "First bullet point without bullet symbol",
"bullet": true,
"level": 0
},
{
"text": "Red colored text",
"color": "FF0000"
},
{
"text": "Theme colored text",
"theme_color": "DARK_1"
},
{
"text": "Regular paragraph text without special formatting"
}
]Shapes not listed in the replacement JSON are automatically cleared:
{
"slide-0": {
"shape-0": {
"paragraphs": [...] // This shape gets new text
}
// shape-1 and shape-2 from inventory will be cleared automatically
}
}Common formatting patterns for presentations:
- Title slides: Bold text, sometimes centered
- Section headers within slides: Bold text
- Bullet lists: Each item needs
"bullet": true, "level": 0 - Body text: Usually no special properties needed
- Quotes: May have special alignment or font properties
7. Apply replacements using the `replace.py` script
python scripts/replace.py working.pptx replacement-text.json output.pptxThe script will:
- First extract the inventory of ALL text shapes using functions from inventory.py
- Validate that all shapes in the replacement JSON exist in the inventory
- Clear text from ALL shapes identified in the inventory
- Apply new text only to shapes with "paragraphs" defined in the replacement JSON
- Preserve formatting by applying paragraph properties from the JSON
- Handle bullets, alignment, font properties, and colors automatically
- Save the updated presentation
Example validation errors:
ERROR: Invalid shapes in replacement JSON:
- Shape 'shape-99' not found on 'slide-0'. Available shapes: shape-0, shape-1, shape-4
- Slide 'slide-999' not found in inventory ERROR: Replacement text made overflow worse in these shapes:
- slide-0/shape-2: overflow worsened by 1.25" (was 0.00", now 1.25")Creating Thumbnail Grids
To create visual thumbnail grids of PowerPoint slides for quick analysis and reference:
python scripts/thumbnail.py template.pptx [output_prefix]Features:
- Creates:
thumbnails.jpg(orthumbnails-1.jpg,thumbnails-2.jpg, etc. for large decks) - Default: 5 columns, max 30 slides per grid (5×6)
- Custom prefix:
python scripts/thumbnail.py template.pptx my-grid - Note: The output prefix should include the path if you want output in a specific directory (e.g.,
workspace/my-grid) - Adjust columns:
--cols 4(range: 3-6, affects slides per grid) - Grid limits: 3 cols = 12 slides/grid, 4 cols = 20, 5 cols = 30, 6 cols = 42
- Slides are zero-indexed (Slide 0, Slide 1, etc.)
Use cases:
- Template analysis: Quickly understand slide layouts and design patterns
- Content review: Visual overview of entire presentation
- Navigation reference: Find specific slides by their visual appearance
- Quality check: Verify all slides are properly formatted
Examples:
# Basic usage
python scripts/thumbnail.py presentation.pptx
# Combine options: custom name, columns
python scripts/thumbnail.py template.pptx analysis --cols 4Converting Slides to Images
To visually analyze PowerPoint slides, convert them to images using a two-step process:
1. Convert PPTX to PDF:
soffice --headless --convert-to pdf template.pptx2. Convert PDF pages to JPEG images:
pdftoppm -jpeg -r 150 template.pdf slideThis creates files like slide-1.jpg, slide-2.jpg, etc.
Options:
-r 150: Sets resolution to 150 DPI (adjust for quality/size balance)-jpeg: Output JPEG format (use-pngfor PNG if preferred)-f N: First page to convert (e.g.,-f 2starts from page 2)-l N: Last page to convert (e.g.,-l 5stops at page 5)slide: Prefix for output files
Example for specific range:
pdftoppm -jpeg -r 150 -f 2 -l 5 template.pdf slide # Converts only pages 2-5Code Style Guidelines
IMPORTANT: When generating code for PPTX operations:
- Write concise code
- Avoid verbose variable names and redundant operations
- Avoid unnecessary print statements
Dependencies
Required dependencies (should already be installed):
- markitdown:
pip install "markitdown[pptx]"(for text extraction from presentations) - pptxgenjs:
npm install -g pptxgenjs(for creating presentations via html2pptx) - playwright:
npm install -g playwright(for HTML rendering in html2pptx) - react-icons:
npm install -g react-icons react react-dom(for icons) - sharp:
npm install -g sharp(for SVG rasterization and image processing) - LibreOffice:
sudo apt-get install libreoffice(for PDF conversion) - Poppler:
sudo apt-get install poppler-utils(for pdftoppm to convert PDF to images) - defusedxml:
pip install defusedxml(for secure XML parsing)
Dependencies for PPTX Skill
Overview
The PPTX skill requires Python packages for PowerPoint manipulation, Node.js packages for presentation generation, and system packages for conversion and image processing. This is the most comprehensive dependency set of all Office skills. This document provides complete installation instructions for all dependencies.
Dependency Categories
Required (Core Functionality)
These packages are required for basic PPTX skill functionality:
Python Packages:
markitdown>=0.1.0- Text extraction from presentations to markdownpython-pptx>=0.6.21- PowerPoint file manipulation and readingdefusedxml>=0.7.1- Safe XML parsing for OOXML operations
Required (Presentation Creation)
These packages are required for creating presentations from scratch:
Node.js Packages:
pptxgenjs- PowerPoint generation from HTML (html2pptx workflow)playwright- HTML rendering for accurate slide conversionsharp- SVG rasterization and image processingreact-icons- Icon support for presentationsreact- React library for icon renderingreact-dom- React DOM for icon rendering
Required (System Tools)
These system packages are required for conversion and analysis:
System Packages:
LibreOffice- PDF conversion (soffice command)poppler-utils- PDF to image conversion (pdftoppm command)
Installation Instructions
Quick Install (All Dependencies)
Install all packages for full PPTX functionality:
# Python packages
pip install markitdown python-pptx defusedxml
# Node.js packages (install globally)
npm install -g pptxgenjs playwright sharp react-icons react react-dom
# Initialize playwright browsers
playwright install
# System packages (see platform-specific instructions below)Platform-Specific Installation
macOS
# Python packages
pip install markitdown python-pptx defusedxml
# Node.js packages
npm install -g pptxgenjs playwright sharp react-icons react react-dom
playwright install
# System packages
brew install libreoffice poppler
# Verify LibreOffice
soffice --version
# Verify poppler
pdftoppm -vUbuntu/Debian Linux
# Python packages
pip install markitdown python-pptx defusedxml
# Node.js packages (ensure Node.js is installed first)
npm install -g pptxgenjs playwright sharp react-icons react react-dom
playwright install
# System packages
sudo apt-get update
sudo apt-get install -y libreoffice poppler-utils
# Verify installations
soffice --version
pdftoppm -vFedora/RHEL/CentOS
# Python packages
pip install markitdown python-pptx defusedxml
# Node.js packages
npm install -g pptxgenjs playwright sharp react-icons react react-dom
playwright install
# System packages
sudo dnf install -y libreoffice poppler-utils
# Verify installations
soffice --version
pdftoppm -vWindows
# Python packages
pip install markitdown python-pptx defusedxml
# Node.js packages
npm install -g pptxgenjs playwright sharp react-icons react react-dom
playwright install
# System packages via Chocolatey (recommended)
choco install libreoffice poppler
# Or download manually:
# - LibreOffice: https://www.libreoffice.org/download/
# - Poppler: https://github.com/oschwartz10612/poppler-windows/releases
# Add installation directories to PATH environment variableVerification
Verify installations with these commands:
Python Packages
# Check markitdown
python -c "import markitdown; print('markitdown installed')"
# Check python-pptx
python -c "import pptx; print(f'python-pptx {pptx.__version__}')"
# Check defusedxml
python -c "import defusedxml; print('defusedxml installed')"Node.js Packages
# Check pptxgenjs
npm list -g pptxgenjs
# Check playwright
npm list -g playwright
playwright --version
# Check sharp
npm list -g sharp
# Check react-icons
npm list -g react-iconsSystem Packages
# Check LibreOffice
soffice --version
# Check poppler-utils
pdftoppm -v
pdfinfo -vAutomated Verification
Use the verification script to check all dependencies:
cd .claude/skills
python common/verification/verify_skill.py pptxExpected output:
Verifying pptx skill dependencies...
Python packages:
markitdown: Installed
python-pptx: Installed
defusedxml: Installed
Node.js packages:
pptxgenjs: Installed
playwright: Installed
sharp: Installed
System commands:
soffice: Available
pdftoppm: Available
✓ pptx skill is readyDependency Details
markitdown
Purpose: Text extraction from PowerPoint presentations to markdown format
Capabilities:
- Convert .pptx to markdown
- Preserve slide structure
- Extract text content
- Support for multiple Office formats
License: MIT
Documentation: https://github.com/microsoft/markitdown
Installation Notes: Use markitdown[pptx] to ensure PPTX support dependencies are included
python-pptx
Purpose: PowerPoint file manipulation and reading in Python
Capabilities:
- Read existing .pptx files
- Access slide content and structure
- Extract text and shapes
- Modify presentations programmatically
- Access slide layouts and masters
License: MIT
Documentation: https://python-pptx.readthedocs.io/
defusedxml
Purpose: Secure XML parsing for OOXML operations
Capabilities:
- Safe XML parsing (prevents XML bombs and vulnerabilities)
- Drop-in replacement for standard XML libraries
- Used by OOXML manipulation scripts
License: Python Software Foundation License
Documentation: https://github.com/tiran/defusedxml
pptxgenjs
Purpose: PowerPoint generation from HTML (html2pptx workflow)
Capabilities:
- Convert HTML slides to PowerPoint
- Accurate positioning and styling
- Chart and table support
- Custom layouts and designs
- Multi-slide presentations
License: MIT
Documentation: https://gitbrent.github.io/PptxGenJS/
Installation Notes: Install globally with -g flag for command-line access
playwright
Purpose: HTML rendering for accurate slide conversion
Capabilities:
- Headless browser automation
- HTML to image rendering
- Screenshot capture
- Cross-browser support
License: Apache-2.0
Documentation: https://playwright.dev/
Installation Notes: After installing, run playwright install to download browser binaries
sharp
Purpose: SVG rasterization and image processing
Capabilities:
- Convert SVG to PNG
- Image resizing and optimization
- Format conversion
- High-performance image processing
License: Apache-2.0
Documentation: https://sharp.pixelplumbing.com/
react-icons
Purpose: Icon library for presentations
Capabilities:
- Access to popular icon sets
- SVG icon rendering
- Integration with React
License: MIT
Documentation: https://react-icons.github.io/react-icons/
Installation Notes: Requires react and react-dom as peer dependencies
LibreOffice
Purpose: PDF conversion from PowerPoint
Capabilities:
- Convert .pptx to .pdf headlessly
- Command-line batch processing
- Cross-platform support
- Preserves layouts and formatting
Command: soffice
License: Mozilla Public License 2.0
Documentation: https://www.libreoffice.org/
Size Warning: LibreOffice is a large download (500MB+)
poppler-utils
Purpose: PDF to image conversion for thumbnails
Capabilities:
- Convert PDF pages to images (pdftoppm)
- Extract PDF metadata (pdfinfo)
- Various PDF utility commands
- High-quality rendering
Commands: pdftoppm, pdfinfo, pdfimages
License: GPL
Documentation: https://poppler.freedesktop.org/
Troubleshooting
ImportError: No module named 'markitdown'
Solution: Install markitdown with PPTX support
pip install "markitdown[pptx]"ModuleNotFoundError: No module named 'pptx'
Solution: Install python-pptx
pip install python-pptxError: Cannot find module 'pptxgenjs'
Solution: Install pptxgenjs globally
npm install -g pptxgenjsplaywright: command not found
Solution: Install playwright and browser binaries
npm install -g playwright
playwright installsharp: Error loading shared library
Solution: Rebuild sharp or install system dependencies
npm uninstall -g sharp
npm install -g sharp
# On Linux, may need:
sudo apt-get install -y libvips-devCommand not found: soffice
Solution: Install LibreOffice
# macOS
brew install libreoffice
# Ubuntu/Debian
sudo apt-get install libreoffice
# Windows
choco install libreofficeCommand not found: pdftoppm
Solution: Install poppler-utils
# macOS
brew install poppler
# Ubuntu/Debian
sudo apt-get install poppler-utils
# Windows
choco install popplerplaywright install fails
Solution: Check Node.js version and install browsers manually
# Verify Node.js version (requires Node 14+)
node --version
# Try installing browsers separately
npx playwright install chromiumPermission denied errors
Solution: Use npm with --unsafe-perm flag or install in user directory
npm install -g --unsafe-perm pptxgenjs playwright sharp
# Or use a Node version manager like nvm
nvm use 18
npm install -g pptxgenjs playwright sharpMinimal Installation
For testing or specific workflows:
# Text extraction only
pip install markitdown defusedxml
# OOXML manipulation only
pip install python-pptx defusedxml
# Full creation workflow
pip install markitdown python-pptx defusedxml
npm install -g pptxgenjs playwright sharp react-icons react react-dom
playwright installDocker Installation
For containerized environments:
FROM node:18-slim
# Install Python
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
libreoffice \
poppler-utils \
libvips-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python packages
RUN pip3 install --no-cache-dir \
markitdown \
python-pptx \
defusedxml
# Install Node.js packages
RUN npm install -g \
pptxgenjs \
playwright \
sharp \
react-icons \
react \
react-dom
# Install playwright browsers
RUN playwright install --with-deps chromium
# Set working directory
WORKDIR /workspaceCI/CD Considerations
For GitHub Actions or other CI environments:
# .github/workflows/test.yml
- name: Install PPTX skill dependencies
run: |
# Python packages
pip install markitdown python-pptx defusedxml
# System packages
sudo apt-get update
sudo apt-get install -y libreoffice poppler-utils
# Node.js packages
npm install -g pptxgenjs playwright sharp react-icons react react-dom
playwright install --with-deps chromiumNote: Tests should skip gracefully if optional dependencies are missing.
Upgrading Dependencies
To upgrade to latest versions:
# Upgrade Python packages
pip install --upgrade markitdown python-pptx defusedxml
# Upgrade Node.js packages
npm update -g pptxgenjs playwright sharp react-icons react react-dom
# Update playwright browsers
playwright installDependency Licenses Summary
| Package | License | Commercial Use |
|---|---|---|
| markitdown | MIT | Yes |
| python-pptx | MIT | Yes |
| defusedxml | PSFL | Yes |
| pptxgenjs | MIT | Yes |
| playwright | Apache-2.0 | Yes |
| sharp | Apache-2.0 | Yes |
| react-icons | MIT | Yes |
| react | MIT | Yes |
| react-dom | MIT | Yes |
| LibreOffice | MPL-2.0 | Yes |
| poppler-utils | GPL | Yes (linking allowed) |
All dependencies are permissive licenses compatible with commercial use.
Node.js Version Requirements
Minimum Node.js Version: 14.x Recommended: 18.x LTS or higher
Check your Node.js version:
node --versionIf needed, upgrade Node.js:
# Using nvm (recommended)
nvm install 18
nvm use 18
# Or download from nodejs.orgCommon Installation Patterns
Development Environment
# Full installation for development
pip install markitdown python-pptx defusedxml
npm install -g pptxgenjs playwright sharp react-icons react react-dom
playwright install
brew install libreoffice poppler # macOSProduction Environment
# Minimal installation for specific workflows
pip install markitdown defusedxml # Text extraction only
# OR
pip install python-pptx defusedxml # OOXML manipulation only
# OR
# Full installation if creating presentationsTesting Environment
# Install all dependencies for comprehensive testing
pip install markitdown python-pptx defusedxml pytest
npm install -g pptxgenjs playwright sharp react-icons react react-dom
playwright install --with-deps
sudo apt-get install -y libreoffice poppler-utils # Linux---
Last Updated: 2025-11-08 Maintained By: amplihack project
PPTX Skill Usage Examples
This document provides practical examples of using the PPTX skill in Claude Code conversations. The PPTX skill enables comprehensive PowerPoint manipulation including creation, editing, and analysis.
Table of Contents
1. Creating Presentations from Scratch 2. Using Templates 3. Editing Existing Presentations 4. Analyzing Presentations 5. Design and Styling 6. Charts and Data 7. Template Workflows 8. Visual Validation 9. Conversion and Export 10. Advanced Techniques
---
1. Creating Presentations from Scratch
Example 1.1: Simple Presentation with Custom Design
User: Create a 5-slide presentation about renewable energy with a modern green color palette
Claude:
I'll create a presentation using the Sage & Terracotta palette which complements the renewable energy theme:
- Sage (#87A96B) - main color
- Terracotta (#E07A5F) - accent
- Cream (#F4F1DE) - background
- Charcoal (#2C2C2C) - text
Slides:
1. Title: "The Future of Renewable Energy"
2. Overview: Types of renewable energy sources
3. Benefits: Environmental and economic advantages
4. Challenges: Current obstacles and solutions
5. Conclusion: Path forward
[Creates HTML slides with custom styling, converts to PPTX using html2pptx]Example 1.2: Data-Driven Presentation with Charts
User: Create a quarterly sales presentation with charts showing revenue trends
Claude:
I'll create a presentation with two-column layouts for charts:
- Classic Blue palette for professional appearance
- Charts in one column, key insights in the other
- Data visualization with horizontal bar charts
[Generates slides with placeholder divs for charts, then uses PptxGenJS to add actual chart data]---
2. Using Templates
Example 2.1: Create Presentation from Corporate Template
User: Use this template.pptx to create a 10-slide investor pitch
Claude:
Workflow:
1. Extract template text and create thumbnail grid
2. Analyze template inventory (73 slides with various layouts)
3. Select appropriate slides for pitch structure:
- Slide 0: Title/Cover
- Slide 34: Title and body (for overview)
- Slide 45: Two-column with image placeholder
- Slide 50: Quote layout
- Slide 54: Closing slide
4. Rearrange slides: python scripts/rearrange.py template.pptx working.pptx 0,34,34,45,45,45,50,34,34,54
5. Extract text inventory
6. Generate replacement text preserving formatting
7. Apply replacements with replace.py
[Executes template workflow with appropriate content]Example 2.2: Duplicate and Customize Template Slides
User: Take slides 5-8 from template.pptx and create 12 slides by duplicating the pattern
Claude:
Using rearrange.py to duplicate and reorder:
python scripts/rearrange.py template.pptx output.pptx 5,6,7,8,5,6,7,8,5,6,7,8
This creates 12 slides by repeating the 5-8 pattern three times.
[Then applies custom text replacement for each duplicated set]---
3. Editing Existing Presentations
Example 3.1: Update Speaker Notes
User: Add speaker notes to slides 2, 3, and 5 in presentation.pptx
Claude:
Using OOXML workflow:
1. Unpack: python ooxml/scripts/unpack.py presentation.pptx workspace/
2. Edit notesSlides/notesSlide2.xml, notesSlide3.xml, notesSlide5.xml
3. Add <a:t> elements with speaker note content
4. Validate: python ooxml/scripts/validate.py workspace/ --original presentation.pptx
5. Pack: python ooxml/scripts/pack.py workspace/ presentation-with-notes.pptx
[Performs XML editing with proper OOXML structure]Example 3.2: Modify Slide Layouts
User: Change slide 4 from single-column to two-column layout
Claude:
OOXML approach:
1. Unpack presentation
2. Read ppt/slides/slide4.xml
3. Modify shape positions and dimensions:
- Split content into two <p:sp> elements
- Adjust <a:off> (position) and <a:ext> (size) for each shape
4. Validate changes
5. Repack presentation
[Applies precise XML modifications]---
4. Analyzing Presentations
Example 4.1: Extract Text Content
User: Extract all text from this presentation to markdown
Claude:
python -m markitdown presentation.pptx > content.mdResult: Clean markdown with slide structure preserved, perfect for analysis or documentation.
Example 4.2: Analyze Design and Typography
User: What fonts and colors are used in this template?
Claude:
I'll analyze the design:
1. Unpack presentation
2. Read ppt/theme/theme1.xml for color scheme
3. Examine ppt/slides/slide1.xml for actual font usage
4. Use grep to find all color and font references
Findings:
- Primary font: Montserrat
- Body font: Open Sans
- Color scheme:
- Dark Blue (#1C2833) - headers
- Gray (#708090) - body text
- Accent: Coral (#FF6B6B)---
5. Design and Styling
Example 5.1: Choose Appropriate Color Palette
User: Create a presentation for a tech startup - suggest a color palette
Claude:
For a tech startup, I recommend the "Deep Purple & Emerald" palette:
- Purple (#B165FB) - innovation and creativity
- Dark Blue (#181B24) - professional and tech-focused
- Emerald (#40695B) - growth and success
- White (#FFFFFF) - clean and modern
This palette conveys: innovation, professionalism, and forward-thinking.
Alternative: "Vibrant Orange" (#F96D00, #F2F2F2, #222831) for more energy.
[Creates presentation with selected palette]Example 5.2: Apply Visual Hierarchy
User: Make the titles really stand out in this presentation
Claude:
I'll apply strong visual hierarchy:
- Title slides: 72pt bold headers with 11pt body (extreme size contrast)
- Section headers: All-caps with wide letter spacing
- Body text: 14pt regular weight
- Accent elements: Thick 5pt underlines beneath headers
- Color contrast: Dark text on light backgrounds (WCAG AA compliant)
[Implements hierarchy in HTML templates]---
6. Charts and Data
Example 6.1: Create Presentation with Multiple Chart Types
User: Create slides with bar chart, line chart, and pie chart showing sales data
Claude:
Using two-column layouts (PREFERRED for charts):
- Header spanning full width
- Left column: Key insights (40% width)
- Right column: Chart (60% width)
Chart styling:
- Monochrome with single accent color
- Minimal gridlines
- Data labels on elements
- No legends (direct labeling)
[Creates placeholder divs in HTML, then adds charts with PptxGenJS API]Example 6.2: Data Visualization Best Practices
User: Show quarterly metrics in an impactful way
Claude:
I'll use oversized numbers for key metrics:
- 125pt font size for main numbers
- Small 18pt labels underneath
- Horizontal bar charts for comparisons
- Color-coded performance indicators (green/red)
- Full-slide layout for maximum impact
[Implements data visualization principles]---
7. Template Workflows
Example 7.1: Complete Template-Based Workflow
User: Create a project status report using corporate-template.pptx
Claude:
Complete workflow:
Step 1: Extract and analyze
python -m markitdown corporate-template.pptx > template-content.md
python scripts/thumbnail.py corporate-template.pptx
Step 2: Create inventory
[Analyzes 73 slides, identifies:
- Slide 0: Title
- Slide 12: Status overview layout
- Slide 25: Milestone timeline
- Slide 40: Risk assessment
- Slide 68: Next steps]
Step 3: Rearrange
python scripts/rearrange.py corporate-template.pptx working.pptx 0,12,25,25,40,68
Step 4: Extract inventory
python scripts/inventory.py working.pptx inventory.json
Step 5: Generate replacement text
[Creates replacement-text.json with:
- Proper paragraph formatting
- Bold titles
- Bullet lists (bullet: true, level: 0)
- Centered headers
- Theme colors]
Step 6: Apply replacements
python scripts/replace.py working.pptx replacement-text.json final.pptx
[Produces final presentation with consistent branding]Example 7.2: Handle Template with Image Placeholders
User: Use template slide 15 which has image placeholders
Claude:
Slide 15 has 3 image placeholder shapes.
Approach:
1. Use rearrange.py to duplicate slide 15
2. Extract inventory showing OBJECT placeholder types
3. In replacement JSON:
- Keep image placeholder shapes empty (no paragraphs field)
- They'll be cleared but structure preserved
- Manually insert images after using python-pptx or OOXML
Note: Template workflow focuses on text replacement.
For images, post-process with python-pptx or manual insertion.---
8. Visual Validation
Example 8.1: Generate and Review Thumbnails
User: Create thumbnails of this 50-slide presentation for quick review
Claude:
python scripts/thumbnail.py presentation.pptx review --cols 5Output:
- review-1.jpg: Slides 0-29 (5×6 grid)
- review-2.jpg: Slides 30-49 (4×5 grid)
Review thumbnails for:
- Text cutoff at slide edges
- Overlap between elements
- Positioning issues
- Contrast problems
- Consistent styling
Example 8.2: Fix Visual Issues After Validation
User: The thumbnail shows text cutoff on slides 3 and 7
Claude:
I'll fix the layout issues:
Slide 3: Header bar cutting off title
- Increase top margin from 0.5" to 0.8"
- Reduce title font size from 44pt to 36pt
Slide 7: Bullet list too long
- Split into two slides
- Or reduce font size from 18pt to 16pt
- Increase line spacing
[Regenerates HTML and converts to PPTX]
[Creates new thumbnails to verify fixes]---
9. Conversion and Export
Example 9.1: Convert Presentation to PDF
User: Convert presentation.pptx to PDF
Claude:
soffice --headless --convert-to pdf presentation.pptxResult: presentation.pdf with all slides preserved
Example 9.2: Convert Slides to Individual Images
User: Export each slide as a high-resolution image
Claude:
# First convert to PDF
soffice --headless --convert-to pdf presentation.pptx
# Then convert PDF pages to images (150 DPI)
pdftoppm -jpeg -r 150 presentation.pdf slideResult: slide-1.jpg, slide-2.jpg, slide-3.jpg, etc.
---
10. Advanced Techniques
Example 10.1: Combine Multiple Templates
User: Use title slide from template-A.pptx and content slides from template-B.pptx
Claude:
Multi-template workflow:
1. Extract slide 0 from template-A:
python scripts/rearrange.py template-A.pptx title.pptx 0
2. Extract slides 5,6,7 from template-B:
python scripts/rearrange.py template-B.pptx content.pptx 5,6,7
3. Manually combine using OOXML or python-pptx:
- Unpack both presentations
- Copy slide XML files from content/ to title/
- Update presentation.xml with new slide references
- Pack combined presentation
[Performs OOXML merging]Example 10.2: Batch Process Multiple Presentations
User: Apply the same text replacements to 10 different presentations
Claude:
for file in *.pptx; do
echo "Processing $file"
python scripts/inventory.py "$file" "inventory-${file}.json"
# Create custom replacement JSON for each
python scripts/replace.py "$file" "replacement-${file}.json" "updated-${file}"
doneEnables bulk updates while preserving each presentation's unique formatting.
Example 10.3: Extract and Analyze Slide Layouts
User: Show me all unique slide layouts in this template
Claude:
Analysis workflow:
1. Unpack template
2. Read ppt/presentation.xml for layout references
3. List all slideLayouts in ppt/slideLayouts/
4. For each layout, extract:
- Layout name
- Placeholder types and positions
- Master slide reference
Result:
- Title Slide (1 title, 1 subtitle)
- Title and Content (1 title, 1 body)
- Two Content (1 title, 2 content areas)
- Comparison (1 title, 2 columns)
- Title Only (1 title, full blank area)
- Blank (no placeholders)
- Content with Caption (1 body, 1 caption)
- Picture with Caption (1 picture, 1 caption)
- Section Header (1 title, 1 text)
[Creates layout reference document]---
Tips and Best Practices
Design Tips
1. Content-First Design: Analyze subject matter before choosing colors 2. Limit Palette: Use 3-5 colors maximum for consistency 3. Web-Safe Fonts: Stick to Arial, Helvetica, Georgia, Times New Roman, etc. 4. Contrast: Ensure WCAG AA compliance (4.5:1 for body text) 5. Hierarchy: Use size, weight, and color to guide attention
Layout Tips
1. Two-Column for Charts: Never stack charts below text vertically 2. White Space: Don't fill every inch - negative space improves readability 3. Alignment: Use consistent margins and alignment patterns 4. Grid Systems: Consider 3×3 or 4×4 modular grids for complex layouts
Template Workflow Tips
1. Always Create Thumbnails: Visual reference prevents errors 2. Verify Indices: Slides are 0-indexed (first slide = 0) 3. Check Inventory: Confirm shapes exist before referencing in replacement JSON 4. Preserve Formatting: Copy paragraph properties from inventory 5. Test Small: Try workflow on 2-3 slides before full presentation
Validation Tips
1. Immediate Validation: Validate after every OOXML edit 2. Visual Inspection: Always review thumbnails after generation 3. Check Overflow: Watch for text overflow warnings from replace.py 4. Test on Device: Preview on actual presentation device if possible
---
Common Patterns
Pattern 1: Professional Corporate Presentation
Structure: Title → Agenda → 3-5 Content Slides → Conclusion
Colors: Classic Blue or Charcoal & Red
Fonts: Arial or Helvetica
Layouts: Two-column for content, full-slide for dataPattern 2: Creative Pitch Deck
Structure: Hook → Problem → Solution → Product → Traction → Ask
Colors: Bold Red or Pink & Purple
Fonts: Mixed (Impact for headers, Arial for body)
Layouts: Full-bleed images, asymmetric columnsPattern 3: Data-Heavy Report
Structure: Executive Summary → Detailed Metrics → Analysis → Recommendations
Colors: Monochrome with single accent
Fonts: Monospace (Courier New) for numbers
Layouts: Charts in right column, insights in left---
Troubleshooting Common Issues
Issue: Text Cutoff in Thumbnails
Solution: Increase margins in HTML, reduce font sizes, or split content across slides
Issue: Template Slide Index Out of Range
Solution: Remember 0-indexing. Template with 73 slides has indices 0-72
Issue: Shape Not Found in Inventory
Solution: Check shape names exactly match inventory.json. Shapes are "shape-0", "shape-1", etc.
Issue: Overflow Warnings from replace.py
Solution: Shorten text, reduce font size, or choose shape with larger dimensions
Issue: Formatting Lost After Replacement
Solution: Ensure replacement JSON includes all paragraph properties (bold, alignment, etc.)
---
Additional Resources
- SKILL.md - Complete skill documentation
- DEPENDENCIES.md - Installation guide
- README.md - Integration overview
- Anthropic PPTX Skill
---
Last Updated: 2025-11-08 Maintained By: amplihack project
PPTX Skill Integration
Overview
The PPTX skill provides comprehensive PowerPoint presentation capabilities for Claude Code, enabling presentation creation, editing, analysis, and template-based workflows. This is the fourth and final Office skill integrated into amplihack, completing the full suite of document manipulation capabilities.
Capabilities
- Create presentations from scratch with custom designs and 18 color palettes
- Edit existing presentations using OOXML manipulation
- Extract text from presentations to markdown
- Create presentations from templates with slide rearrangement
- Generate visual thumbnail grids for analysis
- Replace placeholder text while preserving formatting
- Access raw XML for comments, speaker notes, and animations
- Convert presentations to PDF and images
- Support for charts, tables, and complex layouts
- Design principles with web-safe fonts and visual hierarchy
Integration with amplihack
The PPTX skill follows amplihack's brick philosophy:
- Self-contained: All PPTX processing code and dependencies isolated in this directory
- Clear contract: Well-defined inputs (PPTX files, HTML, templates) and outputs (presentations, thumbnails)
- Regeneratable: Can be rebuilt from SKILL.md specification
- Zero-BS: No placeholders - all functionality works or gracefully degrades
- Shared infrastructure: Uses common OOXML scripts (via symlink) shared with DOCX skill
Quick Start
1. Install dependencies (see DEPENDENCIES.md) 2. Verify installation: python ../common/verification/verify_skill.py pptx 3. Use the skill in Claude Code conversations
Example conversation:
User: Create a 5-slide presentation about AI trends with modern design
Claude: [Uses PPTX skill to create presentation with custom color palette and layouts]Architecture
- SKILL.md: Official skill definition from Anthropic (copied verbatim)
- README.md: This file - amplihack-specific integration notes
- DEPENDENCIES.md: Complete dependency documentation with installation instructions
- scripts/: PPTX-specific scripts (thumbnail.py, rearrange.py, inventory.py, replace.py, html2pptx.js)
- ooxml/: Symlink to ../common/ooxml for shared OOXML manipulation scripts
- examples/: Practical usage examples
- tests/: Verification tests that skip gracefully if dependencies missing
Dependencies
The PPTX skill has the most comprehensive dependencies of all Office skills:
Required (Core functionality):
- markitdown: Text extraction from presentations
- defusedxml: Safe XML parsing for OOXML
- python-pptx: PowerPoint file manipulation
Required (Presentation creation):
- pptxgenjs: PowerPoint generation via html2pptx
- playwright: HTML rendering
- sharp: Image processing and SVG rasterization
Required (System tools):
- LibreOffice: PDF conversion
- poppler-utils: PDF to image conversion
See DEPENDENCIES.md for detailed installation instructions.
Key Workflows
1. Creating from Scratch
Use html2pptx workflow for custom presentations:
- Choose from 18 pre-defined color palettes or create custom
- Apply design principles with visual hierarchy
- Generate HTML slides with proper dimensions
- Convert to PowerPoint with charts and tables
- Validate with thumbnail grids
2. Template-Based Creation
Work with existing templates:
- Extract template text and create thumbnail grids
- Analyze template inventory (slide layouts, placeholders)
- Rearrange and duplicate slides to match content
- Extract text inventory with formatting details
- Replace placeholder text while preserving design
- Validate replacements don't cause overflow
3. Editing Existing Presentations
OOXML manipulation for precise edits:
- Unpack presentation to XML files
- Edit slide content, notes, comments
- Validate changes immediately
- Repack to .pptx format
4. Analysis and Thumbnails
Visual analysis tools:
- Generate thumbnail grids (3-6 columns, configurable)
- Convert presentations to images for review
- Extract typography and color schemes
- Inventory slide structures and placeholders
Design Principles
The PPTX skill emphasizes thoughtful design:
- Content-informed design: Match colors and styles to subject matter
- 18 color palettes: From Classic Blue to Coastal Rose
- Web-safe fonts only: Arial, Helvetica, Times New Roman, Georgia, etc.
- Visual hierarchy: Clear contrast, proper sizing, clean alignment
- Layout best practices: Two-column for charts/tables, never vertical stacking
- Consistency: Repeat patterns and spacing across slides
Testing
Run tests to verify the skill:
cd .claude/skills/pptx
pytest tests/ -vTests will skip gracefully if dependencies are not installed, showing which features are available.
Usage Examples
See examples/example_usage.md for common workflows:
- Creating branded presentations from scratch
- Using templates for consistent design
- Editing existing presentations
- Generating presentation thumbnails
- Analyzing presentation structures
- Converting presentations to images
- Template-based workflows with inventory
- Design palette selection strategies
- Chart and data visualization
- Multi-column layout best practices
Known Limitations
1. Heavy dependencies: Requires Node.js packages (pptxgenjs, playwright, sharp) and system tools (LibreOffice, poppler) 2. Platform differences: Some tools may require different installation on Windows 3. Template complexity: Complex templates with many overlapping shapes require careful inventory analysis 4. HTML to PPTX conversion: Requires careful dimension management and visual validation 5. Large presentations: Memory-intensive for presentations with many slides or large images 6. Font limitations: Limited to web-safe fonts for cross-platform compatibility
Philosophy Compliance
This integration follows amplihack's core principles:
- Ruthless simplicity: Leverages established tools (pptxgenjs, python-pptx, OOXML)
- Modular design: PPTX skill is a brick with clear studs (public API)
- Explicit dependencies: All requirements documented, no automatic installation
- Graceful degradation: Tests skip cleanly if dependencies missing
- Documentation-first: Complete docs before code execution
- Shared infrastructure: Uses common OOXML scripts to avoid duplication
OOXML Shared Infrastructure
The PPTX skill shares OOXML manipulation scripts with the DOCX skill:
- Location:
../common/ooxml/(symlinked as./ooxml/) - Shared scripts: unpack.py, pack.py, validate.py
- PPTX-specific scripts: thumbnail.py, rearrange.py, inventory.py, replace.py (in ./scripts/)
- Single source of truth: OOXML operations centralized to avoid duplication
See ../common/ooxml/README.md for OOXML infrastructure details.
Troubleshooting
Skill not recognized:
1. Verify SKILL.md exists in this directory 2. Check YAML frontmatter is valid 3. Restart Claude Code session
ImportError for dependencies:
1. Run verification script: python ../common/verification/verify_skill.py pptx 2. Install missing dependencies from DEPENDENCIES.md 3. Re-run tests to confirm
Node packages not found:
1. Install globally: npm install -g pptxgenjs playwright sharp 2. Verify: npm list -g pptxgenjs 3. Check PATH includes npm global bin directory
LibreOffice conversion fails:
1. Verify LibreOffice installed: soffice --version 2. Check headless mode works: soffice --headless --convert-to pdf test.pptx 3. Install if missing: brew install libreoffice (macOS) or sudo apt-get install libreoffice (Linux)
Thumbnail generation fails:
1. Check LibreOffice and poppler-utils installed 2. Verify pdftoppm available: pdftoppm -v 3. Install poppler: brew install poppler (macOS) or sudo apt-get install poppler-utils (Linux)
Template workflow errors:
1. Verify slide indices are 0-based (first slide = 0) 2. Check inventory JSON structure matches expected format 3. Validate shape names exist in inventory before replacement 4. Review error messages for specific validation failures
Contributing
This skill is sourced from Anthropic's official skills repository. For issues:
1. amplihack integration issues: Open issue in amplihack repository 2. Skill functionality issues: Report to Anthropic skills repository 3. Documentation improvements: Submit PR to amplihack
References
- SKILL.md - Official skill documentation
- DEPENDENCIES.md - Complete dependency list
- examples/example_usage.md - Usage examples
- tests/test_pptx_skill.py - Verification tests
- ../common/ooxml/README.md - OOXML infrastructure
- Anthropic Skills Repository
License
The PPTX skill is provided by Anthropic under their proprietary license. See SKILL.md and Anthropic's LICENSE.txt for complete terms. The amplihack integration code (this README, DEPENDENCIES.md, tests, examples) follows amplihack's license.
---
Integration Status: Complete (PR #4) Last Updated: 2025-11-08 Maintained By: amplihack project Completion Milestone: ALL 4 Office skills now integrated (PDF, XLSX, DOCX, PPTX)
/**
* html2pptx - Convert HTML slide to pptxgenjs slide with positioned elements
*
* USAGE:
* const pptx = new pptxgen();
* pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions
*
* const { slide, placeholders } = await html2pptx('slide.html', pptx);
* slide.addChart(pptx.charts.LINE, data, placeholders[0]);
*
* await pptx.writeFile('output.pptx');
*
* FEATURES:
* - Converts HTML to PowerPoint with accurate positioning
* - Supports text, images, shapes, and bullet lists
* - Extracts placeholder elements (class="placeholder") with positions
* - Handles CSS gradients, borders, and margins
*
* VALIDATION:
* - Uses body width/height from HTML for viewport sizing
* - Throws error if HTML dimensions don't match presentation layout
* - Throws error if content overflows body (with overflow details)
*
* RETURNS:
* { slide, placeholders } where placeholders is an array of { id, x, y, w, h }
*/
const { chromium } = require("playwright");
const path = require("path");
const sharp = require("sharp");
const PT_PER_PX = 0.75;
const PX_PER_IN = 96;
const EMU_PER_IN = 914400;
// Helper: Get body dimensions and check for overflow
async function getBodyDimensions(page) {
const bodyDimensions = await page.evaluate(() => {
const body = document.body;
const style = window.getComputedStyle(body);
return {
width: parseFloat(style.width),
height: parseFloat(style.height),
scrollWidth: body.scrollWidth,
scrollHeight: body.scrollHeight,
};
});
const errors = [];
const widthOverflowPx = Math.max(0, bodyDimensions.scrollWidth - bodyDimensions.width - 1);
const heightOverflowPx = Math.max(0, bodyDimensions.scrollHeight - bodyDimensions.height - 1);
const widthOverflowPt = widthOverflowPx * PT_PER_PX;
const heightOverflowPt = heightOverflowPx * PT_PER_PX;
if (widthOverflowPt > 0 || heightOverflowPt > 0) {
const directions = [];
if (widthOverflowPt > 0) directions.push(`${widthOverflowPt.toFixed(1)}pt horizontally`);
if (heightOverflowPt > 0) directions.push(`${heightOverflowPt.toFixed(1)}pt vertically`);
const reminder =
heightOverflowPt > 0 ? ' (Remember: leave 0.5" margin at bottom of slide)' : "";
errors.push(`HTML content overflows body by ${directions.join(" and ")}${reminder}`);
}
return { ...bodyDimensions, errors };
}
// Helper: Validate dimensions match presentation layout
function validateDimensions(bodyDimensions, pres) {
const errors = [];
const widthInches = bodyDimensions.width / PX_PER_IN;
const heightInches = bodyDimensions.height / PX_PER_IN;
if (pres.presLayout) {
const layoutWidth = pres.presLayout.width / EMU_PER_IN;
const layoutHeight = pres.presLayout.height / EMU_PER_IN;
if (Math.abs(layoutWidth - widthInches) > 0.1 || Math.abs(layoutHeight - heightInches) > 0.1) {
errors.push(
`HTML dimensions (${widthInches.toFixed(1)}" × ${heightInches.toFixed(1)}") ` +
`don't match presentation layout (${layoutWidth.toFixed(1)}" × ${layoutHeight.toFixed(1)}")`
);
}
}
return errors;
}
function validateTextBoxPosition(slideData, bodyDimensions) {
const errors = [];
const slideHeightInches = bodyDimensions.height / PX_PER_IN;
const minBottomMargin = 0.5; // 0.5 inches from bottom
for (const el of slideData.elements) {
// Check text elements (p, h1-h6, list)
if (["p", "h1", "h2", "h3", "h4", "h5", "h6", "list"].includes(el.type)) {
const fontSize = el.style?.fontSize || 0;
const bottomEdge = el.position.y + el.position.h;
const distanceFromBottom = slideHeightInches - bottomEdge;
if (fontSize > 12 && distanceFromBottom < minBottomMargin) {
const getText = () => {
if (typeof el.text === "string") return el.text;
if (Array.isArray(el.text)) return el.text.find((t) => t.text)?.text || "";
if (Array.isArray(el.items)) return el.items.find((item) => item.text)?.text || "";
return "";
};
const textPrefix = getText().substring(0, 50) + (getText().length > 50 ? "..." : "");
errors.push(
`Text box "${textPrefix}" ends too close to bottom edge ` +
`(${distanceFromBottom.toFixed(2)}" from bottom, minimum ${minBottomMargin}" required)`
);
}
}
}
return errors;
}
// Helper: Add background to slide
async function addBackground(slideData, targetSlide, tmpDir) {
if (slideData.background.type === "image" && slideData.background.path) {
let imagePath = slideData.background.path.startsWith("file://")
? slideData.background.path.replace("file://", "")
: slideData.background.path;
targetSlide.background = { path: imagePath };
} else if (slideData.background.type === "color" && slideData.background.value) {
targetSlide.background = { color: slideData.background.value };
}
}
// Helper: Add elements to slide
function addElements(slideData, targetSlide, pres) {
for (const el of slideData.elements) {
if (el.type === "image") {
let imagePath = el.src.startsWith("file://") ? el.src.replace("file://", "") : el.src;
targetSlide.addImage({
path: imagePath,
x: el.position.x,
y: el.position.y,
w: el.position.w,
h: el.position.h,
});
} else if (el.type === "line") {
targetSlide.addShape(pres.ShapeType.line, {
x: el.x1,
y: el.y1,
w: el.x2 - el.x1,
h: el.y2 - el.y1,
line: { color: el.color, width: el.width },
});
} else if (el.type === "shape") {
const shapeOptions = {
x: el.position.x,
y: el.position.y,
w: el.position.w,
h: el.position.h,
shape: el.shape.rectRadius > 0 ? pres.ShapeType.roundRect : pres.ShapeType.rect,
};
if (el.shape.fill) {
shapeOptions.fill = { color: el.shape.fill };
if (el.shape.transparency != null) shapeOptions.fill.transparency = el.shape.transparency;
}
if (el.shape.line) shapeOptions.line = el.shape.line;
if (el.shape.rectRadius > 0) shapeOptions.rectRadius = el.shape.rectRadius;
if (el.shape.shadow) shapeOptions.shadow = el.shape.shadow;
targetSlide.addText(el.text || "", shapeOptions);
} else if (el.type === "list") {
const listOptions = {
x: el.position.x,
y: el.position.y,
w: el.position.w,
h: el.position.h,
fontSize: el.style.fontSize,
fontFace: el.style.fontFace,
color: el.style.color,
align: el.style.align,
valign: "top",
lineSpacing: el.style.lineSpacing,
paraSpaceBefore: el.style.paraSpaceBefore,
paraSpaceAfter: el.style.paraSpaceAfter,
margin: el.style.margin,
};
if (el.style.margin) listOptions.margin = el.style.margin;
targetSlide.addText(el.items, listOptions);
} else {
// Check if text is single-line (height suggests one line)
const lineHeight = el.style.lineSpacing || el.style.fontSize * 1.2;
const isSingleLine = el.position.h <= lineHeight * 1.5;
let adjustedX = el.position.x;
let adjustedW = el.position.w;
// Make single-line text 2% wider to account for underestimate
if (isSingleLine) {
const widthIncrease = el.position.w * 0.02;
const align = el.style.align;
if (align === "center") {
// Center: expand both sides
adjustedX = el.position.x - widthIncrease / 2;
adjustedW = el.position.w + widthIncrease;
} else if (align === "right") {
// Right: expand to the left
adjustedX = el.position.x - widthIncrease;
adjustedW = el.position.w + widthIncrease;
} else {
// Left (default): expand to the right
adjustedW = el.position.w + widthIncrease;
}
}
const textOptions = {
x: adjustedX,
y: el.position.y,
w: adjustedW,
h: el.position.h,
fontSize: el.style.fontSize,
fontFace: el.style.fontFace,
color: el.style.color,
bold: el.style.bold,
italic: el.style.italic,
underline: el.style.underline,
valign: "top",
lineSpacing: el.style.lineSpacing,
paraSpaceBefore: el.style.paraSpaceBefore,
paraSpaceAfter: el.style.paraSpaceAfter,
inset: 0, // Remove default PowerPoint internal padding
};
if (el.style.align) textOptions.align = el.style.align;
if (el.style.margin) textOptions.margin = el.style.margin;
if (el.style.rotate !== undefined) textOptions.rotate = el.style.rotate;
if (el.style.transparency !== null && el.style.transparency !== undefined)
textOptions.transparency = el.style.transparency;
targetSlide.addText(el.text, textOptions);
}
}
}
// Helper: Extract slide data from HTML page
async function extractSlideData(page) {
return await page.evaluate(() => {
const PT_PER_PX = 0.75;
const PX_PER_IN = 96;
// Fonts that are single-weight and should not have bold applied
// (applying bold causes PowerPoint to use faux bold which makes text wider)
const SINGLE_WEIGHT_FONTS = ["impact"];
// Helper: Check if a font should skip bold formatting
const shouldSkipBold = (fontFamily) => {
if (!fontFamily) return false;
const normalizedFont = fontFamily.toLowerCase().replace(/['"]/g, "").split(",")[0].trim();
return SINGLE_WEIGHT_FONTS.includes(normalizedFont);
};
// Unit conversion helpers
const pxToInch = (px) => px / PX_PER_IN;
const pxToPoints = (pxStr) => parseFloat(pxStr) * PT_PER_PX;
const rgbToHex = (rgbStr) => {
// Handle transparent backgrounds by defaulting to white
if (rgbStr === "rgba(0, 0, 0, 0)" || rgbStr === "transparent") return "FFFFFF";
const match = rgbStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (!match) return "FFFFFF";
return match
.slice(1)
.map((n) => parseInt(n).toString(16).padStart(2, "0"))
.join("");
};
const extractAlpha = (rgbStr) => {
const match = rgbStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
if (!match || !match[4]) return null;
const alpha = parseFloat(match[4]);
return Math.round((1 - alpha) * 100);
};
const applyTextTransform = (text, textTransform) => {
if (textTransform === "uppercase") return text.toUpperCase();
if (textTransform === "lowercase") return text.toLowerCase();
if (textTransform === "capitalize") {
return text.replace(/\b\w/g, (c) => c.toUpperCase());
}
return text;
};
// Extract rotation angle from CSS transform and writing-mode
const getRotation = (transform, writingMode) => {
let angle = 0;
// Handle writing-mode first
// PowerPoint: 90° = text rotated 90° clockwise (reads top to bottom, letters upright)
// PowerPoint: 270° = text rotated 270° clockwise (reads bottom to top, letters upright)
if (writingMode === "vertical-rl") {
// vertical-rl alone = text reads top to bottom = 90° in PowerPoint
angle = 90;
} else if (writingMode === "vertical-lr") {
// vertical-lr alone = text reads bottom to top = 270° in PowerPoint
angle = 270;
}
// Then add any transform rotation
if (transform && transform !== "none") {
// Try to match rotate() function
const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
if (rotateMatch) {
angle += parseFloat(rotateMatch[1]);
} else {
// Browser may compute as matrix - extract rotation from matrix
const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
if (matrixMatch) {
const values = matrixMatch[1].split(",").map(parseFloat);
// matrix(a, b, c, d, e, f) where rotation = atan2(b, a)
const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI);
angle += Math.round(matrixAngle);
}
}
}
// Normalize to 0-359 range
angle = angle % 360;
if (angle < 0) angle += 360;
return angle === 0 ? null : angle;
};
// Get position/dimensions accounting for rotation
const getPositionAndSize = (el, rect, rotation) => {
if (rotation === null) {
return { x: rect.left, y: rect.top, w: rect.width, h: rect.height };
}
// For 90° or 270° rotations, swap width and height
// because PowerPoint applies rotation to the original (unrotated) box
const isVertical = rotation === 90 || rotation === 270;
if (isVertical) {
// The browser shows us the rotated dimensions (tall box for vertical text)
// But PowerPoint needs the pre-rotation dimensions (wide box that will be rotated)
// So we swap: browser's height becomes PPT's width, browser's width becomes PPT's height
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
return {
x: centerX - rect.height / 2,
y: centerY - rect.width / 2,
w: rect.height,
h: rect.width,
};
}
// For other rotations, use element's offset dimensions
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
return {
x: centerX - el.offsetWidth / 2,
y: centerY - el.offsetHeight / 2,
w: el.offsetWidth,
h: el.offsetHeight,
};
};
// Parse CSS box-shadow into PptxGenJS shadow properties
const parseBoxShadow = (boxShadow) => {
if (!boxShadow || boxShadow === "none") return null;
// Browser computed style format: "rgba(0, 0, 0, 0.3) 2px 2px 8px 0px [inset]"
// CSS format: "[inset] 2px 2px 8px 0px rgba(0, 0, 0, 0.3)"
const insetMatch = boxShadow.match(/inset/);
// IMPORTANT: PptxGenJS/PowerPoint doesn't properly support inset shadows
// Only process outer shadows to avoid file corruption
if (insetMatch) return null;
// Extract color first (rgba or rgb at start)
const colorMatch = boxShadow.match(/rgba?\([^)]+\)/);
// Extract numeric values (handles both px and pt units)
const parts = boxShadow.match(/([-\d.]+)(px|pt)/g);
if (!parts || parts.length < 2) return null;
const offsetX = parseFloat(parts[0]);
const offsetY = parseFloat(parts[1]);
const blur = parts.length > 2 ? parseFloat(parts[2]) : 0;
// Calculate angle from offsets (in degrees, 0 = right, 90 = down)
let angle = 0;
if (offsetX !== 0 || offsetY !== 0) {
angle = Math.atan2(offsetY, offsetX) * (180 / Math.PI);
if (angle < 0) angle += 360;
}
// Calculate offset distance (hypotenuse)
const offset = Math.sqrt(offsetX * offsetX + offsetY * offsetY) * PT_PER_PX;
// Extract opacity from rgba
let opacity = 0.5;
if (colorMatch) {
const opacityMatch = colorMatch[0].match(/[\d.]+\)$/);
if (opacityMatch) {
opacity = parseFloat(opacityMatch[0].replace(")", ""));
}
}
return {
type: "outer",
angle: Math.round(angle),
blur: blur * 0.75, // Convert to points
color: colorMatch ? rgbToHex(colorMatch[0]) : "000000",
offset: offset,
opacity,
};
};
// Parse inline formatting tags (<b>, <i>, <u>, <strong>, <em>, <span>) into text runs
const parseInlineFormatting = (
element,
baseOptions = {},
runs = [],
baseTextTransform = (x) => x
) => {
let prevNodeIsText = false;
element.childNodes.forEach((node) => {
let textTransform = baseTextTransform;
const isText = node.nodeType === Node.TEXT_NODE || node.tagName === "BR";
if (isText) {
const text =
node.tagName === "BR" ? "\n" : textTransform(node.textContent.replace(/\s+/g, " "));
const prevRun = runs[runs.length - 1];
if (prevNodeIsText && prevRun) {
prevRun.text += text;
} else {
runs.push({ text, options: { ...baseOptions } });
}
} else if (node.nodeType === Node.ELEMENT_NODE && node.textContent.trim()) {
const options = { ...baseOptions };
const computed = window.getComputedStyle(node);
// Handle inline elements with computed styles
if (
node.tagName === "SPAN" ||
node.tagName === "B" ||
node.tagName === "STRONG" ||
node.tagName === "I" ||
node.tagName === "EM" ||
node.tagName === "U"
) {
const isBold = computed.fontWeight === "bold" || parseInt(computed.fontWeight) >= 600;
if (isBold && !shouldSkipBold(computed.fontFamily)) options.bold = true;
if (computed.fontStyle === "italic") options.italic = true;
if (computed.textDecoration && computed.textDecoration.includes("underline"))
options.underline = true;
if (computed.color && computed.color !== "rgb(0, 0, 0)") {
options.color = rgbToHex(computed.color);
const transparency = extractAlpha(computed.color);
if (transparency !== null) options.transparency = transparency;
}
if (computed.fontSize) options.fontSize = pxToPoints(computed.fontSize);
// Apply text-transform on the span element itself
if (computed.textTransform && computed.textTransform !== "none") {
const transformStr = computed.textTransform;
textTransform = (text) => applyTextTransform(text, transformStr);
}
// Validate: Check for margins on inline elements
if (computed.marginLeft && parseFloat(computed.marginLeft) > 0) {
errors.push(
`Inline element <${node.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`
);
}
if (computed.marginRight && parseFloat(computed.marginRight) > 0) {
errors.push(
`Inline element <${node.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`
);
}
if (computed.marginTop && parseFloat(computed.marginTop) > 0) {
errors.push(
`Inline element <${node.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`
);
}
if (computed.marginBottom && parseFloat(computed.marginBottom) > 0) {
errors.push(
`Inline element <${node.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`
);
}
// Recursively process the child node. This will flatten nested spans into multiple runs.
parseInlineFormatting(node, options, runs, textTransform);
}
}
prevNodeIsText = isText;
});
// Trim leading space from first run and trailing space from last run
if (runs.length > 0) {
runs[0].text = runs[0].text.replace(/^\s+/, "");
runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\s+$/, "");
}
return runs.filter((r) => r.text.length > 0);
};
// Extract background from body (image or color)
const body = document.body;
const bodyStyle = window.getComputedStyle(body);
const bgImage = bodyStyle.backgroundImage;
const bgColor = bodyStyle.backgroundColor;
// Collect validation errors
const errors = [];
// Validate: Check for CSS gradients
if (bgImage && (bgImage.includes("linear-gradient") || bgImage.includes("radial-gradient"))) {
errors.push(
"CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, " +
"then reference with background-image: url('gradient.png')"
);
}
let background;
if (bgImage && bgImage !== "none") {
// Extract URL from url("...") or url(...)
const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
if (urlMatch) {
background = {
type: "image",
path: urlMatch[1],
};
} else {
background = {
type: "color",
value: rgbToHex(bgColor),
};
}
} else {
background = {
type: "color",
value: rgbToHex(bgColor),
};
}
// Process all elements
const elements = [];
const placeholders = [];
const textTags = ["P", "H1", "H2", "H3", "H4", "H5", "H6", "UL", "OL", "LI"];
const processed = new Set();
document.querySelectorAll("*").forEach((el) => {
if (processed.has(el)) return;
// Validate text elements don't have backgrounds, borders, or shadows
if (textTags.includes(el.tagName)) {
const computed = window.getComputedStyle(el);
const hasBg = computed.backgroundColor && computed.backgroundColor !== "rgba(0, 0, 0, 0)";
const hasBorder =
(computed.borderWidth && parseFloat(computed.borderWidth) > 0) ||
(computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) ||
(computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) ||
(computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) ||
(computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0);
const hasShadow = computed.boxShadow && computed.boxShadow !== "none";
if (hasBg || hasBorder || hasShadow) {
errors.push(
`Text element <${el.tagName.toLowerCase()}> has ${hasBg ? "background" : hasBorder ? "border" : "shadow"}. ` +
"Backgrounds, borders, and shadows are only supported on <div> elements, not text elements."
);
return;
}
}
// Extract placeholder elements (for charts, etc.)
if (el.className && el.className.includes("placeholder")) {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
errors.push(
`Placeholder "${el.id || "unnamed"}" has ${rect.width === 0 ? "width: 0" : "height: 0"}. Check the layout CSS.`
);
} else {
placeholders.push({
id: el.id || `placeholder-${placeholders.length}`,
x: pxToInch(rect.left),
y: pxToInch(rect.top),
w: pxToInch(rect.width),
h: pxToInch(rect.height),
});
}
processed.add(el);
return;
}
// Extract images
if (el.tagName === "IMG") {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
elements.push({
type: "image",
src: el.src,
position: {
x: pxToInch(rect.left),
y: pxToInch(rect.top),
w: pxToInch(rect.width),
h: pxToInch(rect.height),
},
});
processed.add(el);
return;
}
}
// Extract DIVs with backgrounds/borders as shapes
const isContainer = el.tagName === "DIV" && !textTags.includes(el.tagName);
if (isContainer) {
const computed = window.getComputedStyle(el);
const hasBg = computed.backgroundColor && computed.backgroundColor !== "rgba(0, 0, 0, 0)";
// Validate: Check for unwrapped text content in DIV
for (const node of el.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text) {
errors.push(
`DIV element contains unwrapped text "${text.substring(0, 50)}${text.length > 50 ? "..." : ""}". ` +
"All text must be wrapped in <p>, <h1>-<h6>, <ul>, or <ol> tags to appear in PowerPoint."
);
}
}
}
// Check for background images on shapes
const bgImage = computed.backgroundImage;
if (bgImage && bgImage !== "none") {
errors.push(
"Background images on DIV elements are not supported. " +
"Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images."
);
return;
}
// Check for borders - both uniform and partial
const borderTop = computed.borderTopWidth;
const borderRight = computed.borderRightWidth;
const borderBottom = computed.borderBottomWidth;
const borderLeft = computed.borderLeftWidth;
const borders = [borderTop, borderRight, borderBottom, borderLeft].map(
(b) => parseFloat(b) || 0
);
const hasBorder = borders.some((b) => b > 0);
const hasUniformBorder = hasBorder && borders.every((b) => b === borders[0]);
const borderLines = [];
if (hasBorder && !hasUniformBorder) {
const rect = el.getBoundingClientRect();
const x = pxToInch(rect.left);
const y = pxToInch(rect.top);
const w = pxToInch(rect.width);
const h = pxToInch(rect.height);
// Collect lines to add after shape (inset by half the line width to center on edge)
if (parseFloat(borderTop) > 0) {
const widthPt = pxToPoints(borderTop);
const inset = widthPt / 72 / 2; // Convert points to inches, then half
borderLines.push({
type: "line",
x1: x,
y1: y + inset,
x2: x + w,
y2: y + inset,
width: widthPt,
color: rgbToHex(computed.borderTopColor),
});
}
if (parseFloat(borderRight) > 0) {
const widthPt = pxToPoints(borderRight);
const inset = widthPt / 72 / 2;
borderLines.push({
type: "line",
x1: x + w - inset,
y1: y,
x2: x + w - inset,
y2: y + h,
width: widthPt,
color: rgbToHex(computed.borderRightColor),
});
}
if (parseFloat(borderBottom) > 0) {
const widthPt = pxToPoints(borderBottom);
const inset = widthPt / 72 / 2;
borderLines.push({
type: "line",
x1: x,
y1: y + h - inset,
x2: x + w,
y2: y + h - inset,
width: widthPt,
color: rgbToHex(computed.borderBottomColor),
});
}
if (parseFloat(borderLeft) > 0) {
const widthPt = pxToPoints(borderLeft);
const inset = widthPt / 72 / 2;
borderLines.push({
type: "line",
x1: x + inset,
y1: y,
x2: x + inset,
y2: y + h,
width: widthPt,
color: rgbToHex(computed.borderLeftColor),
});
}
}
if (hasBg || hasBorder) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const shadow = parseBoxShadow(computed.boxShadow);
// Only add shape if there's background or uniform border
if (hasBg || hasUniformBorder) {
elements.push({
type: "shape",
text: "", // Shape only - child text elements render on top
position: {
x: pxToInch(rect.left),
y: pxToInch(rect.top),
w: pxToInch(rect.width),
h: pxToInch(rect.height),
},
shape: {
fill: hasBg ? rgbToHex(computed.backgroundColor) : null,
transparency: hasBg ? extractAlpha(computed.backgroundColor) : null,
line: hasUniformBorder
? {
color: rgbToHex(computed.borderColor),
width: pxToPoints(computed.borderWidth),
}
: null,
// Convert border-radius to rectRadius (in inches)
// % values: 50%+ = circle (1), <50% = percentage of min dimension
// pt values: divide by 72 (72pt = 1 inch)
// px values: divide by 96 (96px = 1 inch)
rectRadius: (() => {
const radius = computed.borderRadius;
const radiusValue = parseFloat(radius);
if (radiusValue === 0) return 0;
if (radius.includes("%")) {
if (radiusValue >= 50) return 1;
// Calculate percentage of smaller dimension
const minDim = Math.min(rect.width, rect.height);
return (radiusValue / 100) * pxToInch(minDim);
}
if (radius.includes("pt")) return radiusValue / 72;
return radiusValue / PX_PER_IN;
})(),
shadow: shadow,
},
});
}
// Add partial border lines
elements.push(...borderLines);
processed.add(el);
return;
}
}
}
// Extract bullet lists as single text block
if (el.tagName === "UL" || el.tagName === "OL") {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const liElements = Array.from(el.querySelectorAll("li"));
const items = [];
const ulComputed = window.getComputedStyle(el);
const ulPaddingLeftPt = pxToPoints(ulComputed.paddingLeft);
// Split: margin-left for bullet position, indent for text position
// margin-left + indent = ul padding-left
const marginLeft = ulPaddingLeftPt * 0.5;
const textIndent = ulPaddingLeftPt * 0.5;
liElements.forEach((li, idx) => {
const isLast = idx === liElements.length - 1;
const runs = parseInlineFormatting(li, { breakLine: false });
// Clean manual bullets from first run
if (runs.length > 0) {
runs[0].text = runs[0].text.replace(/^[•\-\*▪▸]\s*/, "");
runs[0].options.bullet = { indent: textIndent };
}
// Set breakLine on last run
if (runs.length > 0 && !isLast) {
runs[runs.length - 1].options.breakLine = true;
}
items.push(...runs);
});
const computed = window.getComputedStyle(liElements[0] || el);
elements.push({
type: "list",
items: items,
position: {
x: pxToInch(rect.left),
y: pxToInch(rect.top),
w: pxToInch(rect.width),
h: pxToInch(rect.height),
},
style: {
fontSize: pxToPoints(computed.fontSize),
fontFace: computed.fontFamily.split(",")[0].replace(/['"]/g, "").trim(),
color: rgbToHex(computed.color),
transparency: extractAlpha(computed.color),
align: computed.textAlign === "start" ? "left" : computed.textAlign,
lineSpacing:
computed.lineHeight && computed.lineHeight !== "normal"
? pxToPoints(computed.lineHeight)
: null,
paraSpaceBefore: 0,
paraSpaceAfter: pxToPoints(computed.marginBottom),
// PptxGenJS margin array is [left, right, bottom, top]
margin: [marginLeft, 0, 0, 0],
},
});
liElements.forEach((li) => processed.add(li));
processed.add(el);
return;
}
// Extract text elements (P, H1, H2, etc.)
if (!textTags.includes(el.tagName)) return;
const rect = el.getBoundingClientRect();
const text = el.textContent.trim();
if (rect.width === 0 || rect.height === 0 || !text) return;
// Validate: Check for manual bullet symbols in text elements (not in lists)
if (el.tagName !== "LI" && /^[•\-\*▪▸○●◆◇■□]\s/.test(text.trimStart())) {
errors.push(
`Text element <${el.tagName.toLowerCase()}> starts with bullet symbol "${text.substring(0, 20)}...". ` +
"Use <ul> or <ol> lists instead of manual bullet symbols."
);
return;
}
const computed = window.getComputedStyle(el);
const rotation = getRotation(computed.transform, computed.writingMode);
const { x, y, w, h } = getPositionAndSize(el, rect, rotation);
const baseStyle = {
fontSize: pxToPoints(computed.fontSize),
fontFace: computed.fontFamily.split(",")[0].replace(/['"]/g, "").trim(),
color: rgbToHex(computed.color),
align: computed.textAlign === "start" ? "left" : computed.textAlign,
lineSpacing: pxToPoints(computed.lineHeight),
paraSpaceBefore: pxToPoints(computed.marginTop),
paraSpaceAfter: pxToPoints(computed.marginBottom),
// PptxGenJS margin array is [left, right, bottom, top] (not [top, right, bottom, left] as documented)
margin: [
pxToPoints(computed.paddingLeft),
pxToPoints(computed.paddingRight),
pxToPoints(computed.paddingBottom),
pxToPoints(computed.paddingTop),
],
};
const transparency = extractAlpha(computed.color);
if (transparency !== null) baseStyle.transparency = transparency;
if (rotation !== null) baseStyle.rotate = rotation;
const hasFormatting = el.querySelector("b, i, u, strong, em, span, br");
if (hasFormatting) {
// Text with inline formatting
const transformStr = computed.textTransform;
const runs = parseInlineFormatting(el, {}, [], (str) =>
applyTextTransform(str, transformStr)
);
// Adjust lineSpacing based on largest fontSize in runs
const adjustedStyle = { ...baseStyle };
if (adjustedStyle.lineSpacing) {
const maxFontSize = Math.max(
adjustedStyle.fontSize,
...runs.map((r) => r.options?.fontSize || 0)
);
if (maxFontSize > adjustedStyle.fontSize) {
const lineHeightMultiplier = adjustedStyle.lineSpacing / adjustedStyle.fontSize;
adjustedStyle.lineSpacing = maxFontSize * lineHeightMultiplier;
}
}
elements.push({
type: el.tagName.toLowerCase(),
text: runs,
position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) },
style: adjustedStyle,
});
} else {
// Plain text - inherit CSS formatting
const textTransform = computed.textTransform;
const transformedText = applyTextTransform(text, textTransform);
const isBold = computed.fontWeight === "bold" || parseInt(computed.fontWeight) >= 600;
elements.push({
type: el.tagName.toLowerCase(),
text: transformedText,
position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) },
style: {
...baseStyle,
bold: isBold && !shouldSkipBold(computed.fontFamily),
italic: computed.fontStyle === "italic",
underline: computed.textDecoration.includes("underline"),
},
});
}
processed.add(el);
});
return { background, elements, placeholders, errors };
});
}
async function html2pptx(htmlFile, pres, options = {}) {
const { tmpDir = process.env.TMPDIR || "/tmp", slide = null } = options;
try {
// Use Chrome on macOS, default Chromium on Unix
const launchOptions = { env: { TMPDIR: tmpDir } };
if (process.platform === "darwin") {
launchOptions.channel = "chrome";
}
const browser = await chromium.launch(launchOptions);
let bodyDimensions;
let slideData;
const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
const validationErrors = [];
try {
const page = await browser.newPage();
page.on("console", (msg) => {
// Log the message text to your test runner's console
console.log(`Browser console: ${msg.text()}`);
});
await page.goto(`file://${filePath}`);
bodyDimensions = await getBodyDimensions(page);
await page.setViewportSize({
width: Math.round(bodyDimensions.width),
height: Math.round(bodyDimensions.height),
});
slideData = await extractSlideData(page);
} finally {
await browser.close();
}
// Collect all validation errors
if (bodyDimensions.errors && bodyDimensions.errors.length > 0) {
validationErrors.push(...bodyDimensions.errors);
}
const dimensionErrors = validateDimensions(bodyDimensions, pres);
if (dimensionErrors.length > 0) {
validationErrors.push(...dimensionErrors);
}
const textBoxPositionErrors = validateTextBoxPosition(slideData, bodyDimensions);
if (textBoxPositionErrors.length > 0) {
validationErrors.push(...textBoxPositionErrors);
}
if (slideData.errors && slideData.errors.length > 0) {
validationErrors.push(...slideData.errors);
}
// Throw all errors at once if any exist
if (validationErrors.length > 0) {
const errorMessage =
validationErrors.length === 1
? validationErrors[0]
: `Multiple validation errors found:\n${validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}`;
throw new Error(errorMessage);
}
const targetSlide = slide || pres.addSlide();
await addBackground(slideData, targetSlide, tmpDir);
addElements(slideData, targetSlide, pres);
return { slide: targetSlide, placeholders: slideData.placeholders };
} catch (error) {
if (!error.message.startsWith(htmlFile)) {
throw new Error(`${htmlFile}: ${error.message}`);
}
throw error;
}
}
module.exports = html2pptx;
#!/usr/bin/env python3
"""
Rearrange PowerPoint slides based on a sequence of indices.
Usage:
python rearrange.py template.pptx output.pptx 0,34,34,50,52
This will create output.pptx using slides from template.pptx in the specified order.
Slides can be repeated (e.g., 34 appears twice).
"""
import argparse
import shutil
import sys
from copy import deepcopy
from pathlib import Path
import six
from pptx import Presentation
def main():
parser = argparse.ArgumentParser(
description="Rearrange PowerPoint slides based on a sequence of indices.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python rearrange.py template.pptx output.pptx 0,34,34,50,52
Creates output.pptx using slides 0, 34 (twice), 50, and 52 from template.pptx
python rearrange.py template.pptx output.pptx 5,3,1,2,4
Creates output.pptx with slides reordered as specified
Note: Slide indices are 0-based (first slide is 0, second is 1, etc.)
""",
)
parser.add_argument("template", help="Path to template PPTX file")
parser.add_argument("output", help="Path for output PPTX file")
parser.add_argument("sequence", help="Comma-separated sequence of slide indices (0-based)")
args = parser.parse_args()
# Parse the slide sequence
try:
slide_sequence = [int(x.strip()) for x in args.sequence.split(",")]
except ValueError:
print("Error: Invalid sequence format. Use comma-separated integers (e.g., 0,34,34,50,52)")
sys.exit(1)
# Check template exists
template_path = Path(args.template)
if not template_path.exists():
print(f"Error: Template file not found: {args.template}")
sys.exit(1)
# Create output directory if needed
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
rearrange_presentation(template_path, output_path, slide_sequence)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
except Exception as e:
print(f"Error processing presentation: {e}")
sys.exit(1)
def duplicate_slide(pres, index):
"""Duplicate a slide in the presentation."""
source = pres.slides[index]
# Use source's layout to preserve formatting
new_slide = pres.slides.add_slide(source.slide_layout)
# Collect all image and media relationships from the source slide
image_rels = {}
for rel_id, rel in six.iteritems(source.part.rels):
if "image" in rel.reltype or "media" in rel.reltype:
image_rels[rel_id] = rel
# CRITICAL: Clear placeholder shapes to avoid duplicates
for shape in new_slide.shapes:
sp = shape.element
sp.getparent().remove(sp)
# Copy all shapes from source
for shape in source.shapes:
el = shape.element
new_el = deepcopy(el)
new_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
# Handle picture shapes - need to update the blip reference
# Look for all blip elements (they can be in pic or other contexts)
# Using the element's own xpath method without namespaces argument
blips = new_el.xpath(".//a:blip[@r:embed]")
for blip in blips:
old_rId = blip.get(
"{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed"
)
if old_rId in image_rels:
# Create a new relationship in the destination slide for this image
old_rel = image_rels[old_rId]
# get_or_add returns the rId directly, or adds and returns new rId
new_rId = new_slide.part.rels.get_or_add(old_rel.reltype, old_rel._target)
# Update the blip's embed reference to use the new relationship ID
blip.set(
"{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed",
new_rId,
)
# Copy any additional image/media relationships that might be referenced elsewhere
for rel_id, rel in image_rels.items():
try:
new_slide.part.rels.get_or_add(rel.reltype, rel._target)
except Exception:
pass # Relationship might already exist
return new_slide
def delete_slide(pres, index):
"""Delete a slide from the presentation."""
rId = pres.slides._sldIdLst[index].rId
pres.part.drop_rel(rId)
del pres.slides._sldIdLst[index]
def reorder_slides(pres, slide_index, target_index):
"""Move a slide from one position to another."""
slides = pres.slides._sldIdLst
# Remove slide element from current position
slide_element = slides[slide_index]
slides.remove(slide_element)
# Insert at target position
slides.insert(target_index, slide_element)
def rearrange_presentation(template_path, output_path, slide_sequence):
"""
Create a new presentation with slides from template in specified order.
Args:
template_path: Path to template PPTX file
output_path: Path for output PPTX file
slide_sequence: List of slide indices (0-based) to include
"""
# Copy template to preserve dimensions and theme
if template_path != output_path:
shutil.copy2(template_path, output_path)
prs = Presentation(output_path)
else:
prs = Presentation(template_path)
total_slides = len(prs.slides)
# Validate indices
for idx in slide_sequence:
if idx < 0 or idx >= total_slides:
raise ValueError(f"Slide index {idx} out of range (0-{total_slides - 1})")
# Track original slides and their duplicates
slide_map = [] # List of actual slide indices for final presentation
duplicated = {} # Track duplicates: original_idx -> [duplicate_indices]
# Step 1: DUPLICATE repeated slides
print(f"Processing {len(slide_sequence)} slides from template...")
for i, template_idx in enumerate(slide_sequence):
if duplicated.get(template_idx):
# Already duplicated this slide, use the duplicate
slide_map.append(duplicated[template_idx].pop(0))
print(f" [{i}] Using duplicate of slide {template_idx}")
elif slide_sequence.count(template_idx) > 1 and template_idx not in duplicated:
# First occurrence of a repeated slide - create duplicates
slide_map.append(template_idx)
duplicates = []
count = slide_sequence.count(template_idx) - 1
print(f" [{i}] Using original slide {template_idx}, creating {count} duplicate(s)")
for _ in range(count):
duplicate_slide(prs, template_idx)
duplicates.append(len(prs.slides) - 1)
duplicated[template_idx] = duplicates
else:
# Unique slide or first occurrence already handled, use original
slide_map.append(template_idx)
print(f" [{i}] Using original slide {template_idx}")
# Step 2: DELETE unwanted slides (work backwards)
slides_to_keep = set(slide_map)
print(f"\nDeleting {len(prs.slides) - len(slides_to_keep)} unused slides...")
for i in range(len(prs.slides) - 1, -1, -1):
if i not in slides_to_keep:
delete_slide(prs, i)
# Update slide_map indices after deletion
slide_map = [idx - 1 if idx > i else idx for idx in slide_map]
# Step 3: REORDER to final sequence
print(f"Reordering {len(slide_map)} slides to final sequence...")
for target_pos in range(len(slide_map)):
# Find which slide should be at target_pos
current_pos = slide_map[target_pos]
if current_pos != target_pos:
reorder_slides(prs, current_pos, target_pos)
# Update slide_map: the move shifts other slides
for i in range(len(slide_map)):
if slide_map[i] > current_pos and slide_map[i] <= target_pos:
slide_map[i] -= 1
elif slide_map[i] < current_pos and slide_map[i] >= target_pos:
slide_map[i] += 1
slide_map[target_pos] = target_pos
# Save the presentation
prs.save(output_path)
print(f"\nSaved rearranged presentation to: {output_path}")
print(f"Final presentation has {len(prs.slides)} slides")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Apply text replacements to PowerPoint presentation.
Usage:
python replace.py <input.pptx> <replacements.json> <output.pptx>
The replacements JSON should have the structure output by inventory.py.
ALL text shapes identified by inventory.py will have their text cleared
unless "paragraphs" is specified in the replacements for that shape.
"""
import json
import sys
from pathlib import Path
from typing import Any
from inventory import InventoryData, extract_text_inventory
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.dml import MSO_THEME_COLOR
from pptx.enum.text import PP_ALIGN
from pptx.oxml.xmlchemy import OxmlElement
from pptx.util import Pt
def clear_paragraph_bullets(paragraph):
"""Clear bullet formatting from a paragraph."""
pPr = paragraph._element.get_or_add_pPr()
# Remove existing bullet elements
for child in list(pPr):
if (
child.tag.endswith("buChar")
or child.tag.endswith("buNone")
or child.tag.endswith("buAutoNum")
or child.tag.endswith("buFont")
):
pPr.remove(child)
return pPr
def apply_paragraph_properties(paragraph, para_data: dict[str, Any]):
"""Apply formatting properties to a paragraph."""
# Get the text but don't set it on paragraph directly yet
text = para_data.get("text", "")
# Get or create paragraph properties
pPr = clear_paragraph_bullets(paragraph)
# Handle bullet formatting
if para_data.get("bullet", False):
level = para_data.get("level", 0)
paragraph.level = level
# Calculate font-proportional indentation
font_size = para_data.get("font_size", 18.0)
level_indent_emu = int((font_size * (1.6 + level * 1.6)) * 12700)
hanging_indent_emu = int(-font_size * 0.8 * 12700)
# Set indentation
pPr.attrib["marL"] = str(level_indent_emu)
pPr.attrib["indent"] = str(hanging_indent_emu)
# Add bullet character
buChar = OxmlElement("a:buChar")
buChar.set("char", "•")
pPr.append(buChar)
# Default to left alignment for bullets if not specified
if "alignment" not in para_data:
paragraph.alignment = PP_ALIGN.LEFT
else:
# Remove indentation for non-bullet text
pPr.attrib["marL"] = "0"
pPr.attrib["indent"] = "0"
# Add buNone element
buNone = OxmlElement("a:buNone")
pPr.insert(0, buNone)
# Apply alignment
if "alignment" in para_data:
alignment_map = {
"LEFT": PP_ALIGN.LEFT,
"CENTER": PP_ALIGN.CENTER,
"RIGHT": PP_ALIGN.RIGHT,
"JUSTIFY": PP_ALIGN.JUSTIFY,
}
if para_data["alignment"] in alignment_map:
paragraph.alignment = alignment_map[para_data["alignment"]]
# Apply spacing
if "space_before" in para_data:
paragraph.space_before = Pt(para_data["space_before"])
if "space_after" in para_data:
paragraph.space_after = Pt(para_data["space_after"])
if "line_spacing" in para_data:
paragraph.line_spacing = Pt(para_data["line_spacing"])
# Apply run-level formatting
if not paragraph.runs:
run = paragraph.add_run()
run.text = text
else:
run = paragraph.runs[0]
run.text = text
# Apply font properties
apply_font_properties(run, para_data)
def apply_font_properties(run, para_data: dict[str, Any]):
"""Apply font properties to a text run."""
if "bold" in para_data:
run.font.bold = para_data["bold"]
if "italic" in para_data:
run.font.italic = para_data["italic"]
if "underline" in para_data:
run.font.underline = para_data["underline"]
if "font_size" in para_data:
run.font.size = Pt(para_data["font_size"])
if "font_name" in para_data:
run.font.name = para_data["font_name"]
# Apply color - prefer RGB, fall back to theme_color
if "color" in para_data:
color_hex = para_data["color"].lstrip("#")
if len(color_hex) == 6:
r = int(color_hex[0:2], 16)
g = int(color_hex[2:4], 16)
b = int(color_hex[4:6], 16)
run.font.color.rgb = RGBColor(r, g, b)
elif "theme_color" in para_data:
# Get theme color by name (e.g., "DARK_1", "ACCENT_1")
theme_name = para_data["theme_color"]
try:
run.font.color.theme_color = getattr(MSO_THEME_COLOR, theme_name)
except AttributeError:
print(f" WARNING: Unknown theme color name '{theme_name}'")
def detect_frame_overflow(inventory: InventoryData) -> dict[str, dict[str, float]]:
"""Detect text overflow in shapes (text exceeding shape bounds).
Returns dict of slide_key -> shape_key -> overflow_inches.
Only includes shapes that have text overflow.
"""
overflow_map = {}
for slide_key, shapes_dict in inventory.items():
for shape_key, shape_data in shapes_dict.items():
# Check for frame overflow (text exceeding shape bounds)
if shape_data.frame_overflow_bottom is not None:
if slide_key not in overflow_map:
overflow_map[slide_key] = {}
overflow_map[slide_key][shape_key] = shape_data.frame_overflow_bottom
return overflow_map
def validate_replacements(inventory: InventoryData, replacements: dict) -> list[str]:
"""Validate that all shapes in replacements exist in inventory.
Returns list of error messages.
"""
errors = []
for slide_key, shapes_data in replacements.items():
if not slide_key.startswith("slide-"):
continue
# Check if slide exists
if slide_key not in inventory:
errors.append(f"Slide '{slide_key}' not found in inventory")
continue
# Check each shape
for shape_key in shapes_data.keys():
if shape_key not in inventory[slide_key]:
# Find shapes without replacements defined and show their content
unused_with_content = []
for k in inventory[slide_key].keys():
if k not in shapes_data:
shape_data = inventory[slide_key][k]
# Get text from paragraphs as preview
paragraphs = shape_data.paragraphs
if paragraphs and paragraphs[0].text:
first_text = paragraphs[0].text[:50]
if len(paragraphs[0].text) > 50:
first_text += "..."
unused_with_content.append(f"{k} ('{first_text}')")
else:
unused_with_content.append(k)
errors.append(
f"Shape '{shape_key}' not found on '{slide_key}'. "
f"Shapes without replacements: {', '.join(sorted(unused_with_content)) if unused_with_content else 'none'}"
)
return errors
def check_duplicate_keys(pairs):
"""Check for duplicate keys when loading JSON."""
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"Duplicate key found in JSON: '{key}'")
result[key] = value
return result
def apply_replacements(pptx_file: str, json_file: str, output_file: str):
"""Apply text replacements from JSON to PowerPoint presentation."""
# Load presentation
prs = Presentation(pptx_file)
# Get inventory of all text shapes (returns ShapeData objects)
# Pass prs to use same Presentation instance
inventory = extract_text_inventory(Path(pptx_file), prs)
# Detect text overflow in original presentation
original_overflow = detect_frame_overflow(inventory)
# Load replacement data with duplicate key detection
with open(json_file) as f:
replacements = json.load(f, object_pairs_hook=check_duplicate_keys)
# Validate replacements
errors = validate_replacements(inventory, replacements)
if errors:
print("ERROR: Invalid shapes in replacement JSON:")
for error in errors:
print(f" - {error}")
print("\nPlease check the inventory and update your replacement JSON.")
print(
"You can regenerate the inventory with: python inventory.py <input.pptx> <output.json>"
)
raise ValueError(f"Found {len(errors)} validation error(s)")
# Track statistics
shapes_processed = 0
shapes_cleared = 0
shapes_replaced = 0
# Process each slide from inventory
for slide_key, shapes_dict in inventory.items():
if not slide_key.startswith("slide-"):
continue
slide_index = int(slide_key.split("-")[1])
if slide_index >= len(prs.slides):
print(f"Warning: Slide {slide_index} not found")
continue
# Process each shape from inventory
for shape_key, shape_data in shapes_dict.items():
shapes_processed += 1
# Get the shape directly from ShapeData
shape = shape_data.shape
if not shape:
print(f"Warning: {shape_key} has no shape reference")
continue
# ShapeData already validates text_frame in __init__
text_frame = shape.text_frame # type: ignore
text_frame.clear() # type: ignore
shapes_cleared += 1
# Check for replacement paragraphs
replacement_shape_data = replacements.get(slide_key, {}).get(shape_key, {})
if "paragraphs" not in replacement_shape_data:
continue
shapes_replaced += 1
# Add replacement paragraphs
for i, para_data in enumerate(replacement_shape_data["paragraphs"]):
if i == 0:
p = text_frame.paragraphs[0] # type: ignore
else:
p = text_frame.add_paragraph() # type: ignore
apply_paragraph_properties(p, para_data)
# Check for issues after replacements
# Save to a temporary file and reload to avoid modifying the presentation during inventory
# (extract_text_inventory accesses font.color which adds empty <a:solidFill/> elements)
import tempfile
with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as tmp:
tmp_path = Path(tmp.name)
prs.save(str(tmp_path))
try:
updated_inventory = extract_text_inventory(tmp_path)
updated_overflow = detect_frame_overflow(updated_inventory)
finally:
tmp_path.unlink() # Clean up temp file
# Check if any text overflow got worse
overflow_errors = []
for slide_key, shape_overflows in updated_overflow.items():
for shape_key, new_overflow in shape_overflows.items():
# Get original overflow (0 if there was no overflow before)
original = original_overflow.get(slide_key, {}).get(shape_key, 0.0)
# Error if overflow increased
if new_overflow > original + 0.01: # Small tolerance for rounding
increase = new_overflow - original
overflow_errors.append(
f'{slide_key}/{shape_key}: overflow worsened by {increase:.2f}" '
f'(was {original:.2f}", now {new_overflow:.2f}")'
)
# Collect warnings from updated shapes
warnings = []
for slide_key, shapes_dict in updated_inventory.items():
for shape_key, shape_data in shapes_dict.items():
if shape_data.warnings:
for warning in shape_data.warnings:
warnings.append(f"{slide_key}/{shape_key}: {warning}")
# Fail if there are any issues
if overflow_errors or warnings:
print("\nERROR: Issues detected in replacement output:")
if overflow_errors:
print("\nText overflow worsened:")
for error in overflow_errors:
print(f" - {error}")
if warnings:
print("\nFormatting warnings:")
for warning in warnings:
print(f" - {warning}")
print("\nPlease fix these issues before saving.")
raise ValueError(
f"Found {len(overflow_errors)} overflow error(s) and {len(warnings)} warning(s)"
)
# Save the presentation
prs.save(output_file)
# Report results
print(f"Saved updated presentation to: {output_file}")
print(f"Processed {len(prs.slides)} slides")
print(f" - Shapes processed: {shapes_processed}")
print(f" - Shapes cleared: {shapes_cleared}")
print(f" - Shapes replaced: {shapes_replaced}")
def main():
"""Main entry point for command-line usage."""
if len(sys.argv) != 4:
print(__doc__)
sys.exit(1)
input_pptx = Path(sys.argv[1])
replacements_json = Path(sys.argv[2])
output_pptx = Path(sys.argv[3])
if not input_pptx.exists():
print(f"Error: Input file '{input_pptx}' not found")
sys.exit(1)
if not replacements_json.exists():
print(f"Error: Replacements JSON file '{replacements_json}' not found")
sys.exit(1)
try:
apply_replacements(str(input_pptx), str(replacements_json), str(output_pptx))
except Exception as e:
print(f"Error applying replacements: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
Related skills
How it compares
Pick pptx over markdown-to-slides tools when the required deliverable is a native PowerPoint file with layouts and speaker notes.
FAQ
What does the pptx skill output?
The pptx skill outputs a PowerPoint .pptx file with slides, layouts, and speaker notes. It is aimed at pitches, status reports, and other stakeholder presentations rather than code or API artifacts.
Can pptx update an existing PowerPoint file?
Yes. The pptx skill can build new decks or update existing .pptx files by revising slide content, layouts, and speaker notes when presentation structure or messaging changes.