jquick-pdf 防止 PDF 内容分页断裂:keepTogether 属性妙用
引入
业务 PDF 的分页问题通常不是"能不能换页",而是"应该在哪里换页"。订单摘要的标题落在页尾、金额卡片被拆成两半、审批结论和签名区分开,都会让文件显得不专业。直接用 PdfBox 绘制时,需要自行计算剩余高度并决定是否新建页面;内容一旦动态变化,坐标判断很快变得脆弱。jquick-pdf 提供 keepTogether,让模板可以明确表达"这个块尽量整体开始"。
核心讲解
布局语义
keepTogether 是写在元素 style 中的样式属性,值使用 true 或 false,只对块级元素有意义,尤其是包含标题、字段和结论的 <div>。它的语义是布局意图而不是绝对约束:当块被拆开时,布局引擎会倾向于把整块移到下一个有足够空间的位置开始。
分页决策发生在布局阶段
渲染块之前,布局引擎会估算该块所需空间,并检查当前光标与页面底部边距的关系。当剩余空间不足且块设置了 keepTogether:true 时,引擎先创建新页,再从新页开始绘制。这个策略把分页决策放在布局阶段,而不是等文字已经画到页尾以后再回滚。理解这一点很重要:keepTogether 保护的是块的起始位置和整体布局意图,它不能让一份超过整页的内容永不分页。
自然分页与强制分页的区别
keepTogether属于自然分页:内容仍按顺序流动,只是调整某个块的起始页。<htmlPageBreak>与<areaBreak>属于显式分页,会直接从指定位置开始新页。
前者是自然布局中的空间控制,后者是强制换页方式,三者职责不同:不要为了避免一个小卡片断裂,就把整份文档都插入强制分页,那样容易产生大片空白。
依赖与示例资源
核心依赖仍是 jquick-pdfx。分页相关示例如 style/style.txt 中的 keepTogether:true,布局目录还有 htmlPageBreak.txt 和 areaBreak.txt。
关键细节
样式属性
分页块通常同时使用 padding、marginBottom、border、backgroundColor 和 keepTogether。width、height、minHeight、maxHeight 控制盒子尺寸;文字使用 fontSize、fontColor、bold 和 textAlignment。尺寸单位支持 px、pt、mm、cm、in。边框简写按仓库示例使用类型、宽度和颜色组合,例如 border:solid 1px #333,不要照搬浏览器中未经验证的写法。
临界高度的判断
布局引擎的空间判断存在临界点,因此只用"短样例"验证是不够的。验证时应准备三组数据:刚好能放下的短数据、只差几行空间的临界数据,以及远超一页的长数据。观察重点不是文件是否生成成功,而是新页是否按预期出现、边框和内边距是否完整、后续正文是否继续绘制。PDF 视觉回归比单纯断言字节数组更有价值,因为分页问题通常体现在位置和空白区域上。
内容超过一页时的降级策略
第一,块内容大于整页时,keepTogether 无法创造空间,只能拆分模板,把长内容切成多个小段。第二,过多使用会造成大片留白,页面效果甚至比自然断页更糟。第三,动态字体、长客户名称和多行地址都会改变估算高度,不能只用短样例验证。第四,keepWithNext 在样式模型中可解析,但当前测试说明其渲染消费边界需要自行验证,不能把它当作本篇分页方案的替代品。第五,生成失败时要保留模板版本和订单编号以便复现,但不要输出完整客户隐私。
分层设计:外层小块、内层自然流动
实际设计可以采用"外层小块、内层可流动"的方式。审批卡片中的标题、审批人和结论放在一个 keepTogether 的 div 中;结论正文如果可能很长,就拆成独立的 p 或多个小段。这样既保护了卡片的识别信息,又不会因为一段异常长的意见导致整块无法放置。订单明细则按订单或商品分组,分组标题与第一行摘要放在一起,明细主体允许正常分页。长表格不要把所有行包进一个 keepTogether div,应以表头、分组或小节为单位。
实战说明
依赖
xml
<dependency>
<!-- 引入分页布局和 PDF 渲染能力 -->
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-pdfx</artifactId>
<version>4.0.0</version>
</dependency>
环境为 JDK 8+。建议先用固定高度和固定数据验证分页,再加入真实订单明细和长文本。
KeepTogetherDemo
java
import com.github.paohaijiao.executor.JQuickPdfFactory;
import java.nio.file.Files;
import java.nio.file.Paths;
public class KeepTogetherDemo {
public static void main(String[] args) throws Exception {
String template = "<pdf><body>"
+ "<h1>'订单确认单'</h1>"
// 用正文把光标推近分页边界,便于观察分页行为
+ "<p>'以下是订单处理说明,正文用于把光标推近分页边界。'</p>"
+ "<p>'系统将在审核完成后通知客户。'</p>"
// 整块设置 keepTogether,避免摘要被拆开
+ "<div style=\"keepTogether:true;border:solid 1px #333;padding:12px\">"
+ "<h2>'订单摘要'</h2>"
+ "<p>'订单号:'${orderNo}</p>"
+ "<p>'客户:'${customer}</p>"
+ "<p>'含税金额:'${amount}</p>"
+ "<p>'处理状态:'${status}</p>"
+ "</div>"
+ "<p>'摘要之后的说明从下一个可用位置继续。'</p>"
+ "</body></pdf>";
// 绑定订单数据并渲染
byte[] pdf = JQuickPdfFactory.create()
.bind("orderNo", "SO-20260914-001")
.bind("customer", "示例客户")
.bind("amount", "12,800.00 元")
.bind("status", "待审核")
.executeContent(template);
// 保存 PDF 结果
Files.write(Paths.get("keep-together.pdf"), pdf);
}
}

需要区分模板属性与 Java API:keepTogether 不属于工厂方法,只能写在元素的 style 里。executeContent 适合最小验证,资源模板可用 executeResource。
适用场景
适合保护的对象是"标题 + 少量内容"的完整业务块:订单摘要、审批结论、风险提示、签名栏和报告结论。先让正文自然流动,只有在块确实被拆开时才加 keepTogether。若必须从某个业务节点开始新页,可以使用示例中的 <htmlPageBreak> 或 <areaBreak>,但强制分页会增加空白页和版式波动风险,自然分页与强制分页应当组合使用而不是互相替代。这一策略可用于订单确认、发票说明、审批意见、信用报告结论、合同条款摘要和签名区。
方案对比与性能
手写 iText/PdfBox 需要业务代码计算剩余高度,灵活但维护成本高;jquick-pdf 用样式表达布局意图,适合规则块和模板协作。浏览器 CSS 的 page-break-inside 不能直接推断为 jquick-pdf 属性,必须使用仓库实际的 keepTogether:若需要复杂分页算法、目录和 PDF 对象控制,底层库可能更合适;若重点是稳定生成业务页面,模板方案更清晰。
性能方面,分页估算本身通常不是瓶颈,超长文本、图片和大量块才会放大布局成本。把一个巨型报告拆成语义清晰的小块,避免给每个节点都设置 keepTogether;批量任务控制并发和最大页数,给异常长字段设置业务上限。压测时同时覆盖"刚好放下""差一点放不下""块超过整页"三种情况,观察耗时、文件大小和空白比例。
总结
keepTogether 不是"禁止分页"的开关,而是给布局引擎的明确提示:这个完整业务块应尽量从有足够空间的位置开始。它适合保护整块识别信息,配合外层小块加内层自然流动的分层设计,分页才会既稳定又不浪费页面。
常见误区是把 keepTogether 当成绝对约束,或为避免一处断裂而给整份文档插入强制分页;此外 keepWithNext 当前只确认了解析层能力,不能作为分页承诺。建议把分页测试加入 CI,使用固定长文本、不同字体和临界高度样例,再结合表格、htmlPageBreak 和 areaBreak 研究整份报告的章节编排。
版本基线:jquick-pdfx 4.0.0、JDK 8+,升级前请核对 README_zh.md 的版本对照表(不同版本的渲染引擎与许可证边界可能不同);更多示例见 GitHub 仓库。