nexu-io/pptx-html-fidelity-audit
Audit a python-pptx export against its source HTML deck, identify layout/content drift (footer overflow, cropped content, missing italic/em, lost styling, off-rhythm spacing), and re-export with strict footer-rail + cursor-flow layout discipline. Use this skill whenever the user has a .pptx that was generated from an HTML slide deck and asks to compare/audit/verify/fix the export — including phrases like "compare ppt with html", "fidelity audit", "fix the pptx", "ppt is cut off", "footer overlap", "italic missing in pptx", "re-export the deck", "pptx-html-fidelity-audit", or any case where a python-pptx → HTML round-trip needs verification or repair. Also trigger when the user shows you a deck.html and a deck.pptx side by side and is debugging visual differences.
npx skills add https://github.com/nexu-io/open-design --skill pptx-html-fidelity-audit
A repeatable workflow for catching the ways a python-pptx export silently drifts from its HTML source — and fixing them with a layout discipline that prevents the same regressions on the next pass.
The user has:
<section class="slide"> blocks): <section class="slide light">
<div class="chrome">2026 · Q2 review</div>
<span class="kicker">Pillar 03</span>
<h2 class="h-xl">Shipping <em>velocity</em> doubled</h2>
<p class="lead">…</p>
<div class="foot">page 5 / 14</div>
</section>
If the user only has *one* of those two artifacts, this skill doesn't apply yet — first generate the missing one, or ask the user to provide it.
PPTX is a fixed-canvas, absolute-positioned medium. HTML is a fluid, flow-based medium. A naive python-pptx export pins each block at hand-picked (top, left) coordinates, which works for the *first slide it was tested on* and silently fails for every other slide whose content has different intrinsic height. The result is the most common drift modes:
top + height crosses into the footer row.7.5" (16:9 canvas).<em> in HTML never gets run.font.italic = True.MARGIN_TOP instead of computing center.Every one of these is a *layout discipline* problem, not a content problem. Once you adopt the discipline, they stop happening.
The audit is five steps. Don't skip any of them — the discipline only works if the audit produces a real list of issues to drive the re-export. A fix-without-audit pass tends to leave half the issues alive.
Run scripts/extract_pptx.py <path-to.pptx> > pptx_dump.json. The script walks every shape on every slide and dumps text, position (top / left), size (width / height), and per-run typography (font name, size pt, bold, italic, color). This is the *actual* state of the export — don't trust the export script's intent, trust the dump.
For 14-slide decks, the dump is ~30–60 KB and human-readable.
Read the source HTML and enumerate <section class="slide"> blocks. For each, note:
light / dark / hero light / hero dark).chrome row text (top metadata).kicker (small uppercase eyebrow above the headline).foot row (bottom metadata).<em> or italic-styled spans — italic is the silent regression.Map each HTML slide to a PPTX slide index. For decks following the convention "slide 1 = cover, slide N = closing", the mapping is positional.
For each slide, walk shapes from the dump and check against expected layout rules. Use this exact table format — the severity column is what drives the fix priority:
| Slide | Issue | Severity |
|---|---|---|
| 1 cover | meta-row 底端 6.95" 蓋過 footer (6.7") | 🔴 |
| 5 checklist | row B 步驟描述底端 7.2" 切到 footer | 🔴 |
| 8 3E | 收束段落直接坐在 footer 起點 | 🔴 |
| 9 on-day | step 描述底端剛好碰 footer,無安全距 | 🟠 |
| 多處 | em (Playfair italic) 未保留 | 🟡 |
Severity rubric:
After the table, write a short root-cause section: 90 % of the issues usually come from 2–3 systemic causes (e.g. "no footer rail enforced", "hero stacks pinned to MARGIN_TOP instead of centered", "italic never propagated"). Naming the systemic causes makes the re-export script much smaller and more correct.
This is the load-bearing technique. See references/layout-discipline.md for the full rules; the summary:
Define the rails up front, once, for the whole deck:
from pptx.util import Inches
CANVAS_W = Inches(13.333) # 16:9
CANVAS_H = Inches(7.5)
MARGIN_X = Inches(0.6)
MARGIN_TOP = Inches(0.5)
CONTENT_MAX_Y = Inches(6.70) # NOTHING in content area may cross this
FOOTER_TOP = Inches(6.85) # footer row pinned here, edge-to-edge
> Customizing the rails. The defaults above suit a 16:9 canvas with a slim footer. If your design system uses a wider footer or a 4:3 canvas, override these constants in your export script and pass the same values to verify_layout.py via --content-max-y / --canvas-h / --canvas-w. See references/layout-discipline.md §1 for the full constant table.
Use a cursor for content blocks instead of pinning each block at an absolute y:
class Cursor:
"""Advances down the slide; refuses to cross the footer rail."""
def __init__(self, y_start, cap=CONTENT_MAX_Y):
self.y = y_start
self.cap = cap
def take(self, h, gap=Inches(0.12)): # ~1 line of whitespace at 14pt; tighten/loosen per design system
top = self.y
self.y = top + h + gap
if self.y > self.cap:
raise OverflowError(
f"cursor at {self.y} exceeds footer rail {self.cap}; "
f"reduce block height or split slide"
)
return top
For each slide, instantiate Cursor(MARGIN_TOP) and take(height) each block in reading order. The slide refuses to render if any block would cross the rail, so overflows become loud build errors instead of silent visual bugs.
Hero (vertically-centered) slides use a budget instead of a cursor:
def hero_layout(blocks):
"""blocks = list of (height, gap_after) tuples in reading order."""
total = sum(h + g for h, g in blocks)
y_start = (CANVAS_H - total) / 2
return Cursor(y_start)
That single change kills "hero slide content sticks to top" — the most common hero defect.
Tighten box height to fit text + minimal padding. PowerPoint reveals shape bounds when they overlap (selection halos, Z-order conflicts), and an oversized box can visually cross the footer rail even when the text inside doesn't. Compute box height from text metrics + ~0.05" pad, not from generous wrappers.
Preserve italic / em explicitly:
def add_run(p, text, font, size_pt, italic=False, bold=False, color=None):
r = p.add_run()
r.text = text
r.font.name = font
r.font.size = Pt(size_pt)
r.font.italic = italic
r.font.bold = bold
if color:
r.font.color.rgb = color
return r
When walking HTML, detect <em> / <i> / inline style font-style: italic and pass italic=True. Use the EN serif face (Playfair Display, Source Serif, or fallback Georgia) for italic display copy — the CJK serif typically has no italic and looks broken if you try to italicize it.
For deeper font issues that the layout rails can't catch — variable-font traps where PowerPoint silently swaps to Calibri / Microsoft JhengHei, missing <a:ea> slot causing CJK runs to fall back, fake-italic on Han characters — read references/font-discipline.md. The five layers there cover everything verify_layout.py can't see.
After writing the new .pptx, run scripts/verify_layout.py <path-to.pptx>. The script:
top + height ≤ CONTENT_MAX_Y for content shapes (footer/page-number shapes are allowed below the rail).top + height ≤ CANVAS_H for all shapes (no off-canvas).left + width ≤ CANVAS_W and left ≥ 0.Zero violations is the gate for "this re-export is shippable". Don't claim the audit is fixed without running the verifier — the human eye misses 1–2 mm overflow at zoom-out, the script doesn't.
After Step 5 passes, report:
.pptx.The user is reading for two reasons: confirming the visible bugs are fixed, and trusting the systemic fix is right. Cover both.
scripts/extract_pptx.py — dump every shape on every slide as JSON. Run before the audit. Important: also run on the *original* export to compare, and on the *re-exported* one to confirm.scripts/verify_layout.py — post-export rail checker. Returns nonzero exit code on violations so it slots into a CI pipeline if needed.references/layout-discipline.md — the full footer-rail + cursor-flow rule set with code snippets for each common slide type (hero, content, pipeline, two-column, observation grid).references/font-discipline.md — five-layer font audit: mapping, presence, variable-vs-static traps, the three XML language slots (latin / ea / cs), CJK + Latin italic interaction.references/audit-table-template.md — copy-pasteable table template with severity legend.Read the references when:
layout-discipline.md.Calibri / Microsoft JhengHei in the XML → font-discipline.md.audit-table-template.md.italic=True, and the result looks mechanically deformed. Italicize *only* runs whose primary script supports italic — Latin, Cyrillic, Greek. See references/font-discipline.md Layer 5 for the implementation pattern.MARGIN_TOP for hero slides. Hero slides need *budget centering*, not top-anchored. This is the most common hero defect and the cheapest to fix.An earlier iteration of this skill leaned on visual diffing — render the
.pptx through Keynote → PDF → PNG, screenshot the HTML through Chrome
headless, stitch them side-by-side with magick. It worked, but with
three sharp drawbacks:
magick andfont-discovery commands vary across OSes; CI pipelines on Linux can't
reproduce the chain.
preview. The human eye misses it; the script catches it as a hard
numeric violation.
installed before they can audit. Geometry checks need only
python-pptx.
Geometry-based verification gives up one thing the visual diff is good
at: catching cases where shape positions are correct but the rendered
glyph looks wrong (font fallback, kerning bugs, missing weight). When
that case appears, fall back to a manual screenshot review — the
five-layer audit in references/font-discipline.md covers most of the
underlying causes.
Take nexu-io/pptx-html-fidelity-audit from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.