用HTML画流程图、写说明文档,还要优雅地变成A4可打印PDF:一份真正能落地的工程实践指南
当我第一次尝试用浏览器“写书”时,最大的惊讶不是它能把图文并茂的文档变漂亮,而是它对纸张的敬畏:毫米级的边距、孤行寡行的控制、页码与页眉的精细排布……这些传统排版的讲究,其实都可以用HTML+CSS+SVG严谨地实现。本文将系统分享如何用HTML实现流程图与说明文档,并稳定转换为A4可打印的PDF,重点放在“自然分页”的工程化方法与实战坑点。
目录
- 为什么选择HTML/CSS/SVG来做纸质友好型文档
- 流程图的三条技术路线:从轻到重的选择
- 说明文档的结构化写作与语义标注
- A4分页的科学与艺术:CSS Paged Media要点
- 从HTML到PDF:工具链对比与选型建议
- 一条可复用的实战流水线(含关键代码片段)
- 常见坑点与排错清单
- 进阶玩法:目录页、交叉引用、水印与双栏
- 结语:把文档当工程做,评论区聊聊你的痛点
为什么是HTML/CSS/SVG?
作为写作者兼工程实践者,我偏爱“可计算”的排版。HTML/CSS/SVG有几大天然优势:
- 跨平台可预览:浏览器即所见即所得,避免Office/排版软件版本差异。
- 可维护:版本控制、组件化、自动化构建,像写代码一样管理文档。
- 矢量友好:用SVG绘制流程图,放大打印依旧锐利。
- 精确度:CSS的绝对单位(mm、cm、in、pt)在打印介质上是物理等值的,可直接定义A4边距与版心。
- 自动化:无头浏览器或专业排版引擎可批量出PDF,内建页眉页脚、页码、背景打印等。
对纸张最重要的一点,是“分页作为一等公民”。这意味着你需要理解CSS的分页模型,而不是把网页强行“截图”成PDF。
流程图的三条技术路线
流程图是说明文档里的“逻辑放大镜”。实现路径各有侧重,我的经验是按复杂度与可打印性分三档选择。
1) 轻量路线:语法式图表(如Mermaid)
- 特点:用简短语法写图,工具在浏览器中渲染成SVG。
- 适用:快速出图、频繁迭代、与Markdown配合。
- 打印要点:
- 渲染成SVG后再打印,避免位图糊边。
- 配置高对比主题,移除阴影与动画。
- 控制图宽不超过版心,必要时为整个图设“避免分页”。
2) 稳健路线:原生SVG手工/库辅助
- 特点:直接写SVG元素(rect、path、marker等),或用轻量库生成。
- 适用:需要完全可控的节点样式、箭头、对齐与标注。
- 打印要点:
- 使用marker-end定义可复用箭头;尽量避免滤镜与模糊。
- 颜色选用高对比、避免浅灰背景(很多打印默认不打背景)。
- 单个大图尽量放一页,实在过大时做“分幅”或附录。
3) 重装路线:图布局引擎(Graphviz、布局算法)
- 特点:通过DOT语言或API描述图,由引擎自动做层次布局与连线避免相交。
- 适用:复杂依赖图、多人协作、需要稳定布局。
- 打印要点:
- 使用正交连线(如splines=ortho)提高可读性。
- 控制rankdir(LR或TB)配合页面方向(横/竖)。
- 导出SVG嵌入文档,保持矢量。
示例(DOT片段,说明思路):
digraph G {
graph [rankdir=LR, splines=ortho, nodesep=0.4, ranksep=0.6];
node [shape=box, style=rounded, fontsize=12];
edge [arrowsize=0.7];
Start -> Plan -> Build -> Test -> Deploy;
Build -> Fix -> Build;
}
将其通过布局引擎生成SVG,再嵌入文档即可。
说明文档的结构化与语义标注
对打印友好的HTML文档,应尽可能语义化,方便分页引擎做正确决策。
- 标题层级:h1/h2/h3……,不要用div模拟标题。
- 段落与列表:p、ol/ul/li,避免用br堆砌。
- 图表与说明:figure + figcaption,图文绑定,便于“避免分页”。
- 代码:pre + code,设置可换行规则,避免超出版心。
- 引用:blockquote,保持版式风格一致。
- 元数据:title、meta、article/section、nav(目录)、footer(版权)等。
排版基础参数(纸张感受):
- 正文字号:10.5pt~12pt(中文),行高1.4~1.6。
- 版心宽度:A4常用左右边距各12–20mm,上下18–25mm。
- 字体:正文字体与代码字体分开设置;中文尽量选有完整字形的宋/黑系列。
A4分页的科学与艺术
核心在“CSS Paged Media(分页媒体)”与“Fragmentation(内容分割)”。
关键CSS要点(概念与兼容性建议):
-
页面尺寸与边距
- @page定义纸张与边距:A4(210mm × 297mm),可portrait/landscape。
- 用mm或in确保可打印的物理精度。
-
分页控制
- break-before/after: page(现代)与 page-break-before/after(兼容)。
- break-inside: avoid(现代)与 page-break-inside: avoid(兼容)。
- 对表格、图、标题-正文组合使用“避免分页”包裹容器。
-
孤行寡行
- orphans与widows用于控制段首单行、段末单行,但浏览器实现度不一,视工具链决定是否生效。
-
页眉页脚与页码
- 专业排版引擎支持@page margin boxes与running headers(string-set)。
- 无头浏览器通常通过生成时的模板参数实现页码(见后文)。
-
背景与颜色
- 浏览器默认不打印背景,需要在生成工具中启用printBackground或引导用户勾选。
- 使用高对比配色,尽量避免浅灰填充;黑色文本使用纯黑。
-
绝对单位
- 在打印介质上,CSS的mm、cm、in、pt都映射为物理长度;px按1in=96px换算。
从HTML到PDF:工具链对比
根据“功能完整度”“一致性”“成本/部署”三维来选。
-
浏览器手动打印
- 优点:零成本,所见即所得。
- 缺点:难以自动化,页眉页脚与背景需手工勾选,团队操作不一致。
-
无头浏览器(Puppeteer/Playwright)
- 优点:自动化强、支持现代CSS,页码模板灵活,CI友好。
- 缺点:对@page高级特性支持有限;需要自己处理字体与渲染时机。
-
wkhtmltopdf
- 优点:命令行简单、稳定。
- 缺点:基于旧WebKit,现代CSS支持不足;对复杂分页与中文排版不友好。
-
专业引擎(Prince、Antenna House 等)
- 优点:完整的CSS Paged Media与版面特性,目录、交叉引用、脚注都优雅。
- 缺点:商业授权成本;部署与学习曲线略高。
-
Paged.js / Vivliostyle(浏览器内分页多段落版引擎)
- 优点:接近标准的分页特性、支持running headers与target-counter等。
- 缺点:需要在浏览器中二次排版;对脚本/资源加载顺序更敏感。
经验之谈:团队工程化首选无头浏览器;要求深度分页特性时考虑专业引擎或Paged.js。
实战:一条可复用的流水线
以“含流程图的技术白皮书”为例,目标:A4、自然分页、自动页码、可重复构建。
步骤概览:
- 内容组织:用语义HTML或由Markdown/Pandoc生成HTML骨架。
- 图表生成:使用Mermaid/Graphviz预渲染为SVG并内嵌。
- 字体就绪:项目自带字体文件,@font-face内联,确保PDF可嵌入。
- 打印样式:独立print.css,设置@page与分页控制。
- 生成脚本:Puppeteer自动出PDF,启用背景与页码。
- 预检与回归:CI中对关键页面做像素/文本差异比对。
关键代码片段(展示思路,文件名自定):
- 打印样式(print.css)
/* 纸张与版心 */
@page {
size: A4 portrait; /* 或 A4 landscape */
margin: 18mm 14mm 18mm 14mm; /* 上右下左 */
}
/* 全局打印偏好 */
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
html, body {
color: #111;
font-family: "Noto Serif CJK", "Source Han Serif", serif; /* 替换为你的字体 */
font-size: 11pt;
line-height: 1.55;
}
/* 结构元素 */
h1, h2, h3 {
break-after: avoid;
page-break-after: avoid; /* 兼容旧实现 */
margin: 0 0 8pt 0;
}
p {
orphans: 3;
widows: 3;
margin: 0 0 10pt 0;
}
/* 图与表尽量整体出现在同页 */
figure, table, pre, blockquote {
break-inside: avoid;
page-break-inside: avoid;
margin: 10pt 0;
}
/* 代码块可换行,避免出血 */
pre {
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
background: #f6f7f8;
border: 0.5pt solid #ddd;
padding: 8pt;
}
/* 明确手动分页的类 */
.page-break {
break-before: page;
page-break-before: always;
}
/* 页眉/页脚留白(若用无头浏览器模板,则确保正文不溢出) */
body {
margin-top: 10mm;
margin-bottom: 12mm;
}
/* 流程图SVG的打印优化 */
svg {
max-width: 100%;
height: auto;
shape-rendering: geometricPrecision;
text-rendering: optimizeLegibility;
}
- 字体嵌入(fonts.css)
@font-face {
font-family: "Noto Serif CJK";
src: url("./fonts/NotoSerifCJK-Regular.otf") format("opentype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Noto Serif CJK";
src: url("./fonts/NotoSerifCJK-Bold.otf") format("opentype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Fira Code";
src: url("./fonts/FiraCode-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* 应用到正文与代码 */
body { font-family: "Noto Serif CJK", serif; }
code, pre { font-family: "Fira Code", monospace; }
提示:将所需字体文件放入项目并注意许可证,服务端(CI)也能找到这些字体,PDF才会正确嵌入。
- 使用无头浏览器生成PDF(Node脚本示例,Puppeteer)
import fs from "node:fs";
import path from "node:path";
import puppeteer from "puppeteer";
const htmlPath = path.resolve("./dist/index.html");
const pdfPath = path.resolve("./dist/output.pdf");
function headerTemplate(title) {
return `
<style>
section { font-size: 9pt; color: #666; width: 100%; padding: 0 10mm; }
.left { float:left } .right { float:right }
.clear { clear: both }
</style>
<section>
<span class="left">${title}</span>
<span class="right"></span>
<div class="clear"></div>
</section>`;
}
function footerTemplate() {
return `
<style>
section { font-size: 9pt; color: #666; width: 100%; padding: 0 10mm; }
.left { float:left } .right { float:right }
.clear { clear: both }
</style>
<section>
<span class="left">Page <span class="pageNumber"></span> / <span class="totalPages"></span></span>
<span class="right">© Your Org</span>
<div class="clear"></div>
</section>`;
}
(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
// 以文件方式加载,或使用本地静态服务器
await page.goto("file://" + htmlPath, { waitUntil: "networkidle0" });
// 等待字体与图表渲染完成(Mermaid/Graphviz前端渲染则需自定义ready信号)
await page.emulateMediaType("print");
await page.evaluate(() => document.fonts && document.fonts.ready);
// 若有前端渲染图表,约定window.renderReady = true
try {
await page.waitForFunction("window.renderReady === true", { timeout: 5000 });
} catch { /* 如果没有该变量则忽略 */ }
// PDF生成
await page.pdf({
path: pdfPath,
format: "A4",
printBackground: true,
margin: { top: "12mm", right: "10mm", bottom: "14mm", left: "10mm" },
displayHeaderFooter: true,
headerTemplate: headerTemplate("技术白皮书示例"),
footerTemplate: footerTemplate()
});
await browser.close();
console.log("PDF written to", pdfPath);
})();
要点:
- printBackground: true 以打印背景色与填充。
- emulateMediaType(“print”) 触发@media print样式。
- 使用displayHeaderFooter与模板内置的pageNumber/totalPages生成页码。
- 确保所有资源可本地访问(包括字体与SVG)。
- 流程图渲染时机
- 若在浏览器端用脚本把文本语法渲染为SVG,务必在page.pdf前等待渲染完成,设置window.renderReady标志。
- 更稳妥的做法是预渲染(例如命令行工具把Mermaid或DOT转为SVG),HTML只内嵌最终SVG,避免生成期Race Condition。
常见坑点与排错清单
-
背景丢失
- 症状:色块与浅灰底不见。
- 解决:启用printBackground;尽量使用高对比边框代替大面积背景。
-
字体替换/方框字
- 症状:PDF中中文变形、缺字或回退为系统默认。
- 解决:项目内置CJK字体并用@font-face引用;CI容器安装相同字体;在PDF属性里检查嵌入字体。
-
表格/代码分页不自然
- 症状:表头与主体分离,代码被切断。
- 解决:table、pre、figure统一加page-break-inside: avoid;必要时将大表拆页或缩放字体。
-
流程图模糊
- 症状:打印锯齿或发灰。
- 解决:优先SVG矢量;若必须位图,准备300dpi以上资源,按版心尺寸等比缩放。
-
页码错位或覆盖正文
- 症状:页眉页脚压住正文。
- 解决:给正文预留上下内边距;用生成器的margin与模板配合调试。
-
分页 “抖动”
- 症状:小改动导致大量分页变化。
- 解决:固定图片与图表最大宽度;避免在分页敏感处使用巨大margin/padding;对标题-段落/图表使用“成组避免分页”的容器。
-
颜色偏差
- 症状:打印比屏幕更淡或偏色。
- 解决:使用深色纯色(黑/深蓝);避免低饱和浅灰;必要时在打印机驱动里开启高对比。
-
动态渲染未完成就生成PDF
- 症状:图表空白。
- 解决:显式等待条件(document.fonts.ready、window.renderReady),或改为预渲染。
进阶玩法
-
自动目录与章节编号
- 通过构建脚本生成目录,或在浏览器端扫描h1/h2生成目录列表。
- CSS counters可给标题自动编号;若需要“页码指向目录”,考虑Paged.js等支持target-counter/page的引擎。
-
Running headers(章节名跑页眉)
- 高级分页引擎支持string-set:在h1/h2上记录章节标题,在@page的@top-center输出。
- 使用无头浏览器可改用生成器模板中的headerTemplate,渲染当前章节名则需运行时脚本填充。
-
双栏排版与图文跨栏
- 正文用column-count: 2;注意图/表/代码用break-inside: avoid并设为单栏全宽区块,必要时在双栏间插入.page-break控制布局。
-
水印
- 使用position: fixed的伪元素或容器在页面中心放置浅色“DRAFT”水印;注意透明度与print-color-adjust。
-
归档与规范
- 若需PDF/A等合规标准,浏览器生成可能不完全满足;可考虑专业引擎或后处理工具。
-
国际化与断行
- 中文断行可使用line-break: loose/strict,英文长词使用overflow-wrap: anywhere;混排场景审慎调试。
实战建议的“复盘清单”
- 版心与字体
- A4 + mm单位 + 合理边距 + 可读字号行高。
- 图表策略
- 优先SVG;过大则分幅;统一主题与线宽。
- 分页控制
- 标准break-*与兼容page-break-*并用;对表格/代码/图整体避免分页;必要处手动.page-break。
- 工具链
- 无头浏览器自动化 + 背景开启 + 页眉页脚模板。
- 构建可重复
- 本地字体、固定依赖、CI产物对比;渲染等待信号明确。
- 预检
- 抽样打印验证色彩与留白;检查PDF内嵌字体与页码连贯。
小结
把HTML当作排版引擎,并非“投机取巧”,而是用标准化、工程化方法来获得稳定、可重复的纸质输出。流程图用SVG保证锐利,说明文档用语义结构确保可维护,分页用CSS精细控制,最后用自动化工具链稳定产出A4可打印PDF。这条路线既适合个人作者,也适合团队白皮书、内部规范与学术资料。
我写这篇文章的初衷,是希望你不再被“网页转PDF总是乱掉”困扰,把文档当作软件来构建。认真对待边距与行距,也是在认真对待读者的阅读体验。
评论区话题
- 你在“HTML转A4可打印PDF”的过程中,最棘手的问题是什么:字体、分页、还是图表清晰度?
- 你更偏好哪条流程图路线(Mermaid/SVG手写/Graphviz)?为什么?
- 是否需要我补一套“从Markdown到PDF”的完整开源脚手架?
欢迎在评论区分享你的实践与困难,我会根据反馈继续完善这条可落地的文档工程路线。