# PDF 输出(可选)
把交付的单页 HTML(`article/article.html`)转成 PDF。**这是 Phase 8 Delivery 里的可选步骤**,
由 Checkpoint 3 用户选 "通过 · 同时导出 HTML + PDF" 时触发;不选则不动。
> HTML 仍然是主交付物:它能离线打开、可分享、可在浏览器里完整体验 Raw 交互。PDF 是给"需要
> 归档 / 打印 / 邮件附件 / 不联网阅读"场景的补充,**Raw 交互在 PDF 里只能渲染为初始态**。
---
## 快速用法
```bash
# 工作区根目录
npm run html # 先确保有 article/article.html
bash /scripts/html-to-pdf.sh # 默认 article/article.html → article/article.pdf
bash /scripts/html-to-pdf.sh in.html out.pdf # 自定义路径
```
也可以把这个 bash 调用放到工作区 `package.json` 的 scripts 里,让用户 `npm run pdf` 就跑
(路径是用户机器上 Skill 的绝对路径,**不在脚手架模板里固化**,避免硬编码)。
---
## 前提条件
本机已装 chromium-family 浏览器之一(脚本会自动探测,按下列顺序):
```text
chromium / chromium-browser / google-chrome / google-chrome-stable / chrome
brave-browser / microsoft-edge
/Applications/Google Chrome.app/...
/Applications/Chromium.app/...
/Applications/Microsoft Edge.app/...
/Applications/Brave Browser.app/...
/Applications/Arc.app/...
/usr/bin/chromium / /snap/bin/chromium
```
**找不到任何浏览器** → 脚本不会爆,会给出回退指引:注入了打印 CSS 的临时 HTML 路径会被
打印出来,用户可以用任意浏览器手动 `Cmd+P` / `Ctrl+P` → "另存为 PDF"。
不依赖 Node / npm 包 / puppeteer / playwright / weasyprint —— 故意只用系统已装的浏览器,
零环境配置。
---
## 设计原理(重要 · 调样式前先读)
### 1. 为什么要注入 print CSS(不是改 reacticle)
reacticle 的 TOC 在桌面是**左右栅格**(`.ra-article-layout--with-toc` 是 `display: grid`,
两列:TOC | 文章),在 mobile(≤999px)才塌成单列 `display: block`。
PDF 阅读习惯是**上下排布**(TOC 先 / 正文后),跟 mobile 体验对齐。最干净的实现:在 PDF
生成时**注入一段 `@media print` CSS**,强制 `display: block`,等价于复用 mobile 分支。
这样:
- **不动 reacticle**:任何版本的 reacticle 都能用这个脚本生成合理 PDF。
- **不动用户的 Article.tsx**:用户的源码完全不受影响。
- **CSS 只在打印生效**:浏览器里看 HTML 还是左右栅格。
### 2. 注入了哪几条规则
CSS 抽到了独立文件 **`scripts/pdf-print-overrides.css`**(与脚本同目录),分三组:
```text
A · TOC 排版(让 TOC 上 / 正文下)
A1) .ra-article-layout--with-toc { display: block } → 塌成单列
A2) .ra-toc { position: static; page-break-after: always } → 解 sticky + 独占首页
A3) .ra-toc__list { column-count: 2 } → 长 TOC 双列省纸
A4) .ra-toc__item { break-inside: avoid } → TOC 项不被列间撕开
A5) .ra-article-layout--with-toc > .ra-article → 正文在 TOC 后自然流
A6) .ra-toc a / .ra-article a { text-decoration: none } → 关闭打印链接下划线
B · 分页行为(修长文章的大块空白页)
B1) .ra-section / .ra-subsection { break-inside: auto !important }
→ 撤销 reacticle print.css 里的 break-inside: avoid-page。
对长 Section 来说,那条规则会把整节推到下一页、留前一页大半空白。
B2) h1-h4 + .ra-section__head + .ra-subsection__head { break-after: avoid }
→ 标题不被孤儿化(不会孤零零卡在页底,下面是空白)
B3) .ra-hero / .ra-lead / .ra-conclusion { break-inside: avoid }
→ 短的开场 / 收束块原子化,不撕开
B4) p / li / blockquote { orphans: 3; widows: 3 }
→ 段落不留 1-2 行寡行
B5) figure / .ra-table / .ra-codeblock / .ra-formula / .ra-image / .ra-raw
{ break-inside: avoid }
→ 图表、表格、代码块、Raw 块尽量整块;过高时浏览器仍会自动 fallback 分页
C · 封面(若文章有 3:4 封面,让它独占 PDF 首页)
C1) .ra-cover { break-after: always; break-inside: avoid }
→ 屏幕和 PDF 都保持 3:4 书封;打印时 TOC 从第二页开始
→ 详见 references/cover.md
```
完整带注释的 CSS 直接看 `scripts/pdf-print-overrides.css`。要调样式(比如换页面 break 策略
/ 改双列阈值 / 加打印水印),**直接改这个文件就行**,不用动 bash 脚本。
> **为什么 CSS 是独立文件**:macOS 自带 BSD awk 对 `-v inject="$INLINE_CSS"` 这种多行字符串
> 报 `newline in string` 错(GNU awk 没事)。把 CSS 抽成文件,awk 用 `getline < file` 读取,
> 两个 awk 都吃得下。顺带也让 CSS 变成可独立编辑 / lint / diff 的真正资源。
reacticle 自带的 `print.css` 仍然生效(白底黑字 / 隐藏 export bar / `break-inside: avoid`
等),本脚本只补 TOC 排版那一块。
### 3. 渲染流程
```
article.html ──── awk 注入 print CSS ──── /tmp/article-print.html
│
▼
探测的浏览器 --headless --print-to-pdf
│
▼
article.pdf
```
Chrome 标志:
- `--headless=new`(旧版 fallback 到 `--headless`):无窗口模式。
- `--no-pdf-header-footer` / `--print-to-pdf-no-header`:去掉浏览器自带的 URL / 日期 / 页码
(颠倒了 colophon 的角色)。
- `--virtual-time-budget=5000`:给页面 JS 5 秒初始化时间,让 Raw 组件、KaTeX、Prism 等
渲染完再截屏。
- `--hide-scrollbars` / `--disable-gpu` / `--no-sandbox`:清洁渲染。
### 4. Raw 交互在 PDF 里会怎样?
PDF 是**静态文档**,所有 Raw 交互(滑块、按钮、动画、canvas、视频)只能渲染**初始状态**。
设计建议:
- **interactive-explainer 类型**:PDF 价值有限(核心是"操作"),用户可在 Plan 阶段不开
PDF 导出。
- **其它类型**:Raw 通常是辅助图解(流程图 / SVG / 趋势图),初始态已经够看,PDF 没问题。
- **Raw 内的内容若依赖 hover / click 才显示**:在写 Raw 时考虑"是否打印友好",如默认露出
关键内容、用 `print:` 风格让 hover 状态在打印时强制展开。
---
## 故障排除
### 找不到浏览器
脚本会打印临时 HTML 路径(已经注入打印 CSS),手动 Cmd+P 即可。
### PDF 里 TOC 没分页 / 跟正文挤在一起
某些 Chromium 旧版本对 `page-break-after: always` 的支持有差异。可以:
- 升级 Chrome 到当前主版本。
- 或手动 Cmd+P 时在打印对话框选"两面 / 缩放 / 自定义边距"。
### PDF 分页奇怪 / 出现大块空白页 / 标题孤零零卡在页底
通常是某些"原子块"被强制不分页导致整块被推到下一页。检查 `pdf-print-overrides.css`:
- **B1** 是否生效(看 DevTools Print Preview 里 `.ra-section` 的 `break-inside` 值)。
reacticle `print.css` 那条没 `!important`,理论上我们的 `!important` 一定能赢。
- 自己写的 **Raw 块**里有没有 inline `style={{ breakInside: 'avoid' }}` 或类似 CSS —— 删掉。
- 如果还想"标题永远不卡页底",把 B2 里 `break-after: avoid` 改成
`break-before: avoid; break-after: avoid`(更激进,但偶尔会牺牲一些纸面利用率)。
### PDF 里 Raw 没渲染完整 / 图表是空白
- 增加 `--virtual-time-budget`(编辑脚本,从 5000 改到 10000+)。
- 检查 Raw 的 JS 是否在 DOMContentLoaded 内同步渲染(异步加载的远程图片 / 数据可能没等到
截屏就开拍)。
- 改造 Raw:用 SSR-friendly 的初始态 + 客户端 hydrate 增强,不要"完全靠 JS 后才能看"。
### PDF 字体跟浏览器看不一样
主题用了 `@font-face` 远程字体?headless Chrome 默认不等远程字体加载完。可以:
- 用 system font fallback(推荐,多数主题已经做了)。
- 或在 HTML 里把字体 `data:` 内联(vite-plugin-singlefile 已经处理静态资源,但 woff 字体
要看主题怎么写的)。
### 想自定义页面留白 / 纸张大小
Chromium 的 `--print-to-pdf` 不支持命令行页面尺寸 / 边距参数。当前 CSS 用
`@page { margin: 0 }` 让主题纸色铺满整页,再用 `.ra-root { padding: 0.45in }`
提供内容留白。如果需要改留白,优先调整 `scripts/pdf-print-overrides.css` 里的
`.ra-root` print padding。
如果用户真的要自定义,建议:
1. 用浏览器 GUI 打印(Cmd+P)—— 那里有边距 / 纸张 / 缩放选项。
2. 或者派生一个 `html-to-pdf-puppeteer.sh` 脚本,单独支持自定义。当前脚本默认按 Chrome
默认页面尺寸(Letter 美区 / A4 其它区)输出。
---
## 与 Skill 流程的关系
| 阶段 | 触发 | 动作 |
|---|---|---|
| Phase 8 Delivery | 用户在 Checkpoint 3 选 "通过 · 同时导出 HTML + PDF" | 跑 `npm run html` → 跑 `bash /scripts/html-to-pdf.sh` → 交付 `article.html` + `article.pdf` |
| Checkpoint 3 其它选项 | 用户选 "通过 · 导出 HTML 交付" | 不跑 PDF |
| 用户事后想补 PDF | 任何时刻 | 在工作区根目录手动跑 `bash /scripts/html-to-pdf.sh` |
---
## 不做的事
- ❌ 不在脚手架强行装任何 PDF 相关 npm 包(保持脚手架轻量)。
- ❌ 不在 Checkpoint 3 默认勾选 "导出 PDF"(PDF 不是主交付物)。
- ❌ 不替用户判断"要不要 PDF" —— 这是 Checkpoint 3 用户独立选择项。
- ❌ 不改 reacticle 来支持 PDF(CSS 注入更轻量、跟 reacticle 版本解耦)。