---
slug: "html-to-pptx"
source_type: "skill_md"
source_url: "https://cdn.jsdelivr.net/gh/Emily27-alt/html-to-pptx@main/SKILL.md"
repo: "https://github.com/Emily27-alt/html-to-pptx"
source_file: "SKILL.md"
branch: "main"
---
---
name: html-to-pptx
description: Convert an existing HTML slide deck into an editable .pptx that fully preserves layout, typography, colors, assets, and visual style. Use when the user gives you an HTML file containing <section> slides (or CSS-styled slide markup) and asks for a PowerPoint version that can still be edited — not a deck of screenshots. The output uses native text boxes, shapes, and images laid over pre-baked gradient backgrounds. Handles the CSS↔PPTX unit conversions (px→inches, CSS letter-spacing→charSpacing), gradient-fill limitations (bakes to PNGs), photo-to-panel transitions, Chinese typography, and the Windows/macOS PPTX→PDF→image verification loop.
---

# HTML → PPTX (editable)

Convert an HTML slide deck to an **editable** `.pptx` while preserving as much of the original design as possible. Output must be editable native PowerPoint content — text boxes, shapes, images — not rasterized slide images.

This skill is for the case where the user already has a finished HTML deck (typically one `<section class="slide">` per slide at a fixed canvas size like 1920×1080) and wants a PowerPoint they can hand-edit.

---

## When to reach for this vs. other approaches

| Situation | Use |
|---|---|
| User has HTML and wants editable .pptx | **This skill** |
| User wants a .pptx from scratch / plain content | `pptx` skill (pptxgenjs / python-pptx) |
| User explicitly wants pixel-perfect screenshots | Headless-browser → image-per-slide; then embed as slide backgrounds |
| User wants to edit an existing .pptx | `pptx` skill editing workflow (unpack / pack XML) |

If the user says "完全还原" / "pixel-perfect" / "must look identical", clarify first: editable native shapes *approximate* — screenshots are exact but uneditable. They usually want editable.

---

## Tooling

Node / npm (global):

```bash
npm install -g pptxgenjs sharp
```

Python:

```bash
pip install pillow pymupdf markitdown[pptx]
```

If pptxgenjs is installed globally but Node can't find it in the project directory, run with `NODE_PATH` set to the global modules path:

```bash
NODE_PATH="$(npm root -g)" node build/build.js
```

For HTML → PPTX → PDF on **Windows** use the PowerPoint COM script in `templates/to_pdf.template.ps1`. On **macOS/Linux** use LibreOffice: `soffice --headless --convert-to pdf deck.pptx`.

For PDF → images prefer PyMuPDF (`fitz`) on Windows — `pdftoppm` can fail silently on non-ASCII or spaced paths.

---

## The workflow

1. **Read the HTML.** Enumerate distinct slide archetypes (cover, chapter, person, grid, content, close, etc.). Extract design tokens: colors from `:root` CSS vars, font families, the canvas size (usually `1920×1080`).

2. **Generate gradient backgrounds.** pptxgenjs has **no gradient fill**. Use Pillow to bake one PNG per distinct background variant (see `templates/gen_bg.template.py`). Put scatter/stars, dune silhouettes, and vignettes into the same PNG — anything too decorative to be worth recreating as native shapes.

3. **Generate photo-transition overlays** if the deck has photos that blend into content panels (see `templates/gen_fade.template.py`). Always prefer a single gradient PNG over stacked translucent rectangles — stacks show visible bands.

4. **Write `build.js`** with `pptxgenjs`. Structure:
   - Custom slide layout sized to match the HTML canvas exactly (e.g. `width: 13.333, height: 7.5` for 1920×1080).
   - A `C` color map and `F` font map from the CSS tokens.
   - Unit helpers (`px`, `pt`, `ls`) — see [reference.md](https://github.com/Emily27-alt/html-to-pptx/blob/HEAD/reference.md).
   - Shared chrome helpers: `brandLogo`, `footer`, `cornerRow`, etc. — one per recurring element.
   - One function per slide archetype. Call them in order.
   - `pres.writeFile({ fileName: ... })`.

5. **Export & verify.** PPTX → PDF → per-slide JPG → look at each slide. If you have a lot of slides (> 6), delegate the visual inspection to a subagent — you'll miss problems on your own output.

6. **Fix & re-export.** Build → verify → fix → re-verify until a full pass reveals no new issues. Kill `POWERPNT.EXE` between Windows exports to release the file handle.

---

## The rules that matter (read [reference.md](https://github.com/Emily27-alt/html-to-pptx/blob/HEAD/reference.md) for details)

### R1 · Coordinate math

For a **1920×1080** HTML canvas mapped to a **13.333×7.5″** slide:

- `IN_PER_PX = 13.333 / 1920` → **1 CSS px = 1/144 in** for x/y/w/h
- **1 CSS px = 0.5 pt** for font sizes (this is the key conversion — a 280px headline is 140pt)

### R2 · CSS letter-spacing → pptxgenjs charSpacing ⚠️ HIGH-RISK

pptxgenjs `charSpacing` is **points, absolute**. It is not em, not a ratio, not 100ths of a point.

```javascript
const ls = (em, fontPt) => em * fontPt;   // CSS .4em @ 12pt → 4.8 (pt)
s.addText("AGENDA", { fontSize: 44, charSpacing: ls(0.1, 44) /* = 4.4 */ });
```

Getting this wrong is the **single biggest failure mode** of HTML→PPTX conversions. A value of `20` at a 12pt font produces ~1.7em of tracking — every Latin label wraps character-by-character, every Chinese title spreads into a banner row. Always compute from em × fontPt.

### R3 · Gradients are baked images

pptxgenjs doesn't do gradient fills. Pre-render gradient backgrounds as PNGs in Pillow. For gradient **text fills** (`-webkit-background-clip: text`), pick a solid color near the midpoint of the gradient — PowerPoint gradient text via XML injection is possible but rarely worth it.

### R4 · Photo → panel transition

One gradient PNG overlay, not stacked rectangles:

```javascript
// ✅ Smooth
s.addImage({ path: "photo_fade.png",  x: edge - 280, y: 0, w: 340, h: slideH });
// ❌ Bands
rect(s, { x: edge-260, ..., fill: { color:"3A1F14", transparency:70 }});
rect(s, { x: edge-130, ..., fill: { color:"3A1F14", transparency:45 }});
rect(s, { x: edge- 30, ..., fill: { color:"3A1F14", transparency:15 }});
```

### R5 · Footer-over-photo legibility

If the original design floats a footer over a photo, photo brightness varies and footers will become invisible on bright ones. Add a bottom-scrim gradient PNG (transparent at top → opaque at bottom) over the photo area behind the footer.

### R6 · Outlined / stroked text

CSS `-webkit-text-stroke` has no pptxgenjs equivalent. Approximate with solid fill at 70–85% transparency for a ghostly outline feel. Works well for giant chapter numerals.

### R7 · Rich-text for inline emphasis

```javascript
s.addText([
  { text: "首战沙海，", options: {} },
  { text: "传承安泰荣光", options: { color: C.gold_bright, bold: true } },
  { text: "。", options: {} },
], { x, y, w, h, fontFace: F.serif, fontSize: 17, ... });
```

Each run carries its own `color`, `bold`, `italic`, `breakLine`.

### R8 · Common pptxgenjs pitfalls

- Colors: `"FF0000"`, **never** `"#FF0000"` — leading `#` corrupts the file
- Don't encode alpha in hex (`"00000020"` corrupts); use `transparency` (0–100) or `opacity` (0–1)
- Don't reuse option objects between calls (pptxgenjs mutates) — build fresh with a factory function
- `breakLine: true` between array items; don't rely on `"\n"` alone
- Each `pptxgen()` instance writes one file — don't reuse

---

## Starter templates

The `templates/` folder has copy-and-adapt starters. Read a template, don't blindly copy — paths and the color/font maps always need project-specific edits.

| File | Purpose |
|---|---|
| `templates/build.template.js` | pptxgenjs skeleton with layout, helpers (`px`, `pt`, `ls`), shared chrome stubs |
| `templates/gen_bg.template.py` | Pillow gradient-background generator (desert/ink/cream variants) |
| `templates/gen_fade.template.py` | Horizontal + vertical fade PNG overlays for photo blends |
| `templates/to_pdf.template.ps1` | Windows: PowerPoint COM PPTX → PDF |
| `templates/pdf2img.template.py` | PyMuPDF PDF → per-slide JPG for visual QA |

---

## Quick sanity checklist before declaring success

- [ ] Custom slide layout matches the HTML canvas aspect ratio exactly
- [ ] All `charSpacing` values come from `ls(em, fontPt)` — not hardcoded integers
- [ ] Gradient backgrounds are baked PNGs, not attempted as native fills
- [ ] Photo fades use a single gradient PNG, not stacked rects
- [ ] Footer text is legible on every slide (add scrim if floating over photos)
- [ ] `python -m markitdown output.pptx` shows the text content intact and in order
- [ ] PDF export opens successfully; no mojibake in Chinese
- [ ] Re-opening the .pptx in PowerPoint, every text element is individually editable (click and type — not a flat image)
