diff --git a/README.md b/README.md index c455f942..0771d934 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ This skill uses **progressive disclosure** — the main `SKILL.md` is a concise | `scripts/extract-pptx.py` | PPT content extraction | Phase 4 (conversion) | | `scripts/deploy.sh` | Deploy to Vercel | Phase 6 (sharing) | | `scripts/export-pdf.sh` | Export slides to PDF | Phase 6 (sharing) | +| `scripts/export-pptx.sh` | Export to editable PPTX | Phase 6 (sharing) | This design follows [OpenAI's harness engineering](https://openai.com/index/harness-engineering/) principle: "give the agent a map, not a 1,000-page instruction manual." @@ -164,12 +165,25 @@ bash scripts/export-pdf.sh ./presentation.html ./output.pdf Uses [Playwright](https://playwright.dev) to screenshot each slide at 1920×1080 and combine into a PDF. Installs automatically if needed. Animations are not preserved (it's a static snapshot). +### Export to editable PPTX + +Convert your slides to a native, editable PowerPoint file — for venues that require `.pptx`, for collaborators who want to edit text directly, or for repurposing slides in other decks: + +```bash +bash scripts/export-pptx.sh ./my-deck/index.html +bash scripts/export-pptx.sh ./presentation.html ./output.pptx +``` + +Uses [Playwright](https://playwright.dev) + [dom-to-pptx](https://github.com/atharva9167/dom-to-pptx) to map each `.slide`'s computed styles to native PowerPoint shapes: text frames, vector gradients, real shadows, embedded images. Animations are not preserved. + +Unlike PDF export this is **not a screenshot** — every text run remains directly editable in PowerPoint, Keynote, Google Slides, and WPS. + ## Requirements - [Claude Code](https://claude.ai/claude-code) CLI - For PPT conversion: Python with `python-pptx` library - For URL deployment: Node.js + Vercel account (free) -- For PDF export: Node.js (Playwright installs automatically) +- For PDF and PPTX export: Node.js (Playwright + dom-to-pptx install automatically) ## Credits diff --git a/SKILL.md b/SKILL.md index dfd8ce0b..90a072ec 100644 --- a/SKILL.md +++ b/SKILL.md @@ -218,13 +218,14 @@ When converting PowerPoint files: ## Phase 6: Share & Export (Optional) -After delivery, **ask the user:** _"Would you like to share this presentation? I can deploy it to a live URL (works on any device including phones) or export it as a PDF."_ +After delivery, **ask the user:** _"Would you like to share this presentation? I can deploy it to a live URL (works on any device including phones), export it as a PDF, or export it as an editable PowerPoint file."_ Options: - **Deploy to URL** — Shareable link that works on any device - **Export to PDF** — Universal file for email, Slack, print -- **Both** +- **Export to PPTX** — Editable native PowerPoint file (text, gradients, images all editable) +- **Multiple** - **No thanks** If the user declines, stop here. If they choose one or both, proceed below. @@ -307,6 +308,51 @@ This captures each slide as a screenshot and combines them into a PDF. Perfect f ``` This renders at 1280×720 instead of 1920×1080, typically cutting file size by 50-70% with minimal visual difference. +### 6C: Export to PPTX (Editable PowerPoint) + +This produces a **native, editable** `.pptx` file — every text run is a real PowerPoint text frame; CSS gradients become vector SVGs; CSS box-shadows become real PowerPoint shadows; images embed as picture shapes. Useful when: + +- The user has to upload to a conference / venue laptop that only supports PowerPoint +- The user wants to tweak text right before presenting without going back to HTML +- The user wants to repurpose individual slides in a different deck + +PDF and PPTX answer different needs — PDF is a frozen snapshot anyone can read, PPTX stays editable. Offer both when relevant. + +1. **Run the export script:** + + ```bash + bash scripts/export-pptx.sh [output.pptx] + ``` + + If no output path is given, the PPTX is saved next to the HTML file. + +2. **What happens behind the scenes** (explain briefly to the user): + - A headless browser opens the presentation at 1920×1080 (16:9 widescreen, which maps cleanly to PowerPoint's 13.33"×7.5" canvas) + - The script injects [dom-to-pptx](https://github.com/atharva9167/dom-to-pptx) into the page + - For every `.slide` element, it reads the **computed style** of each descendant — bounding rect, color, font, gradient, shadow — and maps it to a native PowerPoint shape + - The result is a fully editable `.pptx` (open in PowerPoint, Keynote, Google Slides, WPS) + - Animations are not preserved (PPTX export is a static snapshot of the final visual state) + +3. **If the install step fails:** + - First run downloads Playwright + Chromium + dom-to-pptx (~150 MB). Allow 30–60 seconds. + - On second-run failures, try: `npm install -g dom-to-pptx playwright && npx playwright install chromium` + +4. **Deliver the PPTX** — The script auto-opens it. Tell the user: + - The file location and size + - That text is **directly editable** in PowerPoint/Keynote (click any text and type) + - That this is a one-way export — if they edit HTML again, they need to re-export + - The PPTX can be uploaded to Google Slides, projector laptops, etc. + +**⚠ PPTX export gotchas:** + +- **PPTX is a layout-from-computed-styles export, not a screenshot.** This is its main advantage (editable!) but also its biggest gotcha: dom-to-pptx maps each DOM element to a PowerPoint shape using `getBoundingClientRect()`. Decks that present cleanly to humans but rely on deeply nested flexbox / grid centering with `100vh` height inheritance may produce slides where only some content survives. **For maximum fidelity, structure each `.slide` as a fixed 1920×1080 absolute-positioned canvas.** The skill's default `viewport-base.css` works for most cases; if a slide comes out missing shapes, switch its children from flex/grid to `position: absolute` with explicit px coordinates. +- **First run is slow.** The script installs Playwright, dom-to-pptx, and downloads a Chromium browser (~150MB) into a temp directory. This happens once per run. Warn the user it may take 30-60 seconds the first time — subsequent exports within the same session are faster. +- **Slides must use `class="slide"`.** Same as PDF export — the script finds slides by `.slide`. All decks generated by this skill use it. +- **Animations / `.reveal` opacity tricks are auto-revealed before export.** The script force-shows every `.reveal` element so off-screen content is included. If the user uses a custom class for hidden-until-revealed, document that or open an issue. +- **CSS background-image gradients work; raster background-images may not.** Gradients are parsed and rendered as vector SVG. `background-image: url(...)` rasters are best avoided — use `` tags so dom-to-pptx can place them as picture shapes. +- **Fonts:** the script tries to embed used fonts so the PPTX renders identically off the original machine. If a Google Fonts URL is CORS-blocked, PowerPoint falls back to Arial; add `crossorigin="anonymous"` to the `` tag if this happens. +- **Large decks can produce 10MB+ PPTX files.** Each embedded image is stored at full resolution. If size matters, downscale images before referencing them. + --- ## Supporting Files @@ -320,3 +366,4 @@ This captures each slide as a screenshot and combines them into a PDF. Perfect f | [scripts/extract-pptx.py](scripts/extract-pptx.py) | Python script for PPT content extraction | Phase 4 (conversion) | | [scripts/deploy.sh](scripts/deploy.sh) | Deploy slides to Vercel for instant sharing | Phase 6 (sharing) | | [scripts/export-pdf.sh](scripts/export-pdf.sh) | Export slides to PDF | Phase 6 (sharing) | +| [scripts/export-pptx.sh](scripts/export-pptx.sh) | Export slides to editable PPTX (native shapes, not screenshots) | Phase 6 (sharing) | diff --git a/plugins/frontend-slides/skills/frontend-slides/scripts/export-pptx.sh b/plugins/frontend-slides/skills/frontend-slides/scripts/export-pptx.sh new file mode 120000 index 00000000..8612b5df --- /dev/null +++ b/plugins/frontend-slides/skills/frontend-slides/scripts/export-pptx.sh @@ -0,0 +1 @@ +../../../../../scripts/export-pptx.sh \ No newline at end of file diff --git a/scripts/export-pptx.sh b/scripts/export-pptx.sh new file mode 100755 index 00000000..441406fe --- /dev/null +++ b/scripts/export-pptx.sh @@ -0,0 +1,361 @@ +#!/usr/bin/env bash +# export-pptx.sh — Export an HTML presentation to a native, editable PPTX +# +# Usage: +# bash scripts/export-pptx.sh [output.pptx] +# +# Examples: +# bash scripts/export-pptx.sh ./my-deck/index.html +# bash scripts/export-pptx.sh ./presentation.html ./presentation.pptx +# +# What this does: +# 1. Starts a local server to serve the HTML (fonts/assets need HTTP) +# 2. Launches headless Chromium via Playwright at 1920x1080 +# 3. Injects dom-to-pptx (https://github.com/atharva9167/dom-to-pptx) into the page +# 4. Walks every .slide element, mapping its computed styles to native +# PowerPoint shapes (text boxes, gradients, shadows, images) +# 5. Writes a single .pptx file with editable text and vector primitives +# +# Unlike PDF export, the output is fully editable in PowerPoint, Keynote, or WPS: +# every text run is a native text frame; gradients become vector SVGs; +# images are embedded as picture shapes. Animations are not preserved. +set -euo pipefail + +# ─── Colors ──────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +NC='\033[0m' + +info() { echo -e "${CYAN}ℹ${NC} $*"; } +ok() { echo -e "${GREEN}✓${NC} $*"; } +warn() { echo -e "${YELLOW}⚠${NC} $*"; } +err() { echo -e "${RED}✗${NC} $*" >&2; } + +# ─── Parse flags ────────────────────────────────────────── + +# Default canvas: 1920x1080 (matches the rest of the skill's design canvas). +# PowerPoint slide dims are reported in inches; 13.333" x 7.5" is the standard +# 16:9 widescreen layout that maps 1:1 to a 1920x1080 web design. +VIEWPORT_W=1920 +VIEWPORT_H=1080 +SLIDE_W_IN=13.333 +SLIDE_H_IN=7.5 + +POSITIONAL=() +for arg in "$@"; do + case $arg in + *) + POSITIONAL+=("$arg") + ;; + esac +done +set -- "${POSITIONAL[@]}" + +# ─── Input validation ───────────────────────────────────── + +if [[ $# -lt 1 ]]; then + err "Usage: bash scripts/export-pptx.sh [output.pptx]" + err "" + err "Examples:" + err " bash scripts/export-pptx.sh ./my-deck/index.html" + err " bash scripts/export-pptx.sh ./presentation.html ./slides.pptx" + exit 1 +fi + +INPUT_HTML="$1" +if [[ ! -f "$INPUT_HTML" ]]; then + err "File not found: $INPUT_HTML" + exit 1 +fi + +# Resolve to absolute path +INPUT_HTML=$(cd "$(dirname "$INPUT_HTML")" && pwd)/$(basename "$INPUT_HTML") + +# Output PPTX path: use second argument or derive from input name +if [[ $# -ge 2 ]]; then + OUTPUT_PPTX="$2" +else + OUTPUT_PPTX="$(dirname "$INPUT_HTML")/$(basename "$INPUT_HTML" .html).pptx" +fi + +# Resolve output to absolute path so a later `cd` into a temp dir doesn't +# silently relocate the output file. +OUTPUT_DIR=$(dirname "$OUTPUT_PPTX") +mkdir -p "$OUTPUT_DIR" +OUTPUT_DIR=$(cd "$OUTPUT_DIR" && pwd) +OUTPUT_PPTX="$OUTPUT_DIR/$(basename "$OUTPUT_PPTX")" + +echo "" +echo -e "${BOLD}╔══════════════════════════════════════╗${NC}" +echo -e "${BOLD}║ Export Slides to PPTX ║${NC}" +echo -e "${BOLD}╚══════════════════════════════════════╝${NC}" +echo "" + +# ─── Step 1: Check dependencies ─────────────────────────── + +info "Checking dependencies..." + +if ! command -v npx &>/dev/null; then + err "Node.js is required but not installed." + err "" + err "Install Node.js:" + err " macOS: brew install node" + err " or visit https://nodejs.org and download the installer" + exit 1 +fi + +ok "Node.js found" + +# ─── Step 2: Create the export script ───────────────────── + +# We use a temporary Node.js script that: +# 1. Starts a local HTTP server (so fonts/assets resolve correctly) +# 2. Opens the deck in headless Chromium at 1920x1080 +# 3. Loads dom-to-pptx into the page +# 4. Hands every .slide element to dom-to-pptx in one call +# 5. Pipes the resulting Blob out as base64, which bash writes to disk + +TEMP_DIR=$(mktemp -d) +TEMP_SCRIPT="$TEMP_DIR/export-slides.mjs" + +# Figure out which directory to serve (the folder containing the HTML) +SERVE_DIR=$(dirname "$INPUT_HTML") +HTML_FILENAME=$(basename "$INPUT_HTML") + +cat > "$TEMP_SCRIPT" << 'EXPORT_SCRIPT' +// export-slides.mjs — Playwright driver that turns HTML slides into a +// native, editable PPTX file using the dom-to-pptx style engine. +// +// How it works: +// 1. Starts a tiny local HTTP server (fonts + relative assets need HTTP) +// 2. Opens the presentation in headless Chromium at 1920x1080 +// 3. Loads dom-to-pptx (already npm-installed alongside this script) +// 4. Scrolls each slide into view so its layout is committed by the +// browser, then invokes exportToPptx(slides, { skipDownload: true }) +// 5. Receives the PPTX Blob as base64 from the page and writes it to disk + +import { chromium } from 'playwright'; +import { createServer } from 'http'; +import { readFileSync, writeFileSync } from 'fs'; +import { join, extname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const SERVE_DIR = process.argv[2]; +const HTML_FILE = process.argv[3]; +const OUTPUT_PPTX = process.argv[4]; +const VP_WIDTH = parseInt(process.argv[5]) || 1920; +const VP_HEIGHT = parseInt(process.argv[6]) || 1080; +const SLIDE_W_IN = parseFloat(process.argv[7]) || 13.333; +const SLIDE_H_IN = parseFloat(process.argv[8]) || 7.5; + +// Resolve the dom-to-pptx browser bundle that was installed in this temp dir. +const BUNDLE_PATH = resolve( + fileURLToPath(import.meta.url), + '..', + 'node_modules', + 'dom-to-pptx', + 'dist', + 'dom-to-pptx.bundle.js' +); + +// ─── Simple static file server ──────────────────────────── +// (We need HTTP so that Google Fonts and relative assets load correctly.) + +const MIME_TYPES = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.eot': 'application/vnd.ms-fontobject', +}; + +const server = createServer((req, res) => { + // Decode URL-encoded characters (e.g., %20 → space) so filenames with + // spaces resolve correctly. + const decodedUrl = decodeURIComponent(req.url); + const filePath = join(SERVE_DIR, decodedUrl === '/' ? HTML_FILE : decodedUrl); + try { + const content = readFileSync(filePath); + const ext = extname(filePath).toLowerCase(); + res.writeHead(200, { + 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream', + // dom-to-pptx fetches font files via fetch(); allow it. + 'Access-Control-Allow-Origin': '*', + }); + res.end(content); + } catch { + res.writeHead(404); + res.end('Not found'); + } +}); + +const port = await new Promise((r) => { + server.listen(0, () => r(server.address().port)); +}); + +console.log(` Local server on port ${port}`); + +// ─── Load the deck ──────────────────────────────────────── + +const browser = await chromium.launch(); +const ctx = await browser.newContext({ + viewport: { width: VP_WIDTH, height: VP_HEIGHT }, +}); +const page = await ctx.newPage(); + +// Surface page errors so the user can see what went wrong on the browser side. +page.on('pageerror', e => console.error(' [page error]', e.message)); + +await page.goto(`http://localhost:${port}/`, { waitUntil: 'networkidle' }); + +// Wait for fonts to load (dom-to-pptx embeds them by URL — they must +// already be resolvable in the document). +await page.evaluate(() => document.fonts && document.fonts.ready); +await page.waitForTimeout(800); + +// Inject the dom-to-pptx browser bundle directly. This avoids a CORS +// roundtrip to jsdelivr and pins the version we just installed. +const bundleSrc = readFileSync(BUNDLE_PATH, 'utf8'); +await page.addScriptTag({ content: bundleSrc }); + +const slideCount = await page.$$eval('.slide', els => els.length); +if (slideCount === 0) { + console.error(' ERROR: No .slide elements found in the presentation.'); + console.error(' Make sure your HTML uses
or
.'); + await browser.close(); + server.close(); + process.exit(1); +} +console.log(` Found ${slideCount} slides`); + +// ─── Reveal everything that was animation-gated ─────────── +// frontend-slides decks typically rely on IntersectionObserver to add +// .visible / .active classes and remove opacity:0. For an off-screen +// snapshot dom-to-pptx needs the final visual state to be present in +// computed styles, so we force-reveal every .reveal element. +await page.evaluate(() => { + document.querySelectorAll('.slide').forEach(s => { + s.classList.add('visible', 'active'); + s.style.opacity = '1'; + s.style.visibility = 'visible'; + }); + document.querySelectorAll('.reveal').forEach(el => { + el.style.opacity = '1'; + el.style.transform = 'none'; + el.style.visibility = 'visible'; + }); +}); +await page.waitForTimeout(300); + +// ─── Run dom-to-pptx and pull the blob out ──────────────── + +console.log(' Mapping DOM to PowerPoint shapes...'); + +const b64 = await page.evaluate(async ({ widthIn, heightIn }) => { + const slides = Array.from(document.querySelectorAll('.slide')); + const blob = await window.domToPptx.exportToPptx(slides, { + fileName: 'export.pptx', + width: widthIn, + height: heightIn, + svgAsVector: true, + skipDownload: true, + }); + const ab = await blob.arrayBuffer(); + // Chunked base64 to avoid call-stack blowups on large decks. + const bytes = new Uint8Array(ab); + let bin = ''; + const CHUNK = 0x8000; + for (let i = 0; i < bytes.length; i += CHUNK) { + bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK)); + } + return btoa(bin); +}, { widthIn: SLIDE_W_IN, heightIn: SLIDE_H_IN }); + +await browser.close(); +server.close(); + +writeFileSync(OUTPUT_PPTX, Buffer.from(b64, 'base64')); + +console.log(` ✓ PPTX saved to: ${OUTPUT_PPTX}`); +EXPORT_SCRIPT + +# ─── Step 3: Install Playwright + dom-to-pptx in temp dir ─ + +info "Setting up Playwright + dom-to-pptx (first run downloads ~150MB)..." +echo "" + +cd "$TEMP_DIR" + +# Minimal package.json so npm install works. +cat > "$TEMP_DIR/package.json" << 'PKG' +{ "name": "slide-export-pptx", "private": true, "type": "module" } +PKG + +npm install playwright dom-to-pptx &>/dev/null || { + err "Failed to install Playwright and dom-to-pptx." + err "Try running: npm install playwright dom-to-pptx" + rm -rf "$TEMP_DIR" + exit 1 +} + +# Ensure Chromium is downloaded for Playwright. +npx playwright install chromium 2>/dev/null || { + err "Failed to install Chromium browser for Playwright." + err "Try running manually: npx playwright install chromium" + rm -rf "$TEMP_DIR" + exit 1 +} +ok "Toolchain ready" +echo "" + +# ─── Step 4: Run the export ─────────────────────────────── + +info "Exporting slides to PPTX..." +echo "" + +node "$TEMP_SCRIPT" \ + "$SERVE_DIR" "$HTML_FILENAME" "$OUTPUT_PPTX" \ + "$VIEWPORT_W" "$VIEWPORT_H" \ + "$SLIDE_W_IN" "$SLIDE_H_IN" || { + err "PPTX export failed." + rm -rf "$TEMP_DIR" + exit 1 +} + +# ─── Step 5: Cleanup and success ────────────────────────── + +rm -rf "$TEMP_DIR" + +echo "" +echo -e "${BOLD}════════════════════════════════════════${NC}" +ok "PPTX exported successfully!" +echo "" +echo -e " ${BOLD}File:${NC} $OUTPUT_PPTX" +echo "" +FILE_SIZE=$(du -h "$OUTPUT_PPTX" | cut -f1 | xargs) +echo " Size: $FILE_SIZE" +echo "" +echo " Editable in PowerPoint, Keynote, and WPS — every text run is a" +echo " native text frame; gradients are vector SVGs; images are embedded" +echo " as picture shapes. Animations are not preserved." +echo -e "${BOLD}════════════════════════════════════════${NC}" +echo "" + +# Open the PPTX automatically when possible. +if command -v open &>/dev/null; then + open "$OUTPUT_PPTX" +elif command -v xdg-open &>/dev/null; then + xdg-open "$OUTPUT_PPTX" +fi