Install
openclaw skills install html-to-one-pptxConvert 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", "mak...
openclaw skills install html-to-one-pptxRead first:
pptxgenjs.mdfor the full API reference.
Five principles:
tmp/slide_NN/ directorySCALE = min(slide_w/html_w, slide_h/html_h) guarantees 1-page outputPROJECT/
├── 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
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
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
NEVER batch-loop gen.js files — write each one individually and run it before moving to the next.
tmp/parsehtml.mdRead 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:
Cumulative Y verification: After documenting nested structures, verify:
Grid cell calculation: For tile grids:
Every row in §4 Shapes becomes one addShape() call in gen.js
Every row in §5 Text becomes one addText() call in gen.js
Every §6 group becomes one forEach() loop in gen.js
§9 values are copied verbatim into the gen.js boilerplate
If a shape has border-radius → use pres.shapes.ROUNDED_RECTANGLE
If a shape uses clip-path / conic-gradient / polygon → note it and use the closest PPT shape
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
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:
| Source | Pattern |
|---|---|
| Chart.js | new Chart(ctx, { type, data: { labels, datasets } }) |
| ECharts | setOption({ xAxis, series }) |
| CSS | conic-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.
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
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,
});
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.
From parsehtml.md §4 Shapes + §5 Text, construct a parent-child color table:
| text-id | text-color | parent-shape | parent-fill | contrast | pass? |
|---|---|---|---|---|---|
| h-title | #ffffff | header-bg | #0f2460 | 12.4:1 | ✅ |
| tag-1 | #93c5fd | header-bg | #0f2460 | 5.8:1 | ✅ |
| tile-label | #6b7280 | tile-bg | #f0f9ff | 3.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:
For overlapping shape pairs (child on top of parent), flag if:
| issue | element | fill | parent-fill | ΔL | fix-strategy |
|---|---|---|---|---|---|
| invisible card | tile-3 | #f0f9ff | #f0f5fd | 2 | darken fill or add border |
| low-contrast text | sub-label | #9ca3af | #f9fafb | — | darken text |
If the user specified colors:
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
C overrides into gen.jsIn 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,
...
});
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 ✅
"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); });
// ✅ Unicode escapes — always safe
slide.addText("\u6570\u636E\u5B89\u5168", { ... });
// ✅ Raw Chinese safe inside Write tool (UTF-8)
slide.addText("数据安全", { ... });
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:
| Bad | Why broken | Fix |
|---|---|---|
"\u8诗" | 诗 is not a hex digit | Find correct codepoint: "\u8BC7" |
"\u26Q1" | Q is not hex | Use literal or correct escape: "26Q1" |
"\uD83C\uDFCX" | X invalid | Look 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
| Property | Wrong | Correct | Reason |
|---|---|---|---|
fontSize | 12 (px) | 12 * 0.75 = 9 (pt) | PPT uses points, not pixels |
wrap | omit | false (single-line) / true (multi-line) | Control text wrapping |
margin | omit / 5 | 0 | PPT default adds 0.05" per side |
valign | omit | middle / top / bottom | Vertical 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
"PingFang SC" → "Microsoft YaHei"
"system-ui" → "Segoe UI"
"Helvetica Neue" → "Arial"
"Inter" → "Calibri"
"KaiTi", "楷体" → "Microsoft YaHei" // KaiTi not always available
Before writing gen.js, verify:
node ./tmp/gen.js # single HTML
node ./tmp/slide_01/gen.js # per-slide (multi-HTML)
Trigger: run this phase if ANY of these apply:
tmp/slide_NN/output.pptx exist)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
# 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
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.
PROJECT/
└── tmp/
├── slide_01/output.pptx ✅ 42KB, 8 shapes
├── slide_02/output.pptx ✅ 38KB, 6 shapes
└── ...
final.pptx ✅ merged result
| Symptom | Cause | Fix |
|---|---|---|
[ERROR] No slide files found | Wrong working dir | cd to project root first |
| Slide appears blank in final.pptx | gen.js had a runtime error | Re-run node gen.js — check for errors before merging |
| Charts show wrong data after merge | pptxgenjs uses absolute chart paths (/ppt/charts/chart1.xml); multiple slides share the same name → first slide's chart overwrites the rest | Use zip-level merge script (v4.3+) which renames chart files to unique indices |
| Charts display as empty grey box | Chart XML copied but embedding .xlsx not copied, or Content_Types missing chart override | Ensure 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 warning | Using python-pptx relate_to() API to merge — wrong approach | Switch to zip-level merge script; never use add_slide() + relate_to() for merging |
ModuleNotFoundError: pptx | python-pptx not installed | pip install python-pptx lxml |
ModuleNotFoundError: lxml | lxml not installed | pip install lxml |
"#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-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 },
});
shadow: { type:"outer", color:"000000", opacity:0.1, blur:8, offset:2, angle:270 }
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,
});
});
| ❌ Don't | ✅ Do |
|---|---|
| Skip writing parsehtml.md | Complete all 9 sections of parsehtml.md before touching gen.js |
| Write gen.js without parsehtml.md | Every 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 text | Run full contrast map; darken text or lighten/darken fills to meet thresholds |
| Batch-loop all gen.js | Write each gen.js individually, run it, then move to next |
| Draw charts as shapes/polygons | Use addChart() — PPT native charts |
Skip detect_charts.py when <canvas> present | Always run it for Chart.js / ECharts / canvas |
| Use scale = sw/html_w only | Use min(sw/html_w, sh/html_h) — from parsehtml.md §9 |
| Forget X_OFF / Y_OFF | x_in = css_px × SCALE + X_OFF — from parsehtml.md §9 |
| color: "#0891b2" | color: "0891b2" |
| fontFace: "PingFang SC" | fontFace: "Microsoft YaHei" |
| fontSize in CSS px | fontSize × 0.75 for pt |
| Omit wrap/margin on text | wrap:false, margin:0 on every single-line text |
| Copy-paste repeated elements | Use forEach loop (shape_type from parsehtml.md §6) |
| Round coordinates in conversion | Use v * SCALE directly, no Math.round |
| Skip cumulative Y verification | Verify: end_y = start_y + height for each section |
| Misplace draw order | Follow z-order: backgrounds → shapes → text |
| Use px() for positions | Use x()/y() for positions, px() for dimensions |
Write \uXXXX without verifying all 4 chars are hex | Validate 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 |
If output has misaligned elements:
console.log(SCALE, X_OFF, Y_OFF)If colors look wrong or elements are invisible:
const C block was copied into gen.js correctly