When converting HTML to PDF, first determine what the user really wants:
- Highest visual fidelity: The PDF should look as close as possible to the rendered browser screen.
- Selectable text: Text in the PDF can be copied, searched, and embedded as a text layer.
- Formal print document: Accepts print CSS semantics, pursuing stable pagination/long pages and smaller file size.
Must first give the user selection advice
By default, do not directly execute the conversion. First read the HTML, determine the page type, then give the user brief selection advice and wait for the user to choose. Only when the user has clearly specified a strategy in the same sentence and asked to "execute directly/without asking" may you skip the selection confirmation.
Suggested wording:
我看这个 HTML 更像【翻页演示/连续长页】。如果目标是尽量还原浏览器里的原始画面,我建议选 1。
- Screenshot-level fidelity (recommended): Most like the browser screen, preserves canvas/WebGL/backdrop-filter/complex CSS; text is not selectable, file is larger.
- Screen vector attempt: Text is selectable, uses screen media to print PDF; visuals are usually close to the browser, but complex filters/pagination may still differ.
- Print vector long pages: Text is selectable, smaller size, suitable for reports/articles; enters the print pipeline, not guaranteed to fully match the browser screen.
Please reply 1/2/3 or the strategy name, and I will execute.
如果用户的目标已经明确,也要先把它转化为推荐项给用户确认:
- “尽量还原浏览器渲染后的样子 / 原样 / 截图级 / 视觉保真” -> 推荐 **策略 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
Optional parameters:
| Parameter | Default | Description |
|---|---|---|
--width |
1280 | Browser viewport width px |
--height |
720 | Browser viewport height px |
--dpr |
2 | Screenshot device pixel ratio; increase to 3 when text details are not sharp enough |
--format |
png | Screenshot format; use png for lots of text/lines, jpeg for lots of photos when file size must be controlled |
--quality |
92 | JPEG quality, ignored for PNG |
--selector |
Auto | Specify slide selector, for example .slide / [data-slide] |
--ready-expr |
— | Custom JS ready expression, for example window.__PDF_READY === true |
Notes:
slides-rasterwill temporarily set the current slide to active/fixed for each page and hide the other slides.- This isolation only occurs in a temporary Chrome session and will not modify the source HTML file.
- If the page relies on a global background canvas, it will usually still be preserved; if the background exists only within other slides, you need to put the background into each slide or switch to the page's own export state.
1B. Continuous long page -> single-page screenshot 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
Notes:
- The script measures according to the browser screen layout
scrollWidth/scrollHeight。 - Very long pages are captured in chunks, then inserted into the same PDF long page, avoiding failure from a single oversized PNG.
- If the file is too large, you can switch to
--format jpeg --quality 92。
Strategy 2: CDP screen-media vector PDF
Used for scenarios where "text must be selectable, but you also want to stay as close as possible to screen rendering." The script will set Emulation.setEmulatedMedia(media="screen"), measure the page size according to the screen layout, then call 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
Note:
- This is a "vector attempt", not screenshot-level fidelity.
- Text is usually selectable.
- It still goes through the Chrome PDF output pipeline; complex filters, 3D transforms, and some canvas/WebGL may differ from the screen.
- Not as stable as
slides-rasterfor horizontal transform-based slide decks.
Strategy 3: print vector continuous long-page PDF
Suitable for reports, articles, formal documents, and outputs that need a text layer, smaller file size, or stable long pages. This route uses onepage-pdf's "oversized page rendering -> measured cropping" method and enters print media; it is not guaranteed to match the browser screen exactly.
3.1 Check for common print differences in the HTML
Read the HTML and check:
- Responsive breakpoints: whether there is
@media (max-width: N)and N >= 741. Print MQ triggers at about 741px, which may break the desktop layout. - Frosted glass: whether there is
backdrop-filter. It may be silently lost in Chrome PDF output and needs to be downgraded to an opaque background. - vh/vw: whether there is
100vh/100vw. During printing,vhis parsed as an oversized page, which may blow up the height. - Scroll entrance animations:
.fade*/.reveal*/.animate*/ AOS etc. may remain in an invisible state.
3.2 Generate fix CSS
Write the discovered fix rules to a temporary file, for example /tmp/onepage-fixes.css. See references/css-fixes.md。
3.3 Run the conversion
python3 ~/.claude/skills/html-to-pdf/scripts/onepage_pdf.py "{input.html}" \
-o "{output.pdf}" \
--width 1280 \
--extra-css /tmp/onepage-fixes.css
Common parameters:
| Parameter | Default | Description |
|---|---|---|
--width |
1280 | Page design width in px, automatically aligned to 8px |
--bedrock |
18000 | Initial oversized page height; automatically doubles on overflow |
--padding |
32 | Bottom whitespace of content in px |
--crop |
vector | Content detection: vector or pixel |
--virtual-time |
10000 | Virtual time budget in ms for JS-driven pages |
--replace |
— | Text replacement JSON [["旧","新"],...] |
--forbid |
— | Leak detection word list; abort if still present in the PDF |
Verification requirements
After the conversion completes, you must verify; do not just look at whether the command succeeded.
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()
Key checks:
- Whether the page count matches expectations.
- Whether the first screen/first 3 pages match the browser screenshot.
- No bottom truncation, blank pages, text misalignment, or missing images.
- Under the screenshot strategy, unselectable text is the expected result.
- Under the vector strategy, if visual differences are obvious, ask the user whether to switch to screenshot-level fidelity.
Troubleshooting quick reference
| Symptom | Common strategy | Cause | Solution |
|---|---|---|---|
| Visual inconsistency under the requirement that the browser be reproduced as-is | screen-vector / print-vector | The PDF pipeline is not equal to screen rendering | Switch to slides-raster or long-raster |
| Text edges look soft | raster | DPR too low or JPEG compression | --dpr 2/3 --format png |
| File too large | raster | PNG + high DPR | Switch photo-heavy pages to --format jpeg --quality 92 |
| Not all slides recognized | slides-raster | Automatic selector misjudgment | Add --selector ".slide" or [data-slide] |
| Async charts/fonts missing | raster/vector | Screenshot taken too early | Add --ready-expr or increase --delay |
| Horizontal deck captures the wrong page | slides-raster | The selector mistakenly selects the container, or slides are not independent pages | Add an explicit --selector; if necessary, make each page use an independent .slide |
| Layout collapse/grid becomes a single column | print-vector | Print MQ triggers at about 741px | extra CSS locks desktop layout |
| Frosted glass lost | print-vector / screen-vector | Chrome PDF output limitation | Screenshot strategy; or print fallback background |
| Bottom truncated | onepage | bedrock insufficient or cropping misjudged | Increase --bedrock, or --crop pixel |
For more mechanism explanations, see references/mechanics.md。
File structure
~/.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