首先要明确一个核心判断:真正能达到印刷级输出标准的 PDF 文档,并非通过浏览器“另存为”功能生成,而是依赖 WeasyPrint 或 Playwright 的 page.pdf() 这类专业工具生成的矢量文件。WeasyPrint 支持精确分页控制、字体嵌入以及 CSS Paged Media 标准,而浏览器另存为输出的仅是位图——放大后边缘模糊、无法搜索文字、样式也容易变形。这一前提必须提前说明清楚。

使用 page.pdf() 生成的 PDF 才能称为真正的“印刷级”矢量文档,而截图合成方式输出的仅是包含文字的图片,放大后模糊、无法检索内容、字体也可能丢失——这一关键差异必须事先说明。
为什么浏览器“另存为PDF”无法直接用于印刷生产
您是否尝试过用 Chrome 打印预览中的“保存为PDF”功能导出幻灯片?表面看似正常,但放大后文字边缘虚化、复制粘贴出现乱码、页眉页脚位置偏移,甚至部分 CSS Grid 布局完全坍塌。这并非 CSS 代码质量的问题,而是浏览器打印模块对 @media print 的支持仅停留在“尽力而为”层面,远未达到“精确执行”的标准。它会跳过许多现代 CSS 特性(如 contain: layout、scroll-snap、自定义字体的回退行为),并且无法保证在 JavaScript 渲染完成后才执行分页。
- 页面中使用了
Reveal.js或Slidev?它们依赖 JavaScript 控制当前页面状态,打印时很可能仅捕获第一页 - 通过
@font-face加载的思源黑体或 JetBrains Mono 字体?打印时很可能回退为宋体,且不会嵌入字形 - 设置了
break-inside: a void以防止代码块被截断?Chrome 打印引擎大概率会忽略此规则
Playwright 的 page.pdf() 如何规避这些PDF生成陷阱
Playwright 并非调用浏览器的“打印后端”,而是直接复用 Chromium 内置的 PDF 生成器(基于 Blink 内核),在 DOM 树和样式树完全稳定后,将 Layout Tree 转换为 PDF 指令——这才是真正的矢量输出方案。关键在于:您需要主动控制“何时才算渲染稳定”这一时机。
- 不要仅依赖
page.goto()完成,需额外添加await page.waitForFunction('document.readyState === "complete"') - 若页面包含异步图表(如 ECharts 或 Chart.js),需等待其渲染完成:
await page.waitForFunction(() => window.myChart?.getOption) - 对于
Reveal.js类幻灯片,需手动触发跳转至末页并停留:await page.evaluate(() => Reveal.slide(Reveal.getSlides().length - 1)),再等待动画结束 page.pdf()默认不会嵌入字体,必须显式传递参数:{ printBackground: true, format: 'A4', fontEmbedding: true }
WeasyPrint 适用于哪些PDF生成场景,常见问题有哪些
WeasyPrint 不依赖浏览器内核,采用纯 Python 渲染引擎,因此不具备 JavaScript 执行能力,但对静态 HTML 配合 CSS Paged Media 标准的支持精度超越任何浏览器——特别适合合同、发票、产品说明书等结构固定、无需交互的文档。
- 完整支持
@page、@page :first、string-set/string()等CSS特性,可实现自动目录与页眉联动 - 对于
position: fixed页眉,WeasyPrint 会将其重复渲染至每一页,而 Chrome 打印通常仅渲染一次 - 如果页面中包含
fetch()数据加载、localStorage配置读取、或IntersectionObserver触发的懒加载——WeasyPrint 会直接报错或输出空白 - 字体文件必须确保本地可访问,
url('./fonts/NotoSansCJK.ttc')路径需使用绝对路径或相对于 HTML 文件的路径,不支持网络 URL
多列排版、分页控制、页码生成等“印刷细节”如何正确实现
不要轻信网络上流传的 column-count: 3 方案,在打印时几乎全部失效。真正可靠的实现方式是 CSS Grid 配合 break-before 组合。
- 使用
display: grid; grid-template-columns: 1fr 1fr 1fr;替代column-count,打印引擎识别更稳定 - 强制分页不要仅依赖
page-break-after(已废弃),应改用break-after: page配合break-inside: a void - 页码应使用
counter-reset配合counter-increment实现,而非通过 JavaScript 插入数字——WeasyPrint 和page.pdf()均支持此方式 - 页眉中的当前章节名称,通过
string-set: title content(text);配合string(title)实现,这是 CSS Paged Media 标准规范,而非临时 hack
真正的复杂度不在于工具选型,而在于“何时触发 PDF 生成”这一时间节点——JavaScript 渲染完成、CSS 动画结束、第三方库准备就绪,三者必须全部满足,缺一不可。大多数失败案例的根源并非 PDF 库本身,而是等待逻辑中遗漏了某个关键环节。
