html-to-one-pptx

Convert any HTML file or design into a pixel-faithful PowerPoint (.pptx) slide. Trigger when the user uploads HTML file(s) and asks to "convert to PPT", "make a PowerPoint", "turn this into slides", or "一比一还原为 PPT". Handles single HTML or multiple HTML files (merged into one PPT). Always follow this skill — never improvise the workflow.

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "html-to-one-pptx" with this command: npx skills add panhongwei/html-to-one-pptx

HTML → PPTX Conversion Skill (v4.4)

Read first: pptxgenjs.md for the full API reference.

Five principles:

  • One HTML = one slide subfolder — each HTML gets its own tmp/slide_NN/ directory
  • Never batch-script — write and run gen.js individually for each slide
  • Fit-to-pageSCALE = min(slide_w/html_w, slide_h/html_h) guarantees 1-page output
  • Charts use native PPT — detect chart data with detect_charts.py → addChart() (not images)
  • Color Master before gen.js — always audit contrast and visibility before writing any code

Workflow: Single HTML

PROJECT/
├── slide.html
└── tmp/
    ├── parsehtml.md          ← Claude writes this in Phase 1
    ├── colors.md             ← from Phase 2.5 Color Master
    ├── charts.json           ← from detect_charts.py (if charts found)
    ├── gen.js                ← write here
    └── output.pptx

① self-parse HTML   →  Claude reads HTML, writes tmp/parsehtml.md
② detect_charts.py  →  extracts chart data → tmp/charts.json  (skip if no charts)
③ Color Master      →  audit contrast + visibility → tmp/colors.md  (always run)
④ write gen.js      →  driven by parsehtml.md + charts.json + colors.md
⑤ node tmp/gen.js   →  tmp/output.pptx  ✅ done

Workflow: Multiple HTML Files

PROJECT/
├── slide1.html
├── slide2.html
└── tmp/
    ├── slide_01/
    │   ├── parsehtml.md     ← Claude writes this first
    │   ├── colors.md        ← from Phase 2.5 Color Master
    │   ├── charts.json      ← if charts detected
    │   ├── gen.js
    │   └── output.pptx
    ├── slide_02/
    │   └── (same structure)
    └── final.pptx           ← merged result

Multi-HTML step-by-step

For EACH HTML file, one at a time (never batch):

# Slide 1
SLIDE=./tmp/slide_01
mkdir -p $SLIDE
# ① Write tool → $SLIDE/parsehtml.md  (Phase 1 template)
python3 scripts/detect_charts.py slide1.html --out $SLIDE
# ③ Color Master → $SLIDE/colors.md   (Phase 2.5)
# → write $SLIDE/gen.js  (from parsehtml.md + charts.json + colors.md)
node $SLIDE/gen.js   # ✅ output.pptx generated

# Slide 2 — only after slide 1 is done
SLIDE=./tmp/slide_02
mkdir -p $SLIDE
# ① Write tool → $SLIDE/parsehtml.md
python3 scripts/detect_charts.py slide2.html --out $SLIDE
# ③ Color Master → $SLIDE/colors.md
# → write $SLIDE/gen.js
node $SLIDE/gen.js   # ✅ output.pptx generated

# Merge all slides
python3 scripts/merge_slides.py --slides-dir ./tmp

CRITICAL anti-lazy rule

NEVER batch-loop gen.js files — write each one individually and run it before moving to the next.


Phase 1 — Self-Parse HTML → write tmp/parsehtml.md

Read the HTML source completely, then write tmp/parsehtml.md (or tmp/slide_NN/parsehtml.md) with the following structure. This file is your single source of truth for gen.js — every shape, text, and chart must appear here before being coded.

# parsehtml.md

## 1. Canvas
- size: {width}px × {height}px
- background: {color}
- font-family: {family}
- layout: {description of top-level structure with exact px values}
  Example: "header 72px / content 580px (left-panel 668px + right-panel 349px) / summary 48px / footer 20px"

## 2. Layout Blocks
List every top-level structural section with page-level coordinates:

| id | x | y | w | h | bg | notes |
|----|---|---|---|---|----|-------|
| header | 0 | 0 | 1017 | 72 | #0f2460 | flex row, space-between, padding:0 28px |
| left-panel | 0 | 72 | 668 | 580 | #f0f5fd | padding:8 10 6 12, border-right:1px #bfdbfe |
| right-panel | 668 | 72 | 349 | 580 | #fff | padding:10 12 8 10 |
| summary | 0 | 652 | 1017 | 48 | #eef3fc | border-top:2px #2563eb |
| footer | 0 | 700 | 1017 | 20 | #0f2460 | flex space-between |

## 3. Nested Structure Analysis (CRITICAL)
For complex components (e.g., "house" container), document the FULL nested hierarchy:

```
house (x:12, y:80, w:646, h:497)
├── roof (h:44, y:82-126)
│   ├── bg:#0f2460, border-bottom:2px #2563eb at y=124
│   └── padding:0 44px (text starts at x=44)
├── floor-2col #1 (h:148, y:128-276)
│   ├── left: 应用安全 (w:322) + db-hd:24px + tile-grid:124px (2×4)
│   ├── col-sep: 1px at x=336
│   └── right: 网络安全 (w:322) + db-hd:24px + tile-grid:124px (2×4)
├── floor-div (h:1, y:276-277)
├── floor-2col #2 (h:116, y:278-394)
│   ├── left: 数据安全 (w:322) + db-hd:24px + tile-grid-3r:92px (2×3)
│   └── right: 内容安全 (w:322) + db-hd:24px + tile-grid-3r:92px (2×3)
├── floor-div (h:1, y:394-395)
├── model-floor (h:114, y:396-510) 全宽
│   ├── db-hd:24px + tile-grid-model:90px (3×3)
├── floor-div (h:1, y:510-511)
└── ops-floor (h:72, y:512-584) 全宽
    ├── ops-hd:24px + ops-tiles:48px (7 items horizontal)
```

## 4. Shapes
List every visual shape (div/span with background, border, border-radius, box-shadow):

| id | x | y | w | h | fill | border | radius | notes |
|----|---|---|---|---|------|--------|--------|-------|
| header-bg | 0 | 0 | 1017 | 72 | #0f2460 | bottom 3px #2563eb | 0 | |
| house | 12 | 80 | 646 | 497 | #f4f8ff | 2px #1e3a8a | 0 | container |
| roof | 14 | 82 | 642 | 44 | #0f2460 | bottom 2px #2563eb | 0 | trapezoid clip |
| db-hd-app | 14 | 128 | 322 | 24 | #3b82f6 | bottom 1px rgba(255,255,255,0.2) | 0 | |

## 5. Text Elements
List every visible text string with its style:

| id | text | x | y | w | h | size(px) | weight | color | align | wrap |
|----|------|---|---|---|---|----------|--------|-------|-------|------|
| h-title | "AI 安全管理框架" | 28 | 20 | 300 | 40 | 22 | bold | #ffffff | left | no |
| tag-1 | "GB/T 42118" | 560 | 20 | 80 | 28 | 11 | normal | #93c5fd | center | no |

## 6. Repeated Groups
For any element rendered N times (cards, tiles, table rows, tags):

### [group name] — N items, pattern: {forEach description}
- container: x={start_x}, y={start_y}, w={w}, h={h}, gap={g}, cols={c}
- cell calculation: cell_w=(container_w - padding_left - padding_right - (cols-1)*gap)/cols
- cell_h=(container_h - padding_top - padding_bottom - (rows-1)*gap)/rows
- shape_type: RECTANGLE | ROUNDED_RECTANGLE (from CSS border-radius)
- style: fill={color}, border={color:width}, text color={color}, font-size={px}
- data:
  1. "text content"
  2. "text content"
  ...

## 7. Charts
(If none, write "none")
- type: {radar/bar/line/pie/doughnut}
- x, y, w, h (page coords)
- data: {labels and values}
- colors: [...]

## 8. Z-order (draw sequence)
1. {bottom-most shape} - e.g., "header-bg"
2. {next level} - e.g., "left-panel-bg"
3. ...
N. {top-most element} - e.g., "footer text"

## 9. Scale & Offsets
- SLIDE_W = 10, SLIDE_H = 7.5  (LAYOUT_4x3)
- HTML_W = {w}, HTML_H = {h}
- SCALE = min(10/{w}, 7.5/{h}) = {value}
- X_OFF = (10 - HTML_W * SCALE) / 2 = {value}
- Y_OFF = (7.5 - HTML_H * SCALE) / 2 = {value}

Golden Rules:

  1. Cumulative Y verification: After documenting nested structures, verify:

    • Each floor's end_y = start_y + height
    • Divider positions match floor endings
    • Total height = sum of all sections (roof + floors + dividers)
  2. Grid cell calculation: For tile grids:

    • Available width = container_w - padding_left - padding_right
    • Available height = container_h - padding_top - padding_bottom
    • cell_w = (available_w - (cols-1) * gap) / cols
    • cell_h = (available_h - (rows-1) * gap) / rows
  3. Every row in §4 Shapes becomes one addShape() call in gen.js

  4. Every row in §5 Text becomes one addText() call in gen.js

  5. Every §6 group becomes one forEach() loop in gen.js

  6. §9 values are copied verbatim into the gen.js boilerplate

  7. If a shape has border-radius → use pres.shapes.ROUNDED_RECTANGLE

  8. If a shape uses clip-path / conic-gradient / polygon → note it and use the closest PPT shape

Fit-to-Page Scale

const SCALE = Math.min(SLIDE_W / HTML_W, SLIDE_H / HTML_H);
const X_OFF = (SLIDE_W - HTML_W * SCALE) / 2;
const Y_OFF = (SLIDE_H - HTML_H * SCALE) / 2;

Layout reference:

LAYOUT_WIDE  →  13.3 × 7.5 inches
LAYOUT_16x9  →  10   × 5.625 inches
LAYOUT_4x3   →  10   × 7.5 inches   ← default

Phase 2 — Detect Charts

Run only if HTML contains charts (<canvas>, Chart.js, ECharts, conic-gradient):

python3 scripts/detect_charts.py input.html
# Multi-HTML:
python3 scripts/detect_charts.py slide1.html --out ./tmp/slide_01

Detects:

SourcePattern
Chart.jsnew Chart(ctx, { type, data: { labels, datasets } })
EChartssetOption({ xAxis, series })
CSSconic-gradient(...) → pie approximation
SVG<polygon> → radar approximation

Output: ready-to-paste gen.js snippet

// Chart 1: radar
slide.addChart(pres.ChartType.radar, [
  { name:"2024 年", labels:["技术能力","市场份额","客户满意度","创新指数","团队实力"],
    values:[85,70,90,75,80] },
  { name:"2023 年", labels:["技术能力","市场份额","客户满意度","创新指数","团队实力"],
    values:[70,65,80,60,75] }
], {
  x:0.5, y:0.5, w:5.5, h:3.8,
  chartColors: ['0891b2','22c55e'],
  showLegend: true, legendPos: 'b',
  radarStyle: 'filled',
});

Adjust x, y, w, h to match the chart's position from parsehtml.md §7.

If datasets=0 (data not extracted) — fill in the data manually in parsehtml.md §7, then build addChart() by hand.

Supported chart types

pres.ChartType.radar      // spider/radar
pres.ChartType.line       // trend line
pres.ChartType.area       // filled trend area
pres.ChartType.bar        // bar/column (barDir: 'col' or 'bar')
pres.ChartType.pie        // pie
pres.ChartType.doughnut   // doughnut (holeSize: 50)
pres.ChartType.scatter
pres.ChartType.bubble

addChart() full reference

slide.addChart(pres.ChartType.radar, [
  { name: "Series", labels: ["A","B","C","D","E"], values: [85,70,90,75,80] }
], {
  x: 1.0, y: 0.5, w: 5.5, h: 4.0,
  chartColors: ["0891b2", "22c55e", "7c3aed"],  // NO # prefix
  showLegend: true,  legendPos: "b",  legendFontSize: 9,
  showValue: true,   showPercent: true,
  dataLabelFontSize: 9,  dataLabelColor: "374151",
  catAxisLabelFontSize: 9,  valAxisLabelFontSize: 8,  valAxisMajorUnit: 25,
  radarStyle: "filled",          // "standard" | "filled" | "marker"
  barDir: "col",                 // "col" | "bar"
  barGrouping: "clustered",      // "clustered" | "stacked" | "percentStacked"
  barGapWidthPct: 50,
  lineDataSymbol: "circle",      // "circle" | "dash" | "diamond" | "none" | "square" | "triangle"
  lineSmooth: false,
  holeSize: 50,                  // doughnut only (10–90%)
  showTitle: true,  title: "Chart Title",  titleFontSize: 11,
});

Phase 2.5 — Color Master (调色大师)

Always run this phase after parsehtml.md is complete and before writing gen.js. Perform a full color audit every time — write colors.md even if no issues are found.

What to check

  1. Any text element with contrast ratio < 4.5:1 against its parent background
  2. Overlapping or adjacent shapes with visually indistinguishable fills (ΔL < 10 in HSL)
  3. Repeated group cards sharing the same fill as their container (invisible cards)
  4. User's explicit color requirements (e.g. "换成蓝色系", "颜色深一点", "高对比度")

Step 1 — Build the contrast map

From parsehtml.md §4 Shapes + §5 Text, construct a parent-child color table:

text-idtext-colorparent-shapeparent-fillcontrastpass?
h-title#ffffffheader-bg#0f246012.4:1
tag-1#93c5fdheader-bg#0f24605.8:1
tile-label#6b7280tile-bg#f0f9ff3.1:1❌ small text

WCAG contrast ratio calculation (simplified):

L = 0.2126·R + 0.7152·G + 0.0722·B  (linearized)
contrast = (L_lighter + 0.05) / (L_darker + 0.05)

Minimum thresholds:

  • Normal text (< 18px or < 14px bold): contrast ≥ 4.5:1
  • Large text (≥ 18px or ≥ 14px bold): contrast ≥ 3:1
  • UI components / borders vs adjacent bg: contrast ≥ 3:1

Step 2 — Detect invisible / near-invisible elements

For overlapping shape pairs (child on top of parent), flag if:

  • Both fills are light (L > 85%) with ΔL < 10
  • Both fills are dark (L < 20%) with ΔL < 10
  • Card fill ≈ container fill AND border is absent or thin (< 1px)
issueelementfillparent-fillΔLfix-strategy
invisible cardtile-3#f0f9ff#f0f5fd2darken fill or add border
low-contrast textsub-label#9ca3af#f9fafbdarken text

Step 3 — Apply user requirements (if any)

If the user specified colors:

  • Honor the user's hue as the primary change
  • Adjust surrounding text/borders for contrast compliance
  • Never change the dominant brand color unless the user explicitly targets it

Step 4 — Write tmp/colors.md

# colors.md — Color Overrides (Phase 2.5 output)

## User requirements
{none | copy user's exact color words here}

## Contrast issues found
| element-id | text-color | bg-color | contrast | status |
|------------|-----------|---------|---------|--------|
| tile-label | 6b7280 | f0f9ff | 3.1:1 | ❌ fix needed |

## Visibility issues found
| element-id | fill | parent-fill | ΔL | status |
|------------|------|------------|-----|--------|
| tile-3 | f0f9ff | f0f5fd | 2 | ❌ invisible |

## Color override table
| element-id | property | original | optimized | reason |
|------------|----------|----------|-----------|--------|
| tile-label | color | 6b7280 | 374151 | contrast 3.1→7.2:1 |
| tile-3 | fill | f0f9ff | ffffff | card invisible on f0f5fd |
| tile-3 | border-color | bfdbfe | 93c5fd | add visible separation |

## Final palette (verbatim copy into gen.js const C block)
```javascript
const C = {
  tileLabel:  "374151",   // was 6b7280 — contrast fix
  tileFill:   "ffffff",   // was f0f9ff — visibility fix
  tileBorder: "93c5fd",   // was bfdbfe — visibility fix
};
```

If no issues found, still write colors.md with:

# colors.md
## No issues found — using parsehtml.md colors as-is
const C = {};  // no overrides needed

Step 5 — Inject C overrides into gen.js

In gen.js, add the const C block immediately after the boilerplate dimensions, and use C.prop anywhere the color was flagged:

// ── color overrides (from tmp/colors.md) ─────────────────────────────────────
const C = {
  tileLabel:  "374151",
  tileFill:   "ffffff",
  tileBorder: "93c5fd",
};

// Then in addShape / addText calls:
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
  fill: { color: C.tileFill },
  line: { color: C.tileBorder, width: 1 },
  ...
});
slide.addText(label, {
  color: C.tileLabel,
  ...
});

Contrast quick-reference card

White #fff on:
  #0f2460 → 12.4:1 ✅   #1e3a8a → 10.2:1 ✅   #2563eb → 5.9:1 ✅
  #3b82f6 → 3.9:1 ❌txt   #60a5fa → 2.5:1 ❌    #93c5fd → 1.8:1 ❌

Dark #1f2937 on:
  #f9fafb → 15.8:1 ✅   #f0f9ff → 14.7:1 ✅   #e0f2fe → 12.3:1 ✅

Fixes for common gray-on-white fails:
  #9ca3af → use #6b7280 (4.6:1) ✅
  #d1d5db → use #9ca3af is still bad → use #6b7280 ✅
  #6b7280 → use #4b5563 (7.4:1) for small text ✅

Phase 3 — Write gen.js

Boilerplate

"use strict";
const path = require("path");

let pptxgen;
try { pptxgen = require("pptxgenjs"); }
catch(e) { pptxgen = require(path.join(__dirname, "node_modules", "pptxgenjs")); }

// ── dimensions (from parsehtml.md §9) ────────────────────────────────────────
const SLIDE_W = 10, SLIDE_H = 7.5;   // LAYOUT_4x3 — change if needed
const HTML_W  = 1017, HTML_H  = 720; // ← copy from parsehtml.md §9
const SCALE = Math.min(SLIDE_W / HTML_W, SLIDE_H / HTML_H);
const X_OFF = (SLIDE_W - HTML_W * SCALE) / 2;
const Y_OFF = (SLIDE_H - HTML_H * SCALE) / 2;

// Conversion functions (NO rounding - keep precision)
const px = (v) => v * SCALE;
const x  = (v) => v * SCALE + X_OFF;
const y  = (v) => v * SCALE + Y_OFF;

const pres = new pptxgen();
pres.defineLayout({ name:"LAYOUT_4x3", width: SLIDE_W, height: SLIDE_H });
pres.layout = "LAYOUT_4x3";

const slide = pres.addSlide();

// ── shapes + charts ───────────────────────────────────────────────────────────
// Source: tmp/parsehtml.md §4 Shapes  → addShape() calls
// Source: tmp/parsehtml.md §6 Groups  → forEach() loops
// Source: tmp/parsehtml.md §5 Text    → addText() calls
// Source: tmp/charts.json             → addChart() calls
// Source: tmp/colors.md  const C{}    → color overrides (supersede §4/§5/§6 colors)

pres.writeFile({ fileName: path.join(__dirname, "output.pptx") })
    .then(() => console.log("✅ Written:", path.join(__dirname, "output.pptx")))
    .catch(err => { console.error("❌", err); process.exit(1); });

String safety

// ✅ Unicode escapes — always safe
slide.addText("\u6570\u636E\u5B89\u5168", { ... });

// ✅ Raw Chinese safe inside Write tool (UTF-8)
slide.addText("数据安全", { ... });

⚠️ Unicode escape validation — CRITICAL

Every \uXXXX escape must use exactly 4 hexadecimal digits (0–9, a–f, A–F). Non-hex characters cause a SyntaxError: Invalid Unicode escape sequence that aborts the entire slide.

Common failure patterns found in practice:

BadWhy brokenFix
"\u8诗" is not a hex digitFind correct codepoint: "\u8BC7"
"\u26Q1"Q is not hexUse literal or correct escape: "26Q1"
"\uD83C\uDFCX"X invalidLook up the actual emoji codepoint

Before writing gen.js, grep every \u sequence:

# Any \u followed by non-hex char → fix immediately
grep -oP '\\u[0-9a-fA-F]{0,3}[^0-9a-fA-F"\\]' gen.js

Safe alternative — use the literal character in the Write tool (UTF-8):

slide.addText("史诗风格", { ... });   // raw UTF-8, always valid

⚠️ Text properties — CRITICAL

PropertyWrongCorrectReason
fontSize12 (px)12 * 0.75 = 9 (pt)PPT uses points, not pixels
wrapomitfalse (single-line) / true (multi-line)Control text wrapping
marginomit / 50PPT default adds 0.05" per side
valignomitmiddle / top / bottomVertical alignment
// ✅ CORRECT single-line text
slide.addText("\u6570\u636E\u5B89\u5168", {
  x: x(24), y: y(100), w: px(280), h: px(32),
  fontSize: 12 * 0.75,          // CSS px → pt
  fontFace: "Microsoft YaHei",
  color:    "1a1a2e",           // no #
  wrap:      false,             // REQUIRED for single-line
  margin:    0,                 // REQUIRED
  valign:   "middle",
});

// ✅ Multi-line text (word-wrap elements only)
slide.addText("long text...", {
  x: x(24), y: y(200), w: px(320), h: px(64),
  fontSize: 12, fontFace: "Microsoft YaHei",
  wrap: true, margin: 0, valign: "top",
});

CSS px → pt conversion:

 8px→6pt   10px→7.5pt   12px→9pt   14px→10.5pt   16px→12pt   18px→13.5pt   24px→18pt

Font mapping

"PingFang SC"    →  "Microsoft YaHei"
"system-ui"      →  "Segoe UI"
"Helvetica Neue" →  "Arial"
"Inter"          →  "Calibri"
"KaiTi", "楷体"   →  "Microsoft YaHei"  // KaiTi not always available

Draw order follows parsehtml.md §8 Z-order

Layout Master Checklist

Before writing gen.js, verify:

  1. Background layers first: header-bg → panel-bg → container-bg
  2. Borders as separate shapes: bottom borders, divider lines, column separators
  3. Domain headers before tiles: Each domain's header bar drawn before its tiles
  4. Tiles use loop: forEach for repeated tiles, not individual calls
  5. Text on top: All text elements after shapes
  6. Coordinates match parsehtml.md §4: Every x/y/w/h value traces back to the table — layering is intentional, gaps are not

Phase 4 — Run gen.js

node ./tmp/gen.js                 # single HTML
node ./tmp/slide_01/gen.js        # per-slide (multi-HTML)

Phase 5 — Merge Slides (multi-HTML or user request)

Trigger: run this phase if ANY of these apply:

  • More than one HTML was converted (multiple tmp/slide_NN/output.pptx exist)
  • User asks to "合并", "merge", "combine into one file", or "生成最终 PPT"

Pre-merge checklist

Run the script's built-in inspector first — it validates every slide before merging:

# The script auto-rejects empty slides; review its output before proceeding
python3 scripts/merge_slides.py --slides-dir ./tmp

Run merge

# Auto-detect all tmp/slide_*/output.pptx → ./final.pptx
python3 scripts/merge_slides.py

# Explicit slides dir
python3 scripts/merge_slides.py --slides-dir ./tmp

# Custom output path
python3 scripts/merge_slides.py --out ./my-report.pptx

# Explicit file list (custom order)
python3 scripts/merge_slides.py tmp/slide_01/output.pptx tmp/slide_03/output.pptx tmp/slide_02/output.pptx

# Skip empty-slide rejection (not recommended)
python3 scripts/merge_slides.py --no-strict

What the script does

For each source PPTX (after the first), using ZIP-level manipulation:
  1. Read source slide XML verbatim
  2. Parse slide rels — find chart, media, notes relationships
  3. Chart handling (KEY FIX — pptxgenjs uses ABSOLUTE chart paths):
       - pptxgenjs writes Target="/ppt/charts/chart1.xml" (absolute)
       - Every source PPTX has its own chart1.xml → name collision without renaming
       - Script renames: src chart1.xml → dest chartN.xml (next available index)
       - Updates chart refs in slide rels to new absolute path
       - Copies chart rels (.rels) with updated embedding refs
       - Copies embedding .xlsx files renamed to next available index
  4. Media handling: renames image1.png → imageN.png to avoid collision
  5. Removes notesSlide rels (notes not merged)
  6. Writes slide{N}.xml + updated rels into dest zip
  7. Patches presentation.xml sldIdLst with new slide id + rId
  8. Patches ppt/_rels/presentation.xml.rels with new Relationship entry
  9. Patches [Content_Types].xml with Override entries for new slide + charts
Pre-merge:  inspects each PPTX for shapes > 0
Post-merge: re-inspects final.pptx, prints slide/shape counts

Why NOT python-pptx relate_to() API: that API copies part references across presentations and produces duplicate zip entries (Duplicate name: ppt/charts/chart1.xml) causing chart data corruption. Always use ZIP-level manipulation for merging.

Output structure

PROJECT/
└── tmp/
    ├── slide_01/output.pptx   ✅ 42KB, 8 shapes
    ├── slide_02/output.pptx   ✅ 38KB, 6 shapes
    └── ...
final.pptx                     ✅ merged result

Troubleshooting

SymptomCauseFix
[ERROR] No slide files foundWrong working dircd to project root first
Slide appears blank in final.pptxgen.js had a runtime errorRe-run node gen.js — check for errors before merging
Charts show wrong data after mergepptxgenjs uses absolute chart paths (/ppt/charts/chart1.xml); multiple slides share the same name → first slide's chart overwrites the restUse zip-level merge script (v4.3+) which renames chart files to unique indices
Charts display as empty grey boxChart XML copied but embedding .xlsx not copied, or Content_Types missing chart overrideEnsure merge script copies both ppt/charts/chartN.xml AND ppt/charts/_rels/chartN.xml.rels AND ppt/embeddings/*.xlsx
SyntaxError: Invalid Unicode escape sequence\uXXXX in gen.js contains non-hex character (e.g. \u8诗, \u26Q1)Find correct 4-hex codepoint or use raw UTF-8 string literal
Duplicate name: ppt/charts/chart1.xml warningUsing python-pptx relate_to() API to merge — wrong approachSwitch to zip-level merge script; never use add_slide() + relate_to() for merging
ModuleNotFoundError: pptxpython-pptx not installedpip install python-pptx lxml
ModuleNotFoundError: lxmllxml not installedpip install lxml

CSS → PPT Reference

Colors

"#0891b2"              → color: "0891b2"   // strip #
"rgba(8,145,178,0.18)" → color: "0891b2"   // rgb→hex, use dominant
"linear-gradient(…)"   → color: "ecfeff"   // dominant color as flat fill

Border as accent bar

// border-left: 3px solid #0891b2
slide.addShape(pres.shapes.RECTANGLE, {
  x: x(cardX), y: y(cardY), w: px(3), h: px(cardH),
  fill: { color: "0891b2" }, line: { width: 0 },
});

Box shadow

shadow: { type:"outer", color:"000000", opacity:0.1, blur:8, offset:2, angle:270 }

Repeated elements — always forEach

const rows = [
  { name:"采集", desc:"分类分级" },
  { name:"传输", desc:"TLS/国密" },
];
rows.forEach((row, i) => {
  const ry = y(380 + i * 63);
  slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
    x:x(760), y:ry, w:px(320), h:px(58),
    fill:{color:"f0fdf4"}, line:{color:"86efac",width:1},
  });
  slide.addText(row.name, {
    x:x(790), y:ry, w:px(80), h:px(58),
    valign:"middle", fontSize:7, fontFace:"Microsoft YaHei", color:"166534",
    wrap:false, margin:0,
  });
});

Critical Don'ts

❌ Don't✅ Do
Skip writing parsehtml.mdComplete all 9 sections of parsehtml.md before touching gen.js
Write gen.js without parsehtml.mdEvery addShape/addText must trace back to parsehtml.md
Skip Color Master (Phase 2.5)Always write colors.md — even if no issues found, write const C = {}
Ignore invisible cards / low-contrast textRun full contrast map; darken text or lighten/darken fills to meet thresholds
Batch-loop all gen.jsWrite each gen.js individually, run it, then move to next
Draw charts as shapes/polygonsUse addChart() — PPT native charts
Skip detect_charts.py when <canvas> presentAlways run it for Chart.js / ECharts / canvas
Use scale = sw/html_w onlyUse min(sw/html_w, sh/html_h) — from parsehtml.md §9
Forget X_OFF / Y_OFFx_in = css_px × SCALE + X_OFF — from parsehtml.md §9
color: "#0891b2"color: "0891b2"
fontFace: "PingFang SC"fontFace: "Microsoft YaHei"
fontSize in CSS pxfontSize × 0.75 for pt
Omit wrap/margin on textwrap:false, margin:0 on every single-line text
Copy-paste repeated elementsUse forEach loop (shape_type from parsehtml.md §6)
Round coordinates in conversionUse v * SCALE directly, no Math.round
Skip cumulative Y verificationVerify: end_y = start_y + height for each section
Misplace draw orderFollow z-order: backgrounds → shapes → text
Use px() for positionsUse x()/y() for positions, px() for dimensions
Write \uXXXX without verifying all 4 chars are hexValidate every Unicode escape: digits 0–9 and letters a–f/A–F only. Invalid digit (e.g. \u8诗, \u26Q1) = SyntaxError. Use raw UTF-8 literal when unsure.
Merge PPTX with python-pptx add_slide() + relate_to()Use ZIP-level merge script — pptxgenjs absolute chart paths cause duplicate zip entries and wrong chart data with the python-pptx API

Debugging Checklist

If output has misaligned elements:

  1. Verify SCALE calculation: console.log(SCALE, X_OFF, Y_OFF)
  2. Check cumulative Y: Each section's end_y must match next section's start_y
  3. Verify grid calculations: cell_w/cell_h formulas account for all gaps
  4. Confirm z-order: Backgrounds drawn before foreground elements
  5. Check border positions: Separate shape for borders (not included in main rect)
  6. Validate padding: Container padding subtracted from cell calculations

If colors look wrong or elements are invisible:

  1. Re-read colors.md: Verify const C block was copied into gen.js correctly
  2. Check contrast map: Re-run Phase 2.5 audit for the affected element
  3. Verify parent bg: Confirm which shape is the visual parent — may differ from DOM parent

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

Wechat Mp Writer

WeChat Official Account (公众号) content writer with article formatting, headline optimization, and engagement tips. Use when you need to write WeChat articles,...

Registry SourceRecently Updated
General

OpenClaw EverMemory Installer

Use this skill when installing, upgrading, verifying, or publishing the EverMemory OpenClaw plugin and its companion skill, including local path install, npm...

Registry SourceRecently Updated
General

Ip Advisor

知识产权顾问。专利、版权、商业秘密、注册流程、保护策略。IP advisor for patents, copyrights, trade secrets. 知识产权、专利、版权。

Registry SourceRecently Updated
1950ckchzh
General

炒股大师模拟器

炒股大师模拟器 | 股市模拟交易练习 | A股/港股/美股投资学习 | 化身文主任/股神老徐/炒股养家/孙宇晨等各位大师学习投资思路 | 多智能体股票讨论群

Registry SourceRecently Updated