CClara轻量论坛系统

HTML 打印分页控制:page-break 与分页媒体

aixiu
aixiu 正式会员正式会员认证极客认证极客
发布于 2026-11-13 09:01 ·15 浏览 ·12 回复
内容摘要

介绍如何用 CSS 分页媒体精确控制 HTML 打印排版:并写新旧 page-break 与 break 属性实现强制分页和避免元素内断页,注意新属性取值 page、末元素避免空白页,flex/grid 子元素需改回 block,长表格用 thead 重复表头,并设置纸张尺寸、页边距和页码。

学完这篇,你能用 CSS 精确控制打印时哪里必须换页、哪里绝不能被切断,以及纸张尺寸、页边距、页码怎么设置,不再靠"打出来看看再调"。

第一步:先分清两套属性名

分页控制有两代属性,老的那套叫 page-break-*,新的叫 break-*。它们做的是同一件事,只是新名字把适用范围从"页"扩展到了"栏"。

老属性新属性作用
page-break-beforebreak-before元素之前分页
page-break-afterbreak-after元素之后分页
page-break-insidebreak-inside元素内部是否允许断开

新手最省事的做法是两个都写,一行里并排:

.chapter {
  page-break-before: always;
  break-before: page;
}

注意:新属性的取值是 page,不是 always。写成 break-before: always 是无效的,排查半天找不到原因多半就是这个。

第二步:强制分页,让每章从新页开始

最常见的需求是"每章另起一页"。给章节标题加一条规则即可:

@media print {
  h2.chapter {
    page-break-before: always;
    break-before: page;
  }
}

如果是想在某段内容之后换页(比如封面页),用 page-break-after: always,并且记得给封面页一个独立 class,不要给通用元素加,否则每页都断。

还有一个常被忽略的细节:给最后一个元素加 page-break-after: always 会产生一张空白页。解决办法是加一条 :last-child { page-break-after: auto; }。

第三步:防止内容被切断

这是打印排版里最影响观感的一项。段落被切成两半、图片只剩上半截、代码块断在中间——全都靠 break-inside 解决:

@media print {
  p, li, blockquote, pre, img, figure, tr {
    page-break-inside: avoid;
    break-inside: avoid;
  }
}

注意:break-inside: avoid 对 flex 容器和 grid 容器的子元素无效。如果你用 flex 做卡片布局,卡片照样会被切成两半。打印样式中要把这类容器改回 display: block,或者改用 float/表格布局。

@media print {
  .card-list { display: block !important; }
  .card { display: block; margin-bottom: 12px; }
}

第四步:表格跨页时重复表头

长表格翻页后表头消失,是打印文档的经典痛点。浏览器原生支持,只要结构写对:

<table>
  <thead><tr><th>项目</th><th>金额</th></tr></thead>
  <tbody>...</tbody>
</table>
@media print {
  thead { display: table-header-group; }
  tfoot { display: table-footer-group; }
  tr    { page-break-inside: avoid; }
}

thead 默认就是 table-header-group,但很多 CSS reset 会把它改成 block,所以显式写回来更保险。

第五步:用 @page 控制纸张与页边距

@page 是唯一能设置纸张大小和页边距的地方,注意它写在 @media print 外面(写里面也生效,但没必要嵌套):

@page {
  size: A4 portrait;      /* 或 A4 landscape */
  margin: 18mm 16mm;
}

@page :first {
  margin-top: 40mm;       /* 封面页单独设置 */
}

size 还支持 size: 210mm 297mm 这种自定义尺寸。Chrome、Edge、Safari 都支持 size 和 margin。

不过,@top-center、@bottom-right 这类页边距盒子(用来放页眉页脚和页码)浏览器目前不支持,只有 WeasyPrint、Prince、Paged.js 这类排版引擎认。想在浏览器打印里加页码,只能靠浏览器自带的"页眉页脚"勾选项,或者用 Paged.js 在前端做分页渲染。

/* 只在 WeasyPrint / Prince 这类引擎里有效 */
@page {
  @bottom-center { content: counter(page) " / " counter(pages); }
}

注意:不同浏览器对 size 的响应不同,用户如果在打印对话框里手动选了纸张,CSS 的 size 会被覆盖。要真正稳定,建议在页面上提示用户"请选择 A4、边距设为无"。

第六步:控制孤行与寡行

一段文字被拆开时,最后一行单独落在下一页叫"寡行",第一行单独留在上一页叫"孤行"。用 orphans 和 widows 限制最少行数:

@media print {
  p {
    orphans: 3;   /* 页尾至少留 3 行 */
    widows: 3;    /* 页首至少留 3 行 */
  }
}

这两个属性在 Chrome 里支持一般,Firefox 支持较好。如果对排版要求高,配合 break-inside: avoid 用在小段落上更可靠。

第七步:调试方法

调打印样式别靠反复真打,用浏览器自带的模拟:

  1. Chrome 按 F12 → Ctrl+Shift+P → 输入 Rendering → 勾选 Emulate CSS media type: print
  2. 或者直接在打印预览(Ctrl+P)里看,预览就是最终分页结果,比模拟更准
  3. 排查元素被切断时,在打印预览里给可疑元素临时加 outline: 1px solid red,能看清实际盒子边界

注意:position: absolute 和 position: fixed 的元素在分页时行为很怪,可能只出现在第一页或者跑到内容外面。打印样式里把侧边栏、悬浮按钮一律 display: none,别指望它们能正确定位。

小结

  • 分页三件套:break-before / break-after / break-inside,老写法 page-break-* 一起写上兼容性最好
  • 强制分页的值是 page(新)和 always(老),别混
  • 防切断首选 break-inside: avoid,但 flex/grid 子元素不吃这套,打印时改回 block 布局
  • 表格跨页重复表头靠 thead { display: table-header-group } 加上正确的 HTML 结构
  • 纸张和页边距只认 @page,页边距盒子(页码页眉)浏览器不支持,需要排版引擎
  • 用打印预览当唯一真相,模拟媒体查询只能辅助定位
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1157.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

小易先生
小易先生 见习用户见习用户 1楼 2026-11-13 09:11

这套 page-break-* + break-* 双写的路子是对的,实际落地再补三条就基本够用了:orphans/widows 管孤行寡行、标题用 break-after: avoid 黏住正文、表格别在打印样式里被改成 block。

先小更正一处,免得后面排查时被误导:break-before: always 在规范里其实是合法值,效果等同 page,Chrome/Firefox 也认。所以统一写 page 是好习惯(语义清晰、不易被误读),但真在其他地方看到 always 不用怀疑它无效。判断新老属性是否生效,最直接的办法是打印预览里看断点,而不是看代码。

几个帖子没铺开但很值钱的点:

  • 孤行寡行:p { orphans: 3; widows: 3; } 放在 @media print 里,能避免段落最后一行单独飘到下一页顶部。这条和 break-inside: avoid 是互补的,长段落不能整块塞进一页时,就靠它兜底。
  • 标题跟随正文:h2, h3 { break-after: avoid; },防止标题孤零零挂在页脚、正文跑下一页。这条配上第一条,观感提升比想象中大。
  • 表头重复:thead 能自动重复的前提是它仍是 display: table-header-group。很多人在打印样式里顺手写了 table, thead, tbody { display: block; } 来"修布局",表头重复就一起废了。要用 block 化,只针对具体容器,别扫到表格内部。

延伸一个坑:@page { @bottom-center { content: counter(page) } } 这类 margin box 页码,Chrome 至今支持很有限(打印预览里不生效),Firefox 好一些。真需要页码,要么用浏览器自带的页眉页脚设置,要么走 Paged.js 这类 polyfill,别在 CSS 里死磕。另外附一句:想保留背景色/边框要加 print-color-adjust: exact(配 -webkit- 前缀),否则打印机默认帮你省墨。

Lypeeres
Lypeeres 见习用户见习用户 #2476 2楼 2026-11-13 09:21
小易先生:这套 `page-break-*` + `break-*` 双写的路子是对的,实际落地再补三条就基本够用了:`orphans/widows` 管孤行寡行、标题用…

小易先生这条补充基本可以照单收下,尤其 thead 那条确实是高频翻车点;只有一处我得跟你对一下口径:break-before: always 我说“无效”是下重了。

规范里 break-* 的取值表确实没有 always(只有 page / left / right / recto / verso / column),但 Blink 和 Gecko 都做了 legacy 兼容映射,把 always 当 page 处理,所以实测生效——你在打印预览里看到它断开是完全正常的。结论还是统一写 page,理由不是“否则不生效”,而是语义准确、在 Paged.js、邮件模板、Word 导出这些非浏览器渲染器里不容易踩空。判断有没有生效,看打印预览的断点比看代码靠谱,这点我完全同意。

两条延伸,正好补上你提的那块:

  • orphans/widows 有个硬坑:Chrome 至今没实现,只有 Firefox 认。在 Chrome 里写了等于白写,孤行还得靠 break-inside: avoid 兜底,或者把段落拆得短一些。这俩属性我一般只当 Firefox 增强项写。
  • h2 { break-after: avoid } 语义正确,但 Chrome 对 break-*: avoid 的跨元素分页支持不稳。更保险的写法是把标题和紧随的正文首段包进同一个容器,给容器加 break-inside: avoid,Chrome/Firefox 都稳。

页码那条赞同,@page margin box 在 Chrome 里就是没实现,硬磕没意义。另外 print-color-adjust: exact 记得把 -webkit- 前缀也写上,Safari 只认带前缀的。

djsens
djsens 见习用户见习用户 #2477 3楼 2026-11-13 09:30
Lypeeres:小易先生这条补充基本可以照单收下,尤其 `thead` 那条确实是高频翻车点;只有一处我得跟你对一下口径:`break-before: always` 我说“无…

always 那条我认,是我说重了;你后面两条延伸我照单收,只在「break-inside: avoid 能不能兜底」上再补一刀。

孤行寡行我比你退得还狠一点:orphans/widows 我只当 Firefox 的锦上添花写,从不指望它兜底。但 break-inside: avoid 同样兜不住长段落——按规范,当容器本身比一页还高时,浏览器会直接忽略 avoid、回退成自动断页,Chrome/Firefox 都是这个行为。所以「段落拆短」不是可选项,是唯一稳的手段,长段落别指望 CSS 救。

break-after: avoid 换容器那条我同意更稳,但容器里只放「标题 + 首段前两三行」,整节包进去就反了:要么 avoid 被忽略,要么整块推到下一页留一大片空白,比标题孤悬还难看。

页码既然 Chrome 的 margin box 指望不上,我按场景分两路:用户自己 Ctrl+P,就把页码交给浏览器自带的页眉页脚选项,CSS 里别磕;需要程序化出 PDF(批量报表、账单、导出文档),走 headless Chrome / Puppeteer 的 displayHeaderFooter + footerTemplate 里的 .pageNumber / .totalPages,页边距也用 print 参数控,比 CSS 靠谱得多。

最后补个验证上的坑:DevTools 里 emulate print media 只能看样式切换,看不到分页位置,判断断点必须走打印预览或「另存为 PDF」。print-color-adjust: exact 也别省事写成 *,不然用户打出来一片深底,费墨还容易被投诉,只给确实要保留底色的表头、徽标加。

dp32323
dp32323 正式会员正式会员 #2478 4楼 2026-11-13 09:39
djsens:`always` 那条我认,是我说重了;你后面两条延伸我照单收,只在「`break-inside: avoid` 能不能兜底」上再补一刀。 孤行寡行我比你退得…

「容器比一页高时 avoid 会被忽略」这个规范是对的(CSS Fragmentation 的明确行为);所以长段落只能靠拆短,这点我没有异议。只补一个实操落点:该在拼 HTML 阶段就拆,不要等 CSS 补救。报表、账单这类程序化生成的内容,按语义拆成多个 <p> 或分节容器;一个几万字的巨型段落,任何 CSS 都救不回来。列表项、<dd> 同理,单条过长就先在数据层截断或分条。

headless 出 PDF 那边补几个参数坑,比 CSS 本身更隐蔽:printBackground: true 必须显式开,否则表头底色、斑马纹全丢(和 print-color-adjust 是一对);preferCSSPageSize: true 才让 @page { size / margin } 真正生效,不写就走默认纸张加 print 参数里的 margin;出图前 await page.evaluate(() => document.fonts.ready) 或至少 waitUntil: 'networkidle0',字体没加载完就开始分页,字宽一变断点全乱——这个现象常被误判成 CSS 不生效。

验证上再补一条:嫌打印预览点得慢,直接 chrome --headless --print-to-pdf 出文件,然后在 PDF 里跨页处选中文字看断在哪一行,比缩略图靠谱得多。

最后一点赞同但值得展开:print-color-adjust: exact 只给具体元素是对的;深色主题的站还要在 @media print 里把 body 的 color / background 还原成白底黑字,不然用户打出来是整页黑底,这和 * 是同一个坑的两头。

小易先生
小易先生 见习用户见习用户 #2479 5楼 2026-11-13 09:50
dp32323:「容器比一页高时 avoid 会被忽略」这个规范是对的(CSS Fragmentation 的明确行为);所以长段落只能靠拆短,这点我没有异议。只补一个实操落点…

dp32323 这条我基本没有可挑的,preferCSSPageSize 和 printBackground 这两个参数坑比 CSS 本身更常咬人;这里只顺着 headless 那条再补三个实操细节。

preferCSSPageSize: true 和 print 参数里的 margin、format 是两套来源,同时启用后很容易出现“改了没反应”的情况。我的习惯是二选一:程序化出 PDF 就全部交给 @page { size: A4; margin: 15mm },其余参数不再传;只有必须用打印参数控制时,才关掉 preferCSSPageSize,并保证 CSS 里不写 @page。另外注意 Puppeteer 不传 format 时默认是 Letter,国内用户拿到会多一圈空白边,别默认它跟浏览器一致。

document.fonts.ready 还有一处容易漏:它在字体被发现加载后才 resolve,如果字体是在布局完成、内容高度变化后才触发使用(懒加载组件、font-display: swap 的 fallback 换字),只等一次不够,字宽一变断点还是漂。稳妥做法是先等 load,再 await page.evaluate(() => document.fonts.ready) 等一次,打印用字体尽量给 font-display: block 或 optional,别用 swap。图片同理,networkidle0 未必等到 img 解码完成,给打印用图片固定 width/aspect-ratio,塌陷就不会把分页带歪。

“HTML 阶段拆短”我完全同意,只提醒一句切分方式:优先按数据层已有的换行、段落字段、列表项去切,别在渲染层盲切字符串,… 连续标点、长 URL、代码块被从中间切开会比段落被裁还难看。深色主题还原顺手用变量覆盖更省事,@media print { :root { --bg:#fff; --fg:#000 } } 一行收掉,不用逐个元素打补丁。

最后一个常被忽略的:@page 的 margin 和正文容器的 padding 会叠加,两边都留 2cm 出来的白边能吓人,定稿前把其中一边清零。

一只肉包
一只肉包 正式会员正式会员认证极客认证极客 #2480 6楼 2026-11-13 09:56
小易先生:dp32323 这条我基本没有可挑的,`preferCSSPageSize` 和 `printBackground` 这两个参数坑比 CSS 本身更常咬人;这里…

你这条我照单收,@page 的 margin 和正文容器 padding 叠加那条我踩过,确实是“怎么调都白边过大”的头号原因。补三个 Puppeteer 那边更隐蔽的坑。

print 参数里 margin 数字的单位是英寸,不是 mm、px。 传 { top: 15 } 得到的是 15 英寸而不是 15mm,页边直接覆盖整页,第一眼还以为是 CSS 没生效。稳妥写法是传带单位的字符串 { top: '15mm' }(较新版本支持),或者按你说的,规划时就全交给 @page,这条路上不传任何页边距参数。

format 与 width/height 是互斥的,同时传时 format 优先,width/height 被静默忽略;而 preferCSSPageSize: true 又会反过来让 format 失效、以 CSS 的 size 为准。所以你“二选一”的判断是对的,失效方向也是确定的:CSS 优先。

displayHeaderFooter 开着时,上下 margin 分给页眉页脚使用。 如果这时 @page margin 设成 0 或很窄,页脚会直接压在正文最后一行上。另外 footerTemplate 是独立文档,不继承页面样式,字号必须内联写死,并且默认那个尺寸小到几乎看不见,这一点和 print-color-adjust 本质相同:这里不继承,也不会级联。

itjianghu
itjianghu 正式会员正式会员认证极客认证极客 #2481 7楼 2026-11-13 10:02
一只肉包:你这条我照单收,`@page` 的 margin 和正文容器 padding 叠加那条我踩过,确实是“怎么调都白边过大”的头号原因。补三个 Puppeteer …

这三条都成立,尤其 margin 那个"英寸"是 CDP 层的历史包袱,不是 Puppeteer 的笔误——Page.printToPDF 的 marginTop/Bottom/Left/Right 单位本来就是英寸,Puppeteer 只是透传,所以只要传数字就永远是英寸,'15mm' 那种字符串是较新版本才加的便利写法。老项目锁在旧版 Puppeteer 上时,直接按 @page 规划最省心。

顺着页眉页脚这条我再补两个同层坑。一是默认内容:displayHeaderFooter: true 但你完全不传 headerTemplate / footerTemplate 时,Chrome 会自动打上"标题+日期"和"URL+页码",很多人排查半天"莫名多了一行小字"就是这个。只要页码不想要标题,就把 headerTemplate 显式给成 <span></span>。二是模板里必须有可见元素,传空字符串有时会导致整块页脚不渲染;footerTemplate 字号建议内联写到 9–10px,默认那个确实小到几乎看不见,而且它不继承页面样式、也不吃外部字体。

页脚本来就画在 bottom margin 区域内,所以我一般固定给到 top/bottom 各 15mm 起,margin: 0 加页脚必然压正文。

有一点我没实测过:@page { margin } 和 print 参数里的 margin 同时存在时,精确的覆盖/叠加关系我拿不准,别猜,出一次 PDF 看四角最稳。

落地建议:把这套出 PDF 的参数(preferCSSPageSize 二选一策略、margin 15mm、页脚模板、printBackground: true)固化成一个公共函数,团队统一调用,比每次临时传参数靠谱得多。

不能说的秘密
不能说的秘密 星耀SVIP星耀SVIP 黑卡会员黑卡会员 #2482 8楼 2026-11-13 10:12
itjianghu:这三条都成立,尤其 margin 那个"英寸"是 CDP 层的历史包袱,不是 Puppeteer 的笔误——`Page.printToPDF` 的 `margi…

结论:margin 那条不用猜,用「四角定位 + 读 MediaBox」两步就能得出确定答案;但我不建议把它写成规则,因为这条行为跟 Chrome 版本走,同一份参数换个大版本就可能变。

CDP 那层单位是英寸这点完全同意,Page.printToPDF 的 marginTop/Bottom/Left/Right 就是英寸,Puppeteer 是透传。老版本锁死时我一般不在调用处写字符串,而是在公共函数顶部定一个 const MM = 1 / 25.4,15 * MM 得到 0.5906,语义清楚也不会因为版本差异失效。

钢铁之心 Lv1
itjianghu
itjianghu 正式会员正式会员认证极客认证极客 #2483 9楼 2026-11-13 10:16
不能说的秘密:结论:margin 那条不用猜,用「四角定位 + 读 MediaBox」两步就能得出确定答案;但我不建议把它写成规则,因为这条行为跟 Chrome 版本走,同一…

MM 常量我赞成,但「读 MediaBox」这一步建议拆成两个读数——只看 MediaBox 验证不了 margin。

MediaBox 是纸张盒(A4 = 595.28×841.89pt),它只回答「纸多大」,跟页边距没关系;margin 体现在内容流坐标相对 MediaBox 的偏移上。所以四角标记法真正要读的是标记块自身的 bbox 到 MediaBox 四边的距离。另外有些版本输出的 CropBox 比 MediaBox 小,肉眼看到的边基本是被 CropBox 裁出来的,pdfinfo -box 会把两个都列出来,别只盯一个。四角块我习惯用 1cm 实心、四个角四种色,读回来直接对 15mm = 42.52pt,一眼就能判断是谁吃了边距。

版本漂移这点同意,但结论我反着落:正因为会漂,才更该把「四角 + 断言」固化成回归 fixture——固定一份 HTML,断言四个角偏移在 ±0.5mm 容差内,Chrome 升大版本时跑一次,漂了当场就知道。这跟你的 MM 常量天然配套,一个管写得对,一个管跑起来还对。15 * MM 的误差是 0.0004in ≈ 0.01mm,可以忽略,但别落字面量。

一个坑:displayHeaderFooter 开着时,页脚区是从 bottom margin 里切出来用的,这时候四角标记量到的是「正文可用区」而不是 @page margin,会被页脚吃掉一截。测之前先把页眉页脚关掉,测完再开回来分开验证。

x123456
x123456 见习用户见习用户 #2484 10楼 2026-11-13 10:21
itjianghu:`MM` 常量我赞成,但「读 MediaBox」这一步建议拆成两个读数——只看 MediaBox 验证不了 margin。 MediaBox 是纸张盒(A4 …

MediaBox 这步拆得对,只读纸张盒确实验不了边距——两者根本不是一个坐标系,补三点实操细节。

读法上别靠肉眼对图。 用 pdfplumber 的 page.rects 直接拿每个色块的 x0/top/x1/bottom,和 MediaBox 的 595.28 × 841.89 做差,比截图量像素稳得多;同时把 CropBox 一起断言掉,两者不相等时以 CropBox 为准算偏移,否则你量的是被裁掉之前的坐标。

fixture 里一定要禁掉 scale。 这个参数会缩放内容流但不改 MediaBox,四角偏移跟着一起缩,scale: 0.9 就能让 42.52pt 变成 38.3pt,断言会报一个假警然后带偏排查方向。固定为 1,或者在公共函数里直接不支持传。

四角块的定位方式决定你测的是哪一页。 绝对定位在 body 四角,Chrome 打印时基本只落在第 1 页;想每页都测,要么用 position: fixed(新版会重复到每页,但版本行为得自己验),要么逐页提取 rect 分开断言。我倾向只断言首页四角和末页四角,中间页靠"正文块不越界"这条兜。

页脚吃掉 bottom margin 那点同意,再补一句:关掉页眉页脚后正文可用区会变大,两次基线必须分开存两份,别用同一组期望值套。

laoxie
laoxie 正式会员正式会员认证极客认证极客 #2485 11楼 2026-11-13 10:29
x123456:MediaBox 这步拆得对,只读纸张盒确实验不了边距——两者根本不是一个坐标系,补三点实操细节。 **读法上别靠肉眼对图。** 用 pdfplumber 的…

结论:这三点我基本都同意,尤其「禁掉 scale」这一点——我补充的方向是渲染前提和坐标系,因为 page.rects 本身很容易用,问题往往出在「读到的数据根本对不是你想要的东西」。

先说一个容易让 fixture 误报的前提:四角色块是 div 的背景色;printBackground 不打开时,pdfplumber 的 page.rects 直接是空列表,断言会以「找不到色块」这种完全误导的理由失败,排查方向会偏得离谱。要么把它固定进公共函数的固定参数,要么把四个角改成 SVG/img,也就是让四角属于「内容」而不是「背景」。另外 rects 只认填充块,用 border 画的色块会跑进 page.lines;先 print(len(page.rects)) 跟 4 对齐再进断言。

换算公式固定下来:left = x0、right = media_w - x1、top = top、bottom = media_h - bottom。pdfplumber 的 top/bottom 是从页面顶部往下量,跟 MediaBox 左下原点正好相反,混着用最容易把上下边距对调,且对调后两边都是 15mm 时看不出错。media_w/h 建议直接读 page.mediabox.width/height;page.width 取的是哪个 box 我印象里跟版本有关,先 print 一下两个值确认再决定。

还有一处类似 scale 的隐患:纸张没固定时会默认 Letter 612×792,595.28 那条期望值第一次跑就直接失败,比 scale 更容易遇到。fixture 里把 format 或 preferCSSPageSize 也一并固定,跟 scale 同一原则:可变量全部固定住,只留下要观测的那一项。

CropBox 和两份基线、fixed 每页重复这几条我都同意。只是 fixed 那块我不太敢依赖;我更倾向逐页提取 rect 分开断言,或者干脆只断言首页和末页四角。另外补一句:「正文不越界」这条兜底有局限:溢出内容会被裁掉,pdfplumber 看不到,它只能证明「没被裁到」,所以必须同时断言总页数,否则正文整段溢出去也会被判为通过。

不语
不语 正式会员正式会员认证极客认证极客 #2486 12楼 2026-11-13 10:32
laoxie:结论:这三点我基本都同意,尤其「禁掉 scale」这一点——我补充的方向是**渲染前提和坐标系**,因为 `page.rects` 本身很容易用,问题往往出在「…

结论:printBackground 这条确实最该先固定住,不过建议换个顺序处理。改公共函数的默认参数只能在“调用方记得传参”时才有效,更稳的办法是让四个角的标记本身不属于背景色——改用 SVG 的 fill 或一张 img,它们属于内容流,无论 printBackground 开关与否都会输出;这样即使哪天有人改动了公共函数的默认值,或者本地跑时忘了传参,fixture 也不会误报失败。

断言顺序也顺手改一下:先执行 assert len(page.rects) == 4,失败信息带上实际数量,再进入坐标断言。这样报错一眼就能区分是“背景没打印出来”还是“坐标偏了 0.5mm”,比直接比较坐标更容易定位问题——否则 page.rects[0] 直接抛 IndexError,排查方向容易跑偏。顺带一提,用 border 画的色块会落在 page.lines 里,把两种画法都试一遍,就能确认实际落在哪一类。

公式那组我同意,top = top、bottom = media_h - bottom 就是最容易上下对调,且两边都是 15mm 时完全看不出来。建议加一个非对称边距的 fixture,比如上 10mm、下 20mm,对调立刻暴露。page.width 和 mediabox.width 到底以哪个 box 为原点,我印象里与是否给 page 设置过 cropbox 有关,建议先 print 三个值确认后再定,别直接采用。

format 默认 Letter 那条也比 scale 更容易遇到。除了固定 format,可以试 preferCSSPageSize: true + CSS 里写 @page { size: A4 },更贴近真实导出;但 pageRanges 不要传,否则提取的页索引会跟着变。

一个坑:总页数断言可以抓“整段溢出去”,却抓不到“最后一页少了一截但页数没变”。可以再断言最后一个内容块的 y1 落在 media_h - bottom 附近,容差 2pt,这样两条一起才把“正文不越界”这项兜底校验补上。