How to support dynamic page header with openhtmltopdf?
openhtmltopdf是一个纯Java的转换
静态HTML页面为PDF的轻量级库,很适合输出静态页面为PDF文件,即将静态的、样式正确的 HTML/CSS 高质量地转换为 PDF。
但是openhtmltopdf不能完整解释HTML/CSS,因此存在诸多的限制:
无 DOM 操作(最关键的限制):在 PDF 生成过程中,无法修改 已解析和布局好的 DOM 结构。这意味着你不能用 document.createElement, appendChild, innerHTML 或类似的方法来动态添加、删除或修改页面元素,并期望这些变化会反映在最终的 PDF 上。
举例来说,我们可以使用 @Page 的 content element(header) 和 running(header)来实现页眉,例如:
<html>
<head>
<style>
@page {
size: A4;
margin: 2cm;
@top-center {
content: element(header);
}
@bottom-center {
content: "第 " counter(page) " 页 / 共 " counter(pages) " 页";
}
}
div.header {
display: block;
position: running(header);
text-align: center;
font-weight: bold;
font-size: 14px;
}
div.content {
font-size: 12px;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="header">我的文档页眉</div>
<div class="content">
<p>这里是正文,会根据内容自动分页。</p>
<p>...</p>
<p>很多很多内容...</p>
</div>
</body>
</html>
但是这个方法有个很大的缺点,就是报表内容体部分(content)和header部分内容如果是动态的话,会和页眉部分重叠!如果生成的页面是完全静态的的,则没有这个问题,但是如果使用thymeleaf 的动态文本语法例如 <p th:text="${line1}"></p>就会导致重叠问题。
<html xmlns:th="http://www.thymeleaf.org">
<head>
<style>
@page {
size: A4;
margin: 2cm;
border: 1px solid black;
@top-center {
content: element(header);
}
@bottom-center {
content: "第 " counter(page) " 页 / 共 " counter(pages) " 页";
}
}
div.header {
display: block;
position: running(header);
text-align: center;
font-weight: bold;
font-size: 14px;
}
div.content {
font-size: 12px;
line-height: 1.5;
}
body {
padding: 0;
margin: 0;
font: 14px serif;
font-family: "MingLiU", serif;
margin: 0;
padding: 3px 10px;
}
</style>
</head>
<body>
<div class="header">
<table>
<tr>
<td style="width: 45%;border-right: 1px solid black; border-bottom: 1px solid black; text-align: center; vertical-align: middle; font-weight: bold;">
<p th:text="${line1}"></p>
<p th:text="${line2}"></p>
<p th:text="${line3}"></p>
</td>
<td style="width: 55%;border-bottom: 1px solid black;">
<table>
<tr th:each="row : ${headerRows}">
<td style="width: 35%" th:text="${row[1]}"></td>
<td th:text="${row[2]}"></td>
</tr>
</table>
</td>
</tr>
</table>
<table style="border-bottom: 1px solid black">
<tr th:each="row : ${subHeaderRows}">
<td style="width:35%" th:text="${row.left}"></td>
<td style="width:65%" th:text="${row.right}"></td>
</tr>
</table>
</div>
<div class="content">
<p>这里是正文,会根据内容自动分页。</p>
<p>...</p>
<p class="cjk" style="page-break-before: always;">第二页内容</p>
<p class="cjk" style="page-break-before: always;">第二页内容</p>
<p class="cjk" style="page-break-before: always;">第二页内容</p>
<p>很多很多内容...</p>
</div>
</body>
</html>

当然,我们可以把content的margin用代码设置为页眉之下,例如:
<style th:text="'@page { margin: 100pt 30pt 40pt 30pt; }'"></style>
但是这样处理后,无法支持动态高度的header,即如果header的总体高度是这个,我们可以用上面的方法搞定,如果header高度不固定,我们可以使用下面的方法实现:
利用表头在打印的时候,每页会自动重复来实现该功能。
其中,关键点:
-fs-table-paginate: paginate;
-fs-page-break-min-height: 1.5cm;
完整示例代码如下:
<html xmlns:th="http://www.thymeleaf.org">
<head>
<style>
@page {
size: A4;
border: 1px solid black;
@bottom-left {
content: "Printed by: System \A Printed on: 2025-06-19";
white-space: pre;
font: 10px Helvetica;
}
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font: 10px Helvetica;
}
}
table {
width: 100%;
border-collapse: collapse;
-fs-table-paginate: paginate;
-fs-page-break-min-height: 1.5cm;
}
td {
padding: 3px 10px;
word-wrap: break-word;
word-break: break-all;
overflow-wrap: break-word;
white-space: normal;
}
body {
padding: 0;
margin: 0;
font: 14px serif;
font-family: "MingLiU", serif;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>
<table>
<tr>
<td style="width: 45%;border-right: 1px solid black; border-bottom: 1px solid black; text-align: center; vertical-align: middle; font-weight: bold;">
<p th:text="${line1}"></p>
<p th:text="${line2}"></p>
<p th:text="${line3}"></p>
</td>
<td style="width: 55%;border-bottom: 1px solid black;">
<table>
<tr th:each="row : ${headerRows}">
<td style="width: 35%" th:text="${row[1]}"></td>
<td th:text="${row[2]}"></td>
</tr>
</table>
</td>
</tr>
</table>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="content">
<span class="cjk" th:text="${title}"></span><span th:text="${subTitle}"></span>
<p th:each="p : ${para1}" th:text="${p}"></p>
<p class="cjk" th:utext="${pppp}"></p>
<p class="cjk" style="page-break-before: always;">第二页内容,
<div style="color:red">红色内容</div>
</p>
<p th:each="p2 : ${para2}" th:text="${p2}"></p>
<p class="cjk" style="page-break-before: always;">第三页内容</p>
<p th:each="p2 : ${para3}" th:text="${p2}"></p>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>
生成效果如下,完美解决动态页眉: