首页  编辑  

openhtmltopdf实现动态页眉自适应高度变化

Tags: /Java/   Date Created:
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); /* 让 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); /* 让 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高度不固定,我们可以使用下面的方法实现:
利用表头在打印的时候,每页会自动重复来实现该功能。
其中,关键点:
            /* The magical table pagination property. */             -fs-table-paginate: paginate;             /* Recommended to avoid leaving thead on a page by itself. */             -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;

            /* The magical table pagination property. */
            -fs-table-paginate: paginate;

            /* Recommended to avoid leaving thead on a page by itself. */
            -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>
            <!-- 这里可以放置内容体,支持简单的HTML -->
            <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>
生成效果如下,完美解决动态页眉:
完整示例工程下载: demo1.zip
demo1.zip (6.2KB)