| name | html-to-pdf |
| description | 将 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、浏览器原样、高度还原。
|
HTML to PDF — 屏幕保真/向量文本双路线
将 HTML 转换为 PDF 时,先判断用户真正要的是:
- 视觉最高还原:PDF 看起来尽量等同于浏览器渲染后的画面。
- 文本可选中:PDF 中的文字可以复制、检索、嵌入文本层。
- 正式打印文档:接受 print CSS 语义,追求稳定分页/长页和较小体积。
必须先给用户选择建议
默认不要直接执行转换。先读取 HTML,判断页面类型,然后向用户给出简短选择建议,并等待用户选择。只有当用户在同一句中已经明确指定策略并要求“直接执行/不用问”时,才可以跳过选择确认。
建议话术:
我看这个 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
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
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。
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,检查:
- 响应式断点:是否有
@media (max-width: N) 且 N >= 741。打印 MQ 在约 741px 触发,可能破坏桌面布局。
- 毛玻璃:是否有
backdrop-filter。Chrome PDF 输出中可能静默丢失,需降级为不透明背景。
- vh/vw:是否有
100vh / 100vw。打印中 vh 会按超大页面解析,可能拉爆高度。
- 滚动入场动画:
.fade* / .reveal* / .animate* / AOS 等可能停在不可见状态。
3.2 生成修复 CSS
将发现的修复规则写入临时文件,例如 /tmp/onepage-fixes.css。详见 references/css-fixes.md。
3.3 执行转换
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 中仍存在则中止 |
验证要求
转换完成后必须验证,不要只看命令成功。
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。
文件结构
~/.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