# HTML To PDF

> 将 HTML 网页/演示文稿转换为高质量 PDF。优先按用户目标选择策略： - 浏览器原样/高度还原/截图级保真 -> CDP 屏幕截图 PDF - 需要文字可复制且尽量接近屏幕 -> CDP screen-media 向量 PDF - 需要正式文档、文本层、较小体积、连续长页 -> Chrome print 向量长页 PDF 适用场景：用户说"导出PDF""打印成PDF""HTML转PDF""生成PDF版" "把这个PPT转成PDF""导出为PDF分享""打印网页""不分页PDF""网页转PDF" "转成PDF文件""生成长图PDF""多页PDF""单页PDF""高度还原浏览器效果"。 触发词：导出PDF、转PDF、HTML转PDF、打印成PDF、生成PDF、网页转PDF、 长图PDF、不分页PDF、多页PDF、浏览器原样、高度还原。

- Skill: `cslawyer1985/html-to-pdf` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add cslawyer1985/html-to-pdf`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cslawyer1985/html-to-pdf/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: cslawyer1985 (https://skillmd.com/u/cslawyer1985)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/cslawyer1985/html-to-pdf

---


# HTML to PDF — 屏幕保真/向量文本双路线

将 HTML 转换为 PDF 时，先判断用户真正要的是：

- **视觉最高还原**：PDF 看起来尽量等同于浏览器渲染后的画面。
- **文本可选中**：PDF 中的文字可以复制、检索、嵌入文本层。
- **正式打印文档**：接受 print CSS 语义，追求稳定分页/长页和较小体积。

## 必须先给用户选择建议

默认不要直接执行转换。先读取 HTML，判断页面类型，然后向用户给出简短选择建议，并等待用户选择。只有当用户在同一句中已经明确指定策略并要求“直接执行/不用问”时，才可以跳过选择确认。

建议话术：

```text
我看这个 HTML 更像【翻页演示/连续长页】。如果目标是尽量还原浏览器里的原始画面，我建议选 1。

1. 屏幕截图级保真（推荐）：最像浏览器画面，保留 canvas/WebGL/backdrop-filter/复杂 CSS；文字不可选中，文件较大。
2. 屏幕向量尝试：文字可选中，使用 screen media 打 PDF；视觉通常接近浏览器，但复杂滤镜/分页仍可能有差异。
3. 打印向量长页：文字可选中、体积较小、适合报告/文章；会进入 print pipeline，不保证和浏览器屏幕完全一致。

请回复 1/2/3 或策略名，我再执行。
```

如果用户的目标已经明确，也要先把它转化为推荐项给用户确认：

- “尽量还原浏览器渲染后的样子 / 原样 / 截图级 / 视觉保真” -> 推荐 **策略 1**。
- “文字可复制 / 可检索 / 文本层 / 不要截图” -> 推荐 **策略 2**，并说明失败或差异明显时可退到 **策略 3**。
- “长图 PDF / 不分页 / 正式报告 PDF / 体积小” -> 推荐 **策略 3**；若同时强调视觉原样，推荐 **策略 1B**。

## Step 1: 读取 HTML，判断页面类型

读取 HTML 文件并检查：

**翻页演示信号**（命中任一即可认为是 slides）：
- `scroll-snap-type` 存在。
- ≥2 个 `.slide` / `[class*="slide"]` / `[data-slide]` / `<section>` / `.page`。
- 大量元素尺寸接近 `100vw` x `100vh`。
- `<title>` / `<meta>` 含 slide、PPT、演示、presentation 等关键词。
- Reveal.js、Impress.js 等演示框架标记。

**连续长页信号**：
- 没有明确 slide 容器。
- `<article>` / `<main>` / `.content` / 文档流内容连续向下。
- 页面高度显著大于视口高度。

识别后先给用户推荐策略，不要擅自执行。

## 策略 1: CDP 屏幕截图级保真

这是“尽量还原浏览器里看到的原始样子”的默认推荐路线。它不走 print media，而是在 Chrome 屏幕模式下等字体/图片加载完成后截图，再拼成 PDF。翻页演示会临时隔离当前 slide 到视口左上角，避免横向 deck、scroll-snap 或 transform 状态导致重复截第一页。

优点：
- 最接近浏览器渲染结果。
- 保留 canvas、WebGL、backdrop-filter、CSS filter、复杂背景、真实屏幕布局。
- 不需要为 print media 重写大量 CSS。

缺点：
- 文字不可选中。
- 文件通常更大。
- 超长页面可能超过旧 Acrobat 的 14400pt 兼容限制。

### 1A. 翻页演示 -> 多页截图 PDF

```bash
python3 ~/.claude/skills/html-to-pdf/scripts/render_slides_cdp.py "{input.html}" \
  -o "{output.pdf}" \
  --strategy slides-raster \
  --width 1280 --height 720 \
  --dpr 2 \
  --format png
```

可选参数：

| 参数 | 默认 | 说明 |
|------|------|------|
| `--width` | 1280 | 浏览器视口宽度 px |
| `--height` | 720 | 浏览器视口高度 px |
| `--dpr` | 2 | 截图设备像素比；文字细节不够锐时升到 3 |
| `--format` | png | 截图格式；文字/线条多用 png，照片多且需控体积可用 jpeg |
| `--quality` | 92 | JPEG 质量，PNG 忽略 |
| `--selector` | 自动 | 指定 slide 选择器，例如 `.slide` / `[data-slide]` |
| `--ready-expr` | — | 自定义 JS 就绪表达式，例如 `window.__PDF_READY === true` |

说明：
- `slides-raster` 会为每一页临时设置当前 slide 为 active/fixed，并隐藏其他 slide。
- 这种隔离只发生在临时 Chrome 会话中，不会修改源 HTML 文件。
- 如果页面依赖全局背景 canvas，它通常仍会保留；如果背景只存在于其他 slide 内，需要把背景放入每个 slide 或改用页面自己的导出状态。

### 1B. 连续长页 -> 单页截图 PDF

```bash
python3 ~/.claude/skills/html-to-pdf/scripts/render_slides_cdp.py "{input.html}" \
  -o "{output.pdf}" \
  --strategy long-raster \
  --width 1280 --height 900 \
  --dpr 2 \
  --format png \
  --tile-height 4096
```

说明：
- 脚本会按浏览器屏幕布局测量 `scrollWidth/scrollHeight`。
- 超长页面会分块截图，再插入同一张 PDF 长页，避免单张超大 PNG 失败。
- 如果文件过大，可改用 `--format jpeg --quality 92`。

## 策略 2: CDP screen-media 向量 PDF

用于“文字要可选中，但又希望尽量接近屏幕渲染”的场景。脚本会设置 `Emulation.setEmulatedMedia(media="screen")`，按屏幕布局测量页面尺寸，再调用 `Page.printToPDF`。

```bash
python3 ~/.claude/skills/html-to-pdf/scripts/render_slides_cdp.py "{input.html}" \
  -o "{output.pdf}" \
  --strategy screen-vector \
  --width 1280 --height 900
```

注意：
- 这是“向量尝试”，不是截图级保真。
- 文本通常可选中。
- 仍会经过 Chrome PDF 输出管线，复杂滤镜、3D transform、部分 canvas/WebGL 可能与屏幕不同。
- 对横向 transform 型 slide deck 不如 `slides-raster` 稳。

## 策略 3: print 向量连续长页 PDF

适合报告、文章、正式文档、需要文本层/较小体积/稳定长页的输出。该路线使用 onepage-pdf 的“超大页面渲染 -> 实测裁剪”方法，进入 print media，不保证与浏览器屏幕完全一致。

### 3.1 检查 HTML 中的常见 print 差异

读取 HTML，检查：

1. **响应式断点**：是否有 `@media (max-width: N)` 且 N >= 741。打印 MQ 在约 741px 触发，可能破坏桌面布局。
2. **毛玻璃**：是否有 `backdrop-filter`。Chrome PDF 输出中可能静默丢失，需降级为不透明背景。
3. **vh/vw**：是否有 `100vh` / `100vw`。打印中 `vh` 会按超大页面解析，可能拉爆高度。
4. **滚动入场动画**：`.fade*` / `.reveal*` / `.animate*` / AOS 等可能停在不可见状态。

### 3.2 生成修复 CSS

将发现的修复规则写入临时文件，例如 `/tmp/onepage-fixes.css`。详见 `references/css-fixes.md`。

### 3.3 执行转换

```bash
python3 ~/.claude/skills/html-to-pdf/scripts/onepage_pdf.py "{input.html}" \
  -o "{output.pdf}" \
  --width 1280 \
  --extra-css /tmp/onepage-fixes.css
```

常用参数：

| 参数 | 默认 | 说明 |
|------|------|------|
| `--width` | 1280 | 页面设计宽度 px，自动对齐 8px |
| `--bedrock` | 18000 | 初始超大页面高度；溢出自动翻倍 |
| `--padding` | 32 | 内容底部留白 px |
| `--crop` | vector | 内容检测：vector 或 pixel |
| `--virtual-time` | 10000 | JS 驱动页面的虚拟时间预算 ms |
| `--replace` | — | 文本替换 JSON `[["旧","新"],...]` |
| `--forbid` | — | 泄露检测词表，PDF 中仍存在则中止 |

## 验证要求

转换完成后必须验证，不要只看命令成功。

```python
import pymupdf
doc = pymupdf.open("{output.pdf}")
print(f"页数: {doc.page_count}")
for i in range(min(3, doc.page_count)):
    r = doc[i].rect
    print(f"第{i+1}页: {r.width:.0f}x{r.height:.0f}pt")
    doc[i].get_pixmap(dpi=72).save(f"/tmp/pdf-check-{i+1}.png")
doc.close()
```

重点检查：
- 页面数量是否符合预期。
- 首屏/前 3 页是否和浏览器截图一致。
- 没有底部截断、空白页、文字错位、图片缺失。
- 截图策略下文字不可选中是预期结果。
- 向量策略下若视觉差异明显，询问用户是否改用截图级保真。

## 故障排查速查

| 症状 | 常见策略 | 原因 | 解决 |
|------|----------|------|------|
| 浏览器原样要求下视觉不一致 | screen-vector / print-vector | PDF 管线不等于屏幕渲染 | 改用 `slides-raster` 或 `long-raster` |
| 文字边缘发软 | raster | DPR 太低或 JPEG 压缩 | `--dpr 2/3 --format png` |
| 文件过大 | raster | PNG + DPR 高 | 照片型页面改 `--format jpeg --quality 92` |
| slide 没识别全 | slides-raster | 自动选择器误判 | 加 `--selector ".slide"` 或 `[data-slide]` |
| 异步图表/字体缺失 | raster/vector | 截图太早 | 加 `--ready-expr` 或增加 `--delay` |
| 横向 deck 截错页 | slides-raster | 选择器误选到容器或 slide 不是独立页 | 加明确 `--selector`，必要时让每页使用独立 `.slide` |
| 布局塌陷/网格变单列 | print-vector | 打印 MQ 约 741px 触发 | extra CSS 锁定桌面布局 |
| 毛玻璃丢失 | print-vector / screen-vector | Chrome PDF 输出限制 | 截图策略；或 print fallback 背景 |
| 底部截断 | onepage | bedrock 不够或裁剪误判 | 提高 `--bedrock`，或 `--crop pixel` |

更多机理说明见 `references/mechanics.md`。

## 文件结构

```text
~/.claude/skills/html-to-pdf/
├── SKILL.md
├── README.md
├── LICENSE
├── scripts/
│   ├── onepage_pdf.py          # print 向量长页
│   └── render_slides_cdp.py    # CDP 屏幕截图/屏幕向量
└── references/
    ├── css-fixes.md
    ├── mechanics.md
    └── examples.md
```

