网页按 Ctrl+P 存成 PDF 就错位,多数时候不是浏览器坏了:浏览器「打印为 PDF」是内核把页面按分页介质重新排版一遍再写成 PDF的过程,不是把屏幕截图贴进文档,而规范把「背景要不要打、纸张听谁的、越界的内容丢不丢」这几处明确交给了实现自行决定。本文在本机 Chrome 与 Edge 上造了 16 个测试页,把 DevTools 协议参数和命令行开关逐个跑了一遍,再解析 PDF 内容流取回纸幅、字号、字色、填充矩形,把六个坑各量成一个可对质的数字。

信息核验时点:2026-11-03(取证机为 Windows 10 专业版,内部版本 10.0.19045;本机安装的浏览器为 Chrome 156.0.8078.74(chrome.exe 文件版本与产品版本一致,4,525,720 字节)与 Microsoft Edge 156.0.4314.58(msedge.exe 5,453,128 字节),两者产出的 PDF 元数据 Producer 均为 Skia/PDF m156;引用的 CSS 2.1 第 9 章与第 13 章、MDN 的 @page、print-color-adjust、break-inside、打印指南四个页面,以及 DevTools 协议 Page.printToPDF 参数表与微软「在 Microsoft Edge 中打印」帮助页,全部在核验当日逐字取回并留存原文;文中所有字节数、纸幅点数、字号与颜色值来自本机 56 次协议参数调用与 14 次命令行调用产出的 PDF 文件解析结果,这些动作只在临时目录里读写,未改动任何系统设置)


本文要点
  • 一句话定义:浏览器「打印为 PDF」是渲染引擎以 print 媒体类型重新做一次布局,把结果按分页盒子写成 PDF;它读的是同一份 CSS,但用的是一套与屏幕完全不同的页面规则,所以「屏幕上对」和「纸面上对」是两件事。
  • 核心结论:六个坑里有四个是「默认值」造成的——背景默认不打(同一页 35,139 字节对 36,154 字节)、@page 的纸张默认不听(写了 size:A4 仍输出 612×792 的 Letter,正文从 12.00 磅被整体缩成 11.29 磅)、页眉页脚在命令行里默认是开的(多花 20,023 字节)、JS 后到的内容默认等不到(3,000 毫秒的定时器就足以让两块正文彻底消失)。另外两个是规范留白:越界内容允许直接丢弃(1400 像素宽页面的右端标记在 PDF 里一个字符都没有),分页点允许切进盒子中间(一张 300 像素高的卡正好压在分页点上时,被切成 140.2 点与 86.2 点两截,两截都真实地印在纸上)。
  • 最容易看走眼的地方:不勾「背景图形」时浏览器不是简单删掉底色,它还会改写文字颜色——六个浅灰档实测每档红绿蓝各减 84,可纯绿一个字没变、品红按另一套比例压暗,规则不统一,没法靠推算预判。
  • 三条可靠替代的分工:自己维护的页面用打印样式表治本(一条 print-color-adjust: exact 就能顶住丢背景,实测字节与勾选背景完全相同);要批量、要每次一样用无头命令行;排版要求高就别走浏览器这条链路,文档用 Office 或 WPS 导出,网页先用 Edge 的沉浸式阅读器净化或只选中一段再打印。
  • 三个反直觉的澄清:懒加载图片不会缺(三种参数产出字节完全相同的 26,897 字节,10 张图全部在场);中文字体照常嵌入且带 ToUnicode(雅黑、宋体、Arial 三个子集,复制粘贴不出乱码);超链接不会失效(第 1 页 3 个 Link 注释)。这三类网传故障在本机一个都没复现。
  • 适用边界:本文测的是 Chromium 内核的这两个版本在 Windows 上的行为,Firefox 与 Safari 的打印实现没有测,不能套用;本文没有比对打印预览的位图与最终 PDF 的位图是否逐像素一致,只比参数与产物。

浏览器「打印为 PDF」这条链路到底在做什么?

它做三件事:把媒体类型从 screen 切成 print、用页面的 CSS 重新布局一次、把布局结果逐页写进 PDF 盒子。本文的台架因此有三个入口,三个入口的默认值并不相同,这是很多「按教程做却对不上」的源头:

入口怎么触发本文实测的默认行为
打印对话框Ctrl+P,目标选「另存为 PDF」边距「默认」是每侧 1 英寸,纸张列表里 A4 排在字母尺寸之后,页眉页脚与背景图形两个复选框都要手动勾
DevTools 协议Page.printToPDF 方法官方参数表逐字写着 printBackground 与 displayHeaderFooter 都是 Defaults to false,margin 四项 Defaults to 1cm (~0.4 inches),paperWidth Defaults to 8.5 inches
命令行headless 加 --print-to-pdf同一份测试页输出 56,177 字节,页脚带着完整文件地址;背景矩形却在场——与协议默认的 false 相反

协议参数表里还有一句解释了整个坑三的钥匙:preferCSSPageSize 的默认是 false,而 false 的后果是 "in which case the content will be scaled to fit the paper size"(内容会被缩放以适应纸张大小)。这句话在本机量出来的具体形状是正文小了一圈,见坑三。

驱动方式上本文刻意做了双份:既用协议逐参数调用,也用命令行跑同一批页面,两条路的产物再拿去和 Edge 的同名运行做对照,一共凑出 13 组(9 组走协议、4 组走命令行)。结论是这两个浏览器在本文所有组合下文本内容一致、纸幅一致,13 组里每一组都恰好相差 14 字节,所以下面的数字可以同时代表这两个浏览器。

坑一:为什么每一页都印着同一个悬浮条?

因为规范就是这么规定的。CSS 2.1 第 9.3.1 节对 position 的 fixed 取值逐字写着:"In the case of the print media type, the box is rendered on every page, and is fixed with respect to the page box, even if the page is seen through a viewport (in the case of a print-preview, for example).",紧接着还有一句:"UAs must not paginate the content of fixed boxes."。

本机用一个 3 页正文的测试页复现了这两句。固定顶栏与固定底栏各自带一个专用标记,产出的 PDF 里:

标记出现在哪几页各页的位置(以点计,从页底起算)
固定顶栏第 1、2、3 页746.3、746.3、746.3(三页同一位置)
固定底栏第 1、2、3 页37.5、37.5、37.5
绝对定位顶栏仅第 1 页746.3
吸顶(sticky)条仅第 1 页728.3
正文首段仅第 1 页693.8
正文末段仅第 3 页390.8

同一份文件是 43,428 字节(Edge 是 43,442 字节)。这张表里有两件事同时成立:固定定位的条会重复到每一页,而绝对定位与吸顶只在第 1 页出现一次——也就是说,一个用吸顶做的目录条、一个用绝对定位做的悬浮按钮,存成 PDF 后在后面的页面上就凭空不见了。这不是丢内容,是分页模型里这两种盒子本来就没有「每页重画」的语义。

顶栏会不会压住正文,取决于它的坐标和正文起点的相对位置。本文这一页没压上:顶栏在 746.3,正文首段在 693.8,中间还差 52.5 点。但把页边距参数全部设成 0 再跑一次(同一页面 43,376 字节),底部固定条在第 3 页落到了横坐标 4.5、纵坐标 9.0 的位置——贴着纸边,实际打印时会被送纸机构切掉。

处置办法只有一条,写在打印样式表里:把固定与吸顶在 print 下退回普通流或直接隐藏。

@media print {
  .site-header, .floating-toolbar { position: static; }
  .back-to-top, .cookie-banner { display: none; }
}

坑二:背景为什么没了,字色还自己变了?

先说官方口径。MDN 的 print-color-adjust 页面(本页最后更新于 2025 年 11 月 3 日)对默认值 economy 的中文释义逐字是:"用户代理可以根据其认为适当和审慎的方式对元素进行调整,以优化针对其渲染设备的输出效果。例如,在打印时,浏览器可能会选择省略所有背景图像,并调整文本颜色,以确保对比度达到最佳,便于在白纸上阅读。这是默认值。" 微软的 Edge 打印帮助页则把开关位置写得很清楚:在「更多设置」里勾「背景图形」,作用是「以在打印输出中包含网页背景,或将其保留为未选中状态以忽略背景」。

本机的实测把这两句全坐实了,而且量出了第三种情况。同一份含四个色块、一张背景图、七行文字的测试页:

这一轮怎么设产出字节落进 PDF 的填充矩形颜色背景图的嵌入对象数
协议默认(不打背景)35,139只剩紫块与四处白块0
协议显式打背景36,154深蓝、两处红、紫块全在1
不打背景,但注入 print-color-adjust: exact36,154与上一行完全相同1
命令行不加任何参数56,177深蓝、红、紫全在有

第三行是本文最实用的一条:只在样式表里写一行 exact,其余什么都不改,产物的字节数和矩形集合与「老老实实勾选背景图形」完全一致。而页面里那唯一一个自己声明了 exact 的紫块,在默认不打背景的那一轮里也照样带着 #7F00FF 出场——同一条规则的局部效果也量得到。

但「调整文本颜色」这四个字的实测结果比教程里说的复杂。本文造了两组浅字测试:六档浅灰和八档彩色,全部放在深灰底上,然后不打背景打印(底色按规范被省略,纸变成纯白,浅字于是落进「白纸上看不清」的处置流程)。量到的映射是这样的:

原本是不打背景后 PDF 里的实际颜色原本在深底上
#FFFFFF#ABABAB白字
#F5F5F5#A1A1A1接近白
#E0E0E0#8C8C8C浅灰
#CCCCCC#787878浅灰
#AAAAAA#565656中灰
#888888#343434中灰
#C0C0C0#6C6C6C浅灰(彩色组)
#808080#2C2C2C中灰(彩色组)
#404040#404040没动
#00FF00#00FF00没动
#FFFF00#ABAB00黄
#00FFFF#00ABAB青
#FF80FF#AB56AB品红
rgba(255,255,255,.75)#ABABAB半透明白

前六行的规律干净得可疑:每个通道各减 84。但往后看就断了——纯绿 #00FF00 一个通道都没变,#404040 也没变,品红 #FF80FF 中间那个通道按减 84 应该是 44、实测是 86(更接近按 0.671 等比压暗)。也就是说内核里至少并存着两套压暗算法,落在哪个分支由颜色本身决定,用户没法靠推算预判某一行字会被改成什么样。本文能给的只有两条经验:浅色系全部有被改的风险,改完的落点不可预测;想完全避开,要么勾背景,要么给那一段写 exact。

顺带一条防误判:别用文件大小判断背景在不在。上面那份含背景图的页面差 1,015 字节,而另一份只有六个纯色底的页面,打不打背景的产物分别是 76,472 与 76,470 字节——只差 2 个字节。字节数在这种事上没有鉴别力。

坑三:写了 @page size: A4,为什么还是 Letter?

先看规范给了什么。CSS 2.1 第 13.2.1 节逐字是:"In CSS 2.1, only the margin properties ('margin-top', 'margin-right', 'margin-bottom', 'margin-left', and 'margin') apply within the page context.",同节还有一句限制单位的:"The page context has no notion of fonts, so 'em' and 'ex' units are not allowed."(页面上下文里没有字体概念,所以 em 和 ex 不许用)。MDN 的 @page 页面(最后更新于 2025 年 11 月 10 日)则对更新的能力给了明确的进度注记:"警告:还没有任何用户代理实现边距 at 规则(截至 2023 年 8 月)。"

本机的三份纸面参数实测:

这一轮怎么设第 1 页纸幅(点)第 2 页纸幅(点)正文在纸面上的字号
页面写 size:A4,协议不加参数612.0×792.0792.0×612.011.29 磅
同页,协议开 preferCSSPageSize595.0×841.9841.9×595.012.00 磅
同页,协议手动给 8.27×11.69 英寸595.9×841.9841.9×595.912.00 磅

第一行是这个坑的正文:CSS 里明写了 A4,产物却是 612×792 点,也就是 8.5×11 英寸的 Letter;而字号从 12.00 磅变成 11.29 磅,比值 0.9408 正好是 792 除以 A4 的高度 841.9。这就是协议文档那句 "the content will be scaled to fit the paper size" 落到纸面的样子——内容按 A4 排版,再整体缩 5.9% 塞进 Letter。很多人抱怨的「存出来的 PDF 字比屏幕上小一号、行距发紧」,本机量到的就是这一处,而且它不报错、不提示。

第二行和第三行是两条出路。preferCSSPageSize 只有走协议(或用封装了协议的工具)才拿得到:微软帮助页在「更多设置」下列出的项目是纸张大小、边距、缩放、页眉和页脚、背景图形几项,里面没有「听 CSS 的纸张」这一项;命令行的 --print-to-pdf 也没有对应开关。对话框路径下最接近的等价做法是手动把「纸张大小」选成 A4,也就是第三行那条路的界面版本。微软帮助页的纸张列表里 A4 写作 8.3 x 11.7 英寸(四舍五入值,精确的 A4 是 8.27×11.69 英寸),本文手动那一轮按 8.27 传入,纸面得到 595.9×841.9 点。

边距的实测更拧。同一份 CSS 写了 @page { margin: 0 },不加参数那一轮的产物里,第 1 页正文的左边界是 0.0 点,第 2 页正文的左边界却是 26.2 点。26.2 点约合 9.2 毫米,接近但并不等于协议默认的 1cm(1cm 是 28.35 点)——这一轮的正文本来就带着 0.9408 的缩放,28.35 点缩过去也正好落在 26.7 点这一带,可以确定的是它没有听 CSS 写的那 0。把同一份页面开 preferCSSPageSize 再跑,两页的左边界就都回到 0.0 点了;手动传 A4 并把四个边距参数设成 0 的那一轮同样是两页 0.0 点。也就是说默认路径下页边距会一页听 CSS、一页回落实现自己的默认值,两页的正文起点因此不齐。

伪类的行为也值得单独记一笔。另一个只声明了 @page :first 横向与 @page :left 用 A5 的页面,不加参数时三页纸幅是 792×612、612×792、612×792——横向的方向听了、尺寸没听(用的是 Letter 的横向而不是 297×210 毫米),A5 完全没兑现;开了 preferCSSPageSize 之后三页变成 841.9×595.0、420.0×595.0、612.0×792.0,前两个精确对应 297 毫米乘 210 毫米和 A5,第三页则因为基础 @page 没写 size 而回落默认纸。

最后是 MDN 那句警告的实测:本文在测试页里写了边距 at 规则,让它把一段专用字符串打印到页面上半中央,而在全部报告文本里那个字符串出现了 0 次。想用 CSS 自带页眉页脚,在 Chromium 这条链路上目前做不到;能用的只有对话框里的「页眉和页脚」复选框,或协议的两个模板参数。

页眉页脚这两项还有两个附带代价,本机都量到了。同一份只有一屏中文正文的页面,不打页眉页脚是 61,621 字节,打开后是 68,139 字节;打开的那份在纸面顶部 769.5 点处有一行 7.99 磅的「日期加标题」,底部 16.5 点处有一行「本页地址加页码」——地址那一行是完整的 file 协议路径,换成线上页面就是完整网址。把上下边距各设 0.8 英寸再看,正文首块的位置从 720.8 点降到 691.5 点(这里的坐标从纸底起算,数值变小就是往下挪),等于给页眉让出 29.3 点、约合 10.3 毫米。命令行那一侧的体积代价更直接:同一页不加参数 56,177 字节,加上去掉页眉页脚的开关后 36,154 字节,差 20,023 字节,Chrome 和 Edge 两边的差值一模一样。

坑四:JS 后渲染的那一块为什么消失了?

因为打印这个动作没有「等页面忙完」这一步。协议文档里 Page.printToPDF 的参数表没有任何等待相关的选项,它假定调用时文档已经是最终形态。

本机第一版测试页把内容插入的定时器设在 600 毫秒,结果三种调用(不等待、等 1,200 毫秒、等 1,200 毫秒并开背景)产出的 PDF 是同一个字节数——40,788 字节,一个都没差别。这个假象本身是个坑:从发出命令到内核真正开始排版,中间那几次往返用的时间已经超过了 600 毫秒,于是「等待」这个变量在这台机器上根本区分不出来。把定时器改成 3,000 毫秒再测,差异立刻现形:

这一轮怎么设产出字节纸面上的文字行3 秒后才插入的两块
协议,导航完成后立即调用60,94711 行都不在
协议,等 3,600 毫秒再调用80,67613 行都在
命令行,不加等待参数82,17211 行都不在
命令行,--virtual-time-budget=100082,17211 行都不在,字节与上一行完全相同
命令行,--virtual-time-budget=5000101,89713 行都在
命令行,5000 加去掉页眉页脚80,67613 行都在,与协议等待那一轮逐字节同数
Edge,命令行 5000101,91113 行都在,比 Chrome 多 14 字节

这张表给出两件事。第一,虚拟时间预算必须大于页面自身最长那段延时:3,000 毫秒的定时器配 1,000 毫秒预算,产物和不加参数逐字节相同,等于这个参数白写;配 5,000 毫秒,缺的两块全部回来。第二,两条入口可以互相印证:命令行「预算 5,000 且无页眉页脚」和协议「等 3,600 毫秒」拿到的是同一份 80,676 字节的文件,说明本文这批数字不是某一条通路上的偶然。

普通用户在对话框前对应的动作只有一条:按下 Ctrl+P 之前先让页面彻底加载完,把该点的展开点开、该翻的翻页翻完、图片全部出现。反过来说,如果一个页面靠滚动才加载,那本文的懒加载实测(见后面澄清一节)说明滚动本身不是必需的,但脚本驱动的排版(虚拟列表、只渲染可视区域的那种)不吃这个红利。

坑五:一张宽表为什么右边凭空少一截?

因为规范允许丢掉。CSS 2.1 第 13.2.3 节逐字是:"When formatting content in the page model, some content may end up outside the current page box. For example, an element whose 'white-space' property has the value 'pre' may generate a box that is wider than the page box.",处置方式则完全交给实现:"User agents may handle boxes positioned outside the page box in several ways, including discarding them or creating page boxes for them at the end of the document."。同节还有一条给实现的建议:"User agents should avoid generating a large number of empty page boxes to honor the positioning of elements (e.g., you do not want to print 100 blank pages)."。

本机用一个 1400 像素宽的容器复现,容器最右端放了一个专用标记:

这一轮怎么设产出字节纸面文字块数最右端标记
协议默认(纵向 Letter)27,7495整份 PDF 里找不到
协议开 landscape31,7446在,位置 602.6 点
协议 scale 0.531,7696在,但全部字号从 8.00 磅变 4.46 磅
协议 scale 0.2531,7696在,字号 3.00 磅

第一行是本机 Chromium 的选择:丢。默认那一轮只有 1 页,越界的填充矩形最宽的一只从横坐标 39.7 点画到 747.8 点,而纸宽只有 612 点,超出的 135.8 点连同那一段文字一起消失——不产生第 2 页,不产生提示,也不产生任何错误。这就是「宽表右边少两列」的真面目:它不是被挤到下一页了,是根本没写进文件。

对话框这边对应的三个选项,微软帮助页逐句写的是「此选项将自动调整内容的大小以适合纸张的可打印区域」「此选项以原始大小打印文档,不进行任何缩放」「通过选择一个值,可以手动指定打印比例系数」,还补了一个例子:「如果将缩放比例设置为 50%,文档将按其原始大小的一半打印。如果将其设置为 200%,文档将打印为原始大小的两倍。」本文的 scale 两行就是「自定义」那一条的量化后果:缩放确实保住了内容,代价是 8 磅的字变成 4.46 磅,肉眼几乎读不了。所以宽内容的合理顺序是:先试横向(本文那一轮字块的字号仍是 8.00 磅),横向装不下再考虑缩页面宽度或砍列,最后才动缩放比例。

@media print {
  .data-table { width: auto; overflow: visible; }
  .wide-wrapper { max-width: none; }
  pre { white-space: pre-wrap; word-break: break-all; }
}

那行 pre 的换行处理对应规范点名的那个场景:'white-space' 为 pre 的元素本来就可能生成比页面框更宽的盒子,不改成可折行就会掉进同一口坑。

坑六:跨页处为什么总把卡片和行切断?

分页点落在哪里由实现决定,能不能避免则由三个属性共同决定。MDN 的 break-inside 页面(最后更新于 2025 年 11 月 3 日)把规则写成三条:"如果这三个中断属性的值有一个是强制中断值(always、left、right、page、column 或 region),则该属性具有优先权……因此,break-before 值优先于 break-after 值,而后者又优先于 break-inside 值。""如果三个相关值中的任何一个是避免中断值(avoid、avoid-page、avoid-region 或 avoid-column),则不在该点应用此类中断。" 同一页还给了新旧属性的关系:"由于兼容性原因,旧的 page-break-inside 属性应该被浏览器视为 break-inside 的别名。……只有一部分值可使用别名"——可别名的只有 auto 和 avoid 两个取值。CSS 2.1 第 13.3.1 节那一份属性表写得更早:page-break-before 的取值是 auto、always、avoid、left、right、inherit,适用于 block-level elements,不继承,媒体类型是 visual 与 paged。

本机先用一个 40 行表格加两张卡片(内容高度各 300 像素,算上内边距和边框后纸面高 253 点)做实验:

这一轮怎么设产出字节页数卡片在纸面上的样子
协议默认(Letter)101,7497两张卡都完整落在第 6 页,盒顶分别在 566.6 点与 299.6 点
协议手动给 A4 纸张101,2676没声明避让的那张被分页点切开:第 5 页只剩 40.2 点的一截,第 6 页顶部接着画 213.0 点,两截相加 253.2 点正好等于整张卡的高度;声明了避让的那张整块画在第 6 页(334.5 点到 587.3 点)
协议只要第 1 到 2 页51,3042第 1 页 14 行、第 2 页 16 行

为了把「哪一张会被切」和「切开后每截多长」量干净,又补做了一个八张同规格卡片的页面:每张卡高 300 像素,第 1、3、5、7 张什么都不写,第 2、4、6、8 张声明 break-inside: avoid,页面里再写 @page{size:A4;margin:0}。

这一轮怎么设产出字节页数切断情况
协议开 preferCSSPageSize(纸面 595.0×841.9 点)86,6623七张保持完整的 226.5 点盒高;第 7 张被切成两截——第 2 页底部 140.2 点、第 3 页顶部 86.2 点,卡顶标签留在第 2 页(124.9 点),卡底标签出现在第 3 页(762.4 点)
同一页,协议不加参数(Letter,正文缩成 11.29 磅)86,6403完整卡高变成 213.1 点,同一张第 7 卡切成 131.9 加 81.1 点,两截相加 213.0 点

再做一个专门量代价的实验:一个 1,000 像素高的占位块结束后紧接着放一张卡,位置刚好让这张卡跨在分页点上。不写声明的那一轮产物 63,739 字节,卡片被分页点切开,卡顶标签留在第 1 页的 34.9 点、卡底标签跑到第 2 页的 672.4 点;给这张卡写上 break-inside: avoid,产物 63,570 字节,它整块挪到第 2 页顶部(两个标签同页,分别在 826.9 点与 622.9 点),而第 1 页从 49.9 点往下再没有任何内容——底部空出了 49.9 点、约合 17.6 毫米的一条白边。

这四件事同时被量到了。第一,切不切跟纸张有关:同一份内容,默认那轮两张卡都没被切,换成 A4 后每页可用高度一变,没声明避让的那张正好赶上分页点。第二,切断不丢内容,但盒子会被切平:两截都在文件里,长度加起来正好是原盒高度,可第 1 页那一截的下边缘压在纸面边界上,卡片的边框和底色在接缝处各断一次,观感就是「卡片被腰斩」。第三,避让有效,但付的是留白:写上 avoid 的那张整块跑到下一页,上一页底部留下实测 49.9 点的空白——这是 avoid 的固有取舍,不是 bug。第四,avoid 不是硬约束:当盒子本身比一页还高时,任何声明都得被打破,MDN 那句「一旦应用了强制中断,如果需要,可以添加软中断,除了相应值解析为 avoid 的元素边界」说的就是这个边界。

表格行方面本机没有复现出「一行被拆到两页」。倒是有一条网传说法可以直接排除:长表格的表头不会每页重复。本文用语义化的表头标记做了一个跨 3 页的表,两种纸张各跑一遍:

这一轮怎么设页数第 1 页首行第 2 页首行第 3 页首行每页行数
协议默认3表格上方的说明段表头两列文字表头两列文字19、20、6
协议指定 A43表格上方的说明段表头两列文字表头两列文字21、21、3

第 2、3 页的首行都是表身行文字之前的列名,末行是表尾标记,两种纸张下都是每页重复(默认那轮 119,109 字节,A4 那轮 119,124 字节)。前提是用对结构:<table> 里放 thead,而不是用 div 手搭一个「看起来像表头的行」——后者在分页模型里没有任何表头语义,自然不会得到重复待遇。

@media print {
  tr, .card, figure { break-inside: avoid; }
  thead { display: table-header-group; }
  h2, h3 { break-after: avoid; }
}

最后一行管的是另一种错位:标题落在上一页末尾、正文跑到下一页。

三种可靠替代分别适合什么情况?

三条路的定位不同,不要当成三个可互换的选项。

替代一:给自己维护的页面写打印样式表。 这是唯一治本的一条,适用前提是页面归你管。它不需要用户配合,不需要任何命令行,产出的稳定性由代码保证。按本文的六个坑逐条给一份可用的骨架:

@page {
  size: A4;
  margin: 15mm 18mm;
}

@media print {
  html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  .site-header, .sidebar, .ad-slot, .cookie-banner { display: none; }
  .sticky-bar, .fixed-toolbar { position: static; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
  .data-table { width: auto; overflow: visible; font-size: 9pt; }
  tr, .card, figure, blockquote { break-inside: avoid; }
  thead { display: table-header-group; }
  h2, h3 { break-after: avoid; }
  body { font-size: 11pt; line-height: 1.5; }
}

三个说明。单位用毫米和磅,不要写 em 或 ex——页面上下文里没有字体概念,这是规范硬限制;正文长度超过一屏时把字号从像素改成磅,能避开坑三那个 0.9408 的缩放(因为纸张一旦对了就不需要缩);最后那条 a 标签补 URL 的做法对应本文实测的一个事实:PDF 里的链接注释本来就在(第 1 页 3 个 Link),但纸上看不见,打印场景要让人读到地址就得显式补出来。改完不要只看屏幕,按本文的判据自查一次:Ctrl+P 打开预览,确认纸张是 A4、正文没有被整体缩小、背景在不在、跨页处断得合理。

替代二:用无头命令行批量转。 适用前提是要重复、要一致、要能等 JS。本文跑通的命令形状是:

chrome.exe --headless=new --disable-gpu --no-first-run ^
  --user-data-dir=D:\pdf-profile ^
  --print-to-pdf=D:\out\page.pdf ^
  --no-pdf-header-footer --virtual-time-budget=5000 ^
  file:///D:/tmp/a60/pages/dyn2.html

本文实测的六个坑在这条路径上的对应收益是:背景默认就打(不需要额外开关)、页眉页脚能用一个开关干净去掉(同页省下 20,023 字节)、等待时长可控(预算给到 5,000 毫秒后动态内容全部在场,产物与协议等足时间逐字节相同)、Edge 用同一条命令的 msedge.exe 版本可跑,两份产物除 14 字节的固定差额外内容完全一致。三个必须避开的写法:--print-to-pdf-no-header 是废弃开关,本机 156 上加了它产物和不加逐字节相同(56,177 字节、页脚照旧),要用的是 --no-pdf-header-footer;--virtual-time-budget 要给到大于页面最长延时;--user-data-dir 要给绝对路径,相对路径会让反复调用落在同一个临时配置上,产物的时间戳和元数据会互相干扰。至于纸张——命令行的 --print-to-pdf 没有对应的纸张参数,要精确控制纸面尺寸只能走协议或封装工具,这也是替代一仍然必要的原因。

替代三:干脆不走浏览器这条链路。 适用前提是排版要求高于「凑合能看」。分两种情况。手里是文档(Word、Excel、WPS 文字、演示)时,用办公软件自己的「另存为 PDF」或「导出 PDF」,分页、字号、页边距由文档的排版模型决定,比浏览器重排一遍网页可控得多;WPS 那条路径的完整操作本站已有专文,见 WPS PDF 从入门到精通:阅读、编辑、转换与签章全流程(2026 版)。手里是别人的网页、只想干净地留一份正文时,用 Edge 的净化打印:微软帮助页对这个选项的说明是它「可从打印的网页中删除周围的材料,例如广告和网站导航」,但同一页也写明这一项目前还进不了打印对话框——「我们正在努力发布可直接通过打印对话框获取的无杂乱打印选项」,给出的过渡做法是走沉浸式阅读器:「打开要打印的网站。在地址栏中,选择沉浸式阅读器图标」,或在 Windows 设备上按 F9 进入,再从页面顶部的菜单里选打印;那句提醒也一并抄过来——「注意:沉浸式阅读器可能不适用于所有网站」。只想要页面局部时用另一条,官方步骤是「单击并拖动文本或图像部分,以选择要打印的网站部分」,然后在选中的内容上右键选择打印。已经存坏的 PDF 要修版式、补页码或删空白页,则交给专门的编辑器,本站的 PDFgear 怎么用?免费 PDF 编辑器功能与版本介绍 管这一段。

你的情况选哪条依据(本文实测)
页面是自己写的,且要长期用替代一一行 exact 就能顶住丢背景,产物与勾选背景逐字节同数
每周要存几十页,要求每次一样替代二无头路径背景默认在场、页眉页脚可关、等待可量化
源文件本来是文档替代三办公软件的分页模型不依赖 CSS 分页规则的留白
网页内容杂,只想留正文替代三(净化打印)微软页面明确该选项去除广告与导航
一张宽表,必须看全替代一,其次横向打印默认纵向会静默丢掉越界内容

那些「听说不行」的地方,实测结果如何?

三类常见告警在本机一个都没复现,值得单独列出,因为它们占了网上教程里相当篇幅。

懒加载的图片会缺? 不会。一个用原生懒加载、把图片推到首屏之外的页面,三轮调用(默认、先滚动到页底再滚回顶部、打开背景)产出的文件字节数完全相同——26,897 字节、5 页,PDF 里的图像嵌入对象都是 10 个,十个行标记从第 1 页到第 4 页分布一致。内核在分页排版时会取全页需要的图片,人工滚动不会改变结果。真正会变的是需要交互才渲染的区块,那属于坑四,不是懒加载。

中文字体不嵌、复制出来是乱码? 嵌了。一份中英混排的页面,PDF 的字体表里是三个 Type0 子集:微软雅黑、宋体、Arial,三个都是「嵌入」且都带 ToUnicode 映射。有 ToUnicode 就意味着阅读器能把字形反查回字符,复制粘贴出的是正确的汉字。纸面字号也照常分级:标题 21.00 磅、正文 12.00 磅、小字注 8.25 磅(对应 CSS 里的 11 像素)。

超链接点不动? 点得动。带三个链接的页面,产物第 1 页挂着 3 个注释对象,类型是 Link 加 URI。但有两项默认没有:书签大纲默认不生成(打开该项后从 43,203 字节变 43,614 字节,文件里出现大纲对象),无障碍标记默认是开的(关掉 generateTaggedPDF 后产物 32,711 字节,比默认少 10,492 字节,文档标记信息整块消失)。这两项在打印对话框里没有对应复选框,只能走协议或封装工具;要发给读屏软件用的长文档,别关后者。

顺带一条对照实验的结论,因为它能省掉一半「换个浏览器试试」的徒劳:Edge 与 Chrome 的对照一共 13 组(协议 9 组、命令行 4 组),每组的文本内容一致、纸幅一致,文件大小每一组都恰好相差 14 字节(35,139 对 35,153、43,428 对 43,442、26,897 对 26,911、101,749 对 101,763、101,897 对 101,911、56,177 对 56,191)。这两个是同一个内核的两个发行版,遇到本文这六个坑,换浏览器不解决问题。

常见问题 FAQ

打印预览看着挺好,存出来就不对,该信哪个?

先确定「不对」是哪一类。本文没有做预览位图与最终 PDF 位图的逐像素比对,所以不对「预览和产物必然一致」下断言;能给的是三条能落地的判断。第一,规范把打印预览本身当成视口来讨论——CSS 2.1 第 9.3.1 节那句 "even if the page is seen through a viewport (in the case of a print-preview, for example)" 说的就是固定盒子要相对页面盒子定位、每页重画一次,连预览这种「透过视口看」的情形也不例外。因此预览里悬浮条跟着窗口动、存出来变成每页一条,这两件事都符合规范,不用拿预览的观感去推断文件内容。第二,纸张大小、边距、缩放、背景图形这几项都在预览旁边的「更多设置」里,改一项就重排一次,把预览当参数检查器是它最可靠的用法。第三,本文量到的四个默认值坑都能在产物里逐项复核:数一下页数对不对、看同一处字号从 12.00 磅有没有变成 11.29 磅、看底色块在不在、看最右边那一列有没有整段消失。改完参数仍然不对时,问题多半在这份页面的 CSS 本身,按坑一到坑六逐条对照即可。

我只想快速存一份能看的 PDF,最少要做哪几步?

按顺序做四件事,全部在对话框里。第一步把「更多设置」里的纸张大小选成 A4,而不是留着默认尺寸——这是坑三那个 0.9408 缩放的唯一界面解法。第二步勾「背景图形」,微软页面给的提示句正好是这条的理由:"如果打印预览或输出看起来与要打印的网页不同,则应包括背景图形。"第三步确认边距不要留在「默认」,那一档逐字是"此选项将在所有侧保留 1 英寸的边距",四侧各吃 1 英寸对 A4 的正文宽度影响很大;要省纸就选「最低」或「没有」,但选了「没有」要留意本文实测的那条——固定底栏会贴到纸边被切。第四步,如果页面是脚本渲染的(一进去先转圈、内容后弹出来),先在浏览器里让它加载完再按 Ctrl+P,别指望对话框替你等。

@page 规则和对话框里的纸张、边距,谁说了算?

实测下来是「对话框和参数说了算,@page 只在被显式授权时说话」。CSS 写的 size:A4 在默认路径上被无视,产物仍是 612×792 点,正文被整体缩到 11.29 磅;只有走协议并打开 preferCSSPageSize,纸幅才变成 595.0×841.9 点、字号回到 12.00 磅。边距那一半更拧:同一份 @page { margin: 0 },第 1 页正文左边界是 0.0 点、第 2 页却是 26.2 点,后者只是落到了实现自己的默认值上(协议参数表里 1cm 的默认值是 28.35 点,这一轮还带着 0.9408 的缩放),而不是听了 CSS 的那句 0。方向类设置(横向、竖排)在默认路径上会被部分采纳——本文看到 Letter 的横向,也就是听了旋转、没听尺寸。结论落到操作上是一句话:别把纸张和边距的希望寄托在 CSS 上,能改对话框就先改对话框;要两者都稳定,就统一到协议或无头参数上,并在同一套参数下反复出片。

为什么不勾背景图形时,白字变成了灰字,而不是保持白色?

因为默认的 print-color-adjust 值是 economy,而它的定义里就包含「调整文本颜色」这一手,MDN 的中文表述逐字是"浏览器可能会选择省略所有背景图像,并调整文本颜色,以确保对比度达到最佳,便于在白纸上阅读"。本机的实测比这句描述更细:六档浅灰每档三个通道各减 84,压到了 #ABABAB 到 #343434 之间,够深的 #404040 与纯绿 #00FF00 则一动不动,而品红 #FF80FF 的中间通道从 128 变成 86,更接近等比压暗而不是减固定值。也就是说内核里有多套压暗策略,落点由颜色本身决定,用户预判不了。这条对普通用户的实际含义是:关掉背景不等于「只是没有底色」,它同时改了字的颜色;如果那份网页是深底浅字的设计(代码块、图表、暗色主题的文档站很常见),勾背景是唯一的可靠做法,或者让页面作者给那一段加 print-color-adjust: exact。

命令行转 PDF 要不要等 JS?要等多久?

要等,而且等多少取决于页面而不是取决于命令。本文把一个内容插入的定时器从 600 毫秒改到 3,000 毫秒才测出差异:600 毫秒那一版,立即调用和等 1,200 毫秒调用的产物是同一个 40,788 字节,等待这个变量根本没有作用面;3,000 毫秒那一版,不等待是 60,947 字节、11 行文字,等足 3,600 毫秒是 80,676 字节、13 行,缺的两块补上了。命令行的对应开关是 --virtual-time-budget,本文的数是不加参数与给 1,000 毫秒的产物逐字节相同(82,172 字节、两块都缺),给到 5,000 毫秒才有全部(101,897 字节)。所以取值的定法不是「给个大数」,而是「大于页面最长的那段延时并留余量」;判断页面最长延时,可以打开控制台看网络与计时器面板,或者干脆两种取值各跑一次,比字节数——字节数一样的两档,说明低的那档等于没加。

Edge 和 Chrome 存出来的 PDF 有区别吗?

在本文这一批测试里没有可感知的区别。13 组对照的文本内容一致、页数一致、纸幅一致,文件大小的差每一组都是 14 字节(本文最大的一组是 101,749 对 101,763,最小的一组 35,139 对 35,153)。两者的 PDF 生成器元数据都是 Skia/PDF m156,只是版本号同一个量级下各自发行:本机 Chrome 是 156.0.8078.74,Edge 是 156.0.4314.58。所以「换个浏览器就好」对本文这六个坑无效;真正可能有区别的是界面入口——Edge 的打印对话框多了「使用系统对话框打印」的入口,微软帮助页给的快捷键提示是「请在 Windows 设备上按 Ctrl+Shift+P,或在 macOS 设备上按 Option+Command+P」,走系统对话框时能拿到打印机驱动级的选项(纸张来源、介质、质量),但那已经不是「另存为 PDF」这条路了。

写在最后

把这批数字串起来看,「网页另存为 PDF 全是错位」这句话里其实混着三种不同的问题。第一种是默认值问题,占本文四个坑:背景不打、纸张不听 CSS、页眉页脚在命令行里默认开着、脚本后到的内容默认等不到——它们的共同特点是用户什么都没做错,参数没设而已,改一次就永久解决。第二种是规范留白,占两个坑:越界内容允许丢弃、分页点允许切进盒子——规范只写了「实现可以这么做」,所以任何按屏幕布局写出来的 CSS 都要额外考虑「分页之后会怎样」,而这一步在网页开发里几乎从不被测试。第三种是认知错位,即把打印当成截图,指望「屏幕上看到的」等于「纸上出现的」。

按性价比排的话,普通读者先做三件事:纸张手动选 A4、勾上背景图形、等页面加载完再打印。这三步能消掉本文四个默认值坑里的三个半,全部在对话框里,零成本。写页面的人多做一件:给站点补一份打印样式表,长度不超过三十行,把 fixed 退回 static、给必须保色的段落加 exact、给表格行和卡片加 break-inside: avoid、把正文单位从像素换成磅。要批量出片的再补命令行那条路,但记住 --print-to-pdf-no-header 在本机的 156 上已经不起作用,要用 --no-pdf-header-footer,虚拟时间预算要给到超过页面延时。最后,如果这份 PDF 是要给别人看、要打印出来、要归档的,那最省时间的决定往往是别用浏览器打印它——文档回到办公软件导出,网页先净化再打印,已经坏掉的交给专门的编辑器处理,本站那篇 Windows10和11打印故障排查:无响应卡纸乱码怎么办 管的是打印机这一侧的故障,与本文的 PDF 生成侧不重叠。


本文全部参数组合、字节数、纸幅点数与颜色值采集于 2026-11-03,取证机为 Windows 10 专业版 10.0.19045,浏览器为 Chrome 156.0.8078.74 与 Microsoft Edge 156.0.4314.58;测试页均为本文自造的本地静态页面,未对任何线上站点做打印;打印预览与产物的位图比对、以及 Firefox 与 Safari 的行为未测。以 W3C、MDN 与微软官方文档及后续浏览器版本为准。

发表评论

验证码图片,点击可更换