# HTML → PPTX 转换方案

> 华平小超人 Scratch 课件 · 完整技术文档
> 
> 2026-05-24 整理

---

## 一、技术栈

```
HTML 设计稿 (1920×1080px)
    ↓ 合并
  combined.html
    ↓ Playwright + Chromium 渲染
  dom-to-pptx 转换
    ↓ 浏览器 download 事件拦截
  output.pptx
    ↓ Cloudflare Pages
  files-f0v.pages.dev 交付
```

| 工具 | 版本 | 用途 |
|------|------|------|
| dom-to-pptx | npm latest | 核心转换引擎，浏览器端把 DOM 元素转成 PPTX XML |
| Playwright | npm latest | 驱动无头 Chromium，执行页面渲染 + 导出 |
| Chromium | Playwright 内置 | 渲染引擎 |
| python-pptx | 1.0.2 | 辅助：占位图生成、文件后处理 |
| Pillow | 12.2.0 | 辅助：占位图绘制 |
| Cloudflare Pages | — | 文件站部署交付 |

---

## 二、详细流程

### Step 1 — 设计单页 HTML

每页一个独立 HTML 文件，按 1920×1080 画布设计。所有样式内联在 `style=""` 属性中。

```html
<!-- page-01.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700;900&display=swap');
  </style>
</head>
<body style="margin:0;padding:0">
  <div class="slide-container" style="
    width:1920px;
    height:1080px;
    position:relative;
    overflow:hidden;
    background:linear-gradient(150deg,#0a1628,#142840,#1B4F72);
    font-family:'Noto Sans SC',sans-serif
  ">
    <!-- 光晕装饰 -->
    <div style="position:absolute;top:-80px;right:-60px;width:500px;height:500px;
      border-radius:50%;
      background:radial-gradient(circle,rgba(255,210,63,.12),transparent 70%)">
    </div>
    
    <!-- 标题 -->
    <h1 style="position:absolute;top:50%;left:50%;transform:translate(-50%,-55%);
      font-size:72px;font-weight:900;color:#fff">
      保卫小鸡
    </h1>
    
    <!-- 更多内容... -->
  </div>
</body>
</html>
```

**HTML 约束清单**：

| ✅ 允许 | ❌ 禁止（dom-to-pptx 不支持） |
|--------|------------------------------|
| 内联 `style="..."` | `::before` / `::after` 伪元素 |
| 绝对定位 `position:absolute` | `conic-gradient` |
| px 单位 | CSS `border` 三角形 |
| `linear-gradient` / `radial-gradient` | `background-image`（改用 `<img>`） |
| `border-radius` / `box-shadow` | 外部 CSS 文件 |
| `<img>` 标签 | `<canvas>` |
| 内联 `<svg>` | JavaScript 动态内容 |
| `@import` Google Fonts | — |

---

### Step 2 — 合并为 combined.html

用 Python 脚本把多页 HTML 合并为单文件：

```python
# merge_slides.py
import os, re

PAGES_DIR = "./slides"
OUTPUT = "./combined.html"

pages = sorted([f for f in os.listdir(PAGES_DIR) if f.endswith('.html')])

with open(OUTPUT, 'w', encoding='utf-8') as out:
    out.write('''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
  @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700;900&display=swap');
  body { margin:0; padding:0; background:#111; }
  .slide { width:1920px; height:1080px; margin:0 auto 20px; position:relative; overflow:hidden; }
</style>
</head>
<body>
''')
    
    for fname in pages:
        with open(f"{PAGES_DIR}/{fname}", 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 提取 .slide-container 内部内容
        start = content.find('<div class="slide-container"')
        end = content.rfind('</div>')
        inner = content[start:end]
        
        # 修复资源路径 ../assets/ → ./assets/
        inner = inner.replace('../assets/', './assets/')
        
        out.write(f'<div class="slide">{inner}</div>\n')
    
    out.write('''
<script src="./dom-to-pptx.bundle.js"></script>
<script>
  window.exportToPptx();
</script>
</body>
</html>
''')

print(f"Merged {len(pages)} slides → {OUTPUT}")
```

---

### Step 3 — Playwright 导出脚本

```javascript
// export.js
const { chromium } = require('playwright');
const path = require('path');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 }
  });
  const page = await context.newPage();
  
  // 加载合并后的 HTML
  const filePath = path.resolve(__dirname, 'combined.html');
  await page.goto(`file://${filePath}`);
  
  // 等待 dom-to-pptx 初始化完毕
  await page.waitForFunction(
    () => typeof window.exportToPptx === 'function',
    { timeout: 30000 }
  );
  
  console.log('dom-to-pptx 就绪，开始导出...');
  
  // 拦截浏览器下载事件
  const [download] = await Promise.all([
    page.waitForEvent('download', { timeout: 120000 }),
    page.evaluate(() => window.exportToPptx())
  ]);
  
  const outputPath = path.resolve(__dirname, 'output.pptx');
  await download.saveAs(outputPath);
  
  console.log(`✅ 导出完成: ${outputPath}`);
  console.log(`   文件大小: ${(await require('fs').statSync(outputPath)).size / 1024 / 1024} MB`);
  
  await browser.close();
})();
```

**运行**：

```bash
npm install playwright dom-to-pptx
npx playwright install chromium
node export.js
```

---

### Step 4 — 部署交付

```bash
# 复制到文件站
cp output.pptx ~/projects/files/dist/L1/L1-11保卫小鸡/

# 重新生成文件列表 + 部署
cd ~/projects/files
python3 generate.py
bash deploy.sh
```

部署后即可通过文件站访问：
- 主页：https://files-f0v.pages.dev/
- 直接链接：https://files-f0v.pages.dev/L1/L1-11保卫小鸡/L1-11保卫小鸡.pptx

---

## 三、dom-to-pptx 核心原理

```
浏览器 DOM 树
    ↓ 递归遍历
每个 DOM 节点 → 分类处理：
    ├── <div>       → PPTX 矩形形状 (p:sp)
    │   ├── background      → 形状填充 (a:solidFill)
    │   ├── border          → 形状边框 (a:ln)
    │   ├── border-radius   → 圆角 (a:prstGeom)
    │   └── box-shadow      → 阴影效果 (a:effectLst)
    ├── <img>       → PPTX 图片 (p:pic)
    │   └── src             → 内嵌图片数据
    ├── <span>/<p>  → PPTX 文本框 (p:txBody)
    │   ├── font-family     → 字体引用
    │   ├── font-size       → 字号
    │   ├── color           → 文字颜色
    │   └── text-align      → 对齐方式
    └── <svg>       → PPTX 矢量图形
        └── path/polygon    → 自定义形状
```

**关键能力**：

1. **字体自动嵌入** — 检测 CSS `@import` / `font-family` 中的字体 URL，下载并嵌入 PPTX
2. **图片内嵌** — `<img>` 的图片数据直接写入 PPTX 的 `media/` 目录
3. **1920→标准比例** — 自动缩放 1920×1080 到 16:9 标准 PPTX 尺寸
4. **原生可编辑** — 输出的是真实 PPTX 形状/文本框，不是截图，Microsoft PowerPoint / WPS 中可直接编辑每个元素

---

## 四、关键坑位与解决方案

| # | 问题 | 原因 | 解决 |
|---|------|------|------|
| 1 | **不能直接 Node.js 调用** | dom-to-pptx 是浏览器端库，依赖 DOM API | 必须用 Playwright + Chromium 驱动 |
| 2 | **字体在其他电脑丢失** | 目标电脑没有设计字体 | Google Fonts `@import`，dom-to-pptx 自动嵌入 |
| 3 | **缺失图片不阻塞导出** | `<img src>` 404 | 不影响导出，但尽量提供占位图让 PPTX 有视觉参考 |
| 4 | **css background-image 消失** | dom-to-pptx 不处理背景图 | 改用 `<img>` 标签绝对定位替代 |
| 5 | **伪元素消失** | `::before`/`::after` 不是真实 DOM 节点 | 改用真实 `<div>` 元素 |
| 6 | **conic-gradient 报错** | dom-to-pptx 不支持圆锥渐变 | 用 `radial-gradient` 或 `linear-gradient` 替代 |
| 7 | **CSS border 三角形消失** | 原理是 border hack，不是形状 | 用内联 SVG `<polygon>` 替代 |
| 8 | **导出触发浏览器下载** | `exportToPptx()` 在浏览器中触发 download 事件 | Playwright `page.waitForEvent('download')` 拦截 |
| 9 | **容器网络限制** | 本环境无法直连外部服务器 SSH/SCP | 用 Cloudflare Pages 文件站交付 |
| 10 | **批量页面色差** | 不同页面背景色不一致 | 合并时统一检查 + 全局 style.json 约束 |

---

## 五、环境依赖

```bash
# Node.js 依赖
npm install playwright dom-to-pptx
npx playwright install chromium

# Python 依赖（辅助）
pip install python-pptx Pillow

# Cloudflare Pages CLI（部署用）
npm install -g wrangler
```

---

## 六、实战数据

以 L1-1 海底世界 28 页为例：

| 阶段 | 耗时 |
|------|------|
| HTML 逐页设计 | 2-3 小时 |
| 合并 + 导出 PPTX | 2-5 分钟 |
| 部署到文件站 | 30 秒 |
| PPTX 文件大小 | 6-8 MB |

L1 全 11 课总计：268 页 HTML + 11 个 PPTX。

---

## 七、备选方案对比

| 方案 | 优点 | 缺点 | 适用 |
|------|------|------|------|
| **dom-to-pptx**（当前） | 原生可编辑、高保真、字体嵌入 | 需 Playwright、有 CSS 限制 | 视觉丰富的课件 |
| python-pptx 直接生成 | 纯 Python、代码可控 | 丢渐变/阴影/圆角，质量差 | 纯文字报告 |
| PptxGenJS (5.4k⭐) | JS 代码生成，Node 友好 | 排版不如 HTML 灵活 | 数据图表 PPT |
| Marp CLI (3.5k⭐) | Markdown → PPTX，极简 | 缺视觉设计能力 | 内部技术分享 |
| Slidev (46.6k⭐) | Markdown + Vue 组件 | 学习成本高，偏开发者 | 技术演讲 |

---

## 八、当前可用状态

| 组件 | 状态 | 备注 |
|------|------|------|
| dom-to-pptx | ⚠️ 需安装 | `npm install dom-to-pptx` |
| Playwright | ⚠️ 需安装 | `npm install playwright` + `npx playwright install chromium` |
| python-pptx | ✅ 1.0.2 已安装 | 辅助用 |
| Pillow | ✅ 12.2.0 已安装 | 占位图 |
| 历史合并/导出脚本 | 📁 `_archive/packager/src/` | 可复用 |
| 文件站 | ✅ 运行中 | https://files-f0v.pages.dev/ |

---

> 下一步：安装 dom-to-pptx + Playwright 后即可恢复完整导出管线。