HTML deck -> native, editable PPTX
The rule this skill enforces: never paste slide images into PowerPoint. Every square, bar, dot, pill, line and text block must be a real PowerPoint object the user can move, recolor and retype. The converter reads the rendered page (Playwright + the browser's own computed layout) instead of re-guessing the CSS, then writes python-pptx shapes at the exact same coordinates.
Run it
python scripts/html_to_pptx.py <deck.html> <out.pptx> [all | 1,3,5]
Needs: pip install -r requirements.txt and playwright install chromium.
Slides are the elements matching .slide (1280x720 px). Put the .html somewhere with its images inline
(data: URIs) or on disk beside it. examples/sample-deck.html is a small deck to try first.
What the page needs to look like when captured (the script does these for you): anim class removed from
<html>, CSS animations/transitions off, window beforeprint fired (decks often reveal charts on print),
print media, zoom 1, elements with class .mode or .jump hidden (theme toggle, slide navigator). Decks
with a light/dark toggle convert in whatever theme is showing — make sure that is the one the user wants.
What becomes what
| In the HTML | In the PPTX |
|---|---|
| div / card with background, border, radius | rectangle / rounded rectangle (radius -> corner adjust) |
| gradient background (linear) | native gradient fill |
| one-sided border (accent stripe) | thin rectangle; hairline/dashed borders -> real lines |
border-radius:999px pills, circles | pill / oval |
text (any block, with bold/colour runs, <br>, links) | one text box per block, runs preserved, hyperlinks kept |
<li> | real PowerPoint bullets (marker colour kept) |
| inline SVG rect/circle/line/polyline/path/text | rectangle, oval, connector (arrowheads kept), freeform with real Bezier curves, text box |
SVG <textPath> (text on a curve) | PowerPoint curved text (Arch Up/Down warp) |
CSS clip-path: polygon(...) (chevrons, diagonal cuts) | freeform shape; a photo inside is cropped to the polygon |
<img>, photos, icon PNGs | pictures (they are bitmaps — that is expected) |
text-transform, letter-spacing, strikethrough, opacity/alpha | applied to the runs / fills |
Two-view toggle (.vtog with data-view=code and data-view=time buttons) | second slide for the other view |
| JS animations | final resting state only |
Hard-won rules (each cost real time — keep them)
- Text position is calibrated, do not "fix" it by eye. PowerPoint 100% line spacing = 1.2 x font size
(Arial and Arial Black alike). A text box must be placed at
top = rangeTop + (ascent-cap)*fs - lineHeight + K*fswith(ascent-cap, K) = (0.189, 0.95)for Arial and(0.385, 1.0)for Arial Black, andline_spacing = cssLineHeight / (1.2*fs). Single-line text uses spacing 100% andwrap=nonewith slack so it never re-wraps. - Chrome on Windows renders
font-family: Arialat weight 800/900 as ARIAL BLACK. The converter maps it to Arial Black (not bold). Without this every heavy headline looks too thin and the wrong width. - Never swallow inline-block children into a text block. Only
display:inlinechildren are merged into one text box; inline-block/flex children are handled separately. - Measure text nodes in place (Range on the node). Wrapping a node in a temp
<span>changes grid/flex layout. - Open the result in PowerPoint to prove it is valid. python-pptx will happily write a file PowerPoint
refuses ("PowerPoint could not open the file"). Known causes: negative/NaN shape sizes (clamp, drop NaN
points), and
calc()insideclip-path(evaluate it). Bisect by deleting shapes in halves and re-opening. - A picture with custom geometry fills its path's bounding box, not its frame. To crop a photo to a polygon,
size the picture to the polygon's bounding box and set a source crop (
srcRect) for the rest. - Do not screenshot-capture shapes ("rasterize the odd ones"). Convert logos' SVG paths and clip-path
polygons to freeforms instead. The
rasterbranch is a last resort. - Skip clipped ghost text (big faded numerals cut off by their card) — PowerPoint cannot clip text boxes.
- PowerPoint
a:lngoes after the fill inspPr; delete the themep:styleor shapes pick up shadows. - Never overwrite a .pptx the user has open — write a new file name instead.
Verify (always — never ship unseen)
Windows with PowerPoint installed:
powershell -NoProfile -File scripts/render_pptx.ps1 -pptx <out.pptx> -outdir <png_dir> # PowerPoint export to PNG
python scripts/compare.py <deck.html> <png_dir> 1,2,3 <out.png> # HTML | PPTX side by side
python scripts/compare.py <deck.html> <png_dir> 6:6,7:8 <out.png> # html:pptx when slide numbers differ
render_pptx.ps1 leaves PowerPoint running if the user already had it open. Without PowerPoint, render the
.pptx to PNGs another way (for example LibreOffice headless) into s1.png, s2.png… and run compare.py.
Read the comparison PNG, fix, rebuild. Typical clean-up after a run: text slightly wider in PowerPoint (kerning), nothing structural.
Known limits (tell the user)
- Text placement is calibrated for Arial and Arial Black. Other fonts convert, but text may sit a few pixels high or low until that font is calibrated (measure cap height and line spacing in PowerPoint).
- Photos, icons and logos supplied as bitmaps stay pictures. Text sits in its own box on top of its card (not grouped).
- Animations/toggles are flattened to one frame (second view of a toggle becomes an extra slide).
- Radial gradients and per-corner radii are approximated; box-shadows are dropped.
- Slide size is fixed to 1280x720 px -> 13.333 x 7.5 in.
Credit
Built by Impera Business Services. MIT License — see LICENSE.