Java生成pdf,并解决表格分割

Maven依赖

xml 复制代码
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf</artifactId>
    <version>9.1.16</version>
</dependency>

自定义ftl模板

该模板是由html页面直接后缀而成,模版名称定为template-01.ftl

注意事项

中文乱码问题

需要在模板中添加font-family: SimSun, serif;标签,可解决中文乱码问题

html 复制代码
body {
    /*解决中文乱码*/
    font-family: SimSun, serif;
    /*自动换行*/
    word-break: break-all;
}

页眉和页脚

其实页眉和页脚可以通过定义的ftl模板来实现

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>

    <style>
        /*页眉的上下左右边距*/
        @page {
            margin: 30mm 20mm 30mm 20mm;
        }

        @page {
            /*页眉*/
            @top-center {
                content: element(header)
            }
            /*页脚*/
            @bottom-center {
                content: element(footer)
            }
        }
        /*页眉*/
        #header {
            position: running(header);
            margin-top: 10mm;
        }
        /*页脚*/
        #footer {
            position: running(footer);
        }

        /*分页*/
        #page-number:before {
            content: counter(page);
        }
        
        /*分页*/
        #page-count:before {
            content: counter(pages);
        }

    </style>

</head>
<body>

<!--页眉-->
<div id="header">
    深圳市xxx有限公司
    <hr/>
</div>
<!--页脚-->
<div id="footer">
    页码<span id="page-number"></span>/<span id="page-count"></span>
</div>

</body>
</html>

完整ftl模板页面

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>

    <style>
        /*页眉的上下左右边距*/
        @page {
            margin: 30mm 20mm 30mm 20mm;
        }

        @page {
            /*页眉*/
            @top-center {
                content: element(header)
            }
            /*页脚*/
            @bottom-center {
                content: element(footer)
            }
        }
        /*页眉*/
        #header {
            position: running(header);
            margin-top: 10mm;
        }
        /*页脚*/
        #footer {
            position: running(footer);
        }

        /*分页*/
        #page-number:before {
            content: counter(page);
        }
        
        /*分页*/
        #page-count:before {
            content: counter(pages);
        }

        * {
            padding: 0;
            margin: 0;
        }

        body {
            /*解决中文乱码*/
            font-family: SimSun, serif;
            /*自动换行*/
            word-break: break-all;
        }

        .main {
            width: 100%;
            height: auto;
            margin: 0 auto;
            text-align: center;
        }

        table {
            width: 100%;
            border-collapse: collapse;
        }

        td, th {
            line-height: 20px;
            padding: 7px 5px;
            border: 1px solid #999999;
        }

    </style>

</head>
<body>

<!--页眉-->
<div id="header">
    深圳市xxx有限公司
    <hr/>
</div>
<!--页脚-->
<div id="footer">
    页码<span id="page-number"></span>/<span id="page-count"></span>
</div>

<div class="main">

    <h1>深圳市xxx有限公司</h1>

    <p style="margin: 30px 0 50px 0;text-align: left;">
        人在世俗的世界中行走着,在慢慢流逝的时间里静静等待着成年那一刻的全速奔跑。可漫长的等待过后却发现,形形色色的欲望与世俗观念像橡皮泥一样粘在身上,越积越重,最后竟无限膨胀,束缚了我们的双腿,减缓了我们的步伐。我们不能轻松上路,也不能全速奔跑。它们甚至遮蔽住我们的双眼,遮掩住我们纯真的心,让我们的脚步开始凌乱,旋转在灯红酒绿的花花世界里......
    </p>

    <table>
        <thead>
        <tr>
            <th>姓名</th>
            <th>年龄</th>
            <th>性别</th>
        </tr>
        </thead>
        <tbody>

        <#if !data?? || (data?size==0)>
            <tr>
                <td colspan="3">无</td>
            </tr>
        <#else>
            <#list data as item>
                <tr>
                    <td>${item.name}</td>
                    <td>${item.age}</td>
                    <td>${item.sex}</td>
                </tr>
            </#list>
        </#if>
        </tbody>
    </table>

</div>

</body>
</html>

PDF工具类

字体包SimSun.ttc、ArialUni.ttf自行下载

java 复制代码
package org.example;

import com.lowagie.text.pdf.BaseFont;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;

import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Locale;

/**
 * @author 苦瓜不苦
 * @date 2023/11/28 18:33
 **/
public class PDFUtil {


    /**
     * 模板生成器
     *
     * @param createFile 生成文件的路径
     * @param ftlName    模板名称
     * @param object     数据
     */
    public static void processTemplate(String createFile, String ftlName, Object object) {
 
        Configuration configuration = null;
        StringWriter writer = null;
        ByteArrayOutputStream outputStream = null;
        try {
            // 初始化模版
            configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
            writer = new StringWriter();
            outputStream = new ByteArrayOutputStream();
            // 加载模板目录
            configuration.setClassForTemplateLoading(MainApi.class, "/module");
            configuration.setClassicCompatible(true);

            ITextRenderer renderer = new ITextRenderer();
            // 设置字体
            ITextFontResolver fontResolver = renderer.getFontResolver();
            fontResolver.addFont("fonts/SimSun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            fontResolver.addFont("fonts/ArialUni.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            configuration.setEncoding(Locale.CHINA, "UTF-8");

            // 读取模板文件
            Template template = configuration.getTemplate(ftlName, "UTF-8");
            // 写入数据到模板中
            template.process(object, writer);
            writer.flush();
            // 获取填充好数据的html页面
            String html = writer.toString();
            renderer.setDocumentFromString(html);
            renderer.layout();
            // 通过html页面字符串转换成pdf文件
            renderer.createPDF(outputStream);
            renderer.finishPDF();

            return outputStream.toByteArray();

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (Objects.nonNull(outputStream)) {
                    outputStream.close();
                }
                if (Objects.nonNull(writer)) {
                    writer.close();
                }
                if (Objects.nonNull(configuration)) {
                    configuration.clone();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

调用测试

java 复制代码
public class Main {

    public static void main(String[] args) {
        String fromFile = "./" + System.currentTimeMillis() + ".pdf";
        String toFile = "template-01.ftl";

        List<JSONObject> data = new ArrayList<>();
        for (int i = 0; i < 60; i++) {
            JSONObject object = new JSONObject();
            object.set("name", "张三");
            object.set("sex", "男");
            object.set("age", "18");
            data.add(object);
        }

        JSONObject object = new JSONObject();
        object.set("data", data);

        byte[] bytes = PDFUtil.processTemplate(toFile, object);
        File file = FileUtil.writeBytes(bytes, fromFile);
        System.err.println(file);
    }
}

扩展情况

以上代码即可生成好一份PDF文档了,但是会存在一些问题,

表格的形式会被自动切割,出现以下情况

按照不同的需求,可以使用不同的方式来处理。

一是,当被分页时,每页都需要一个标题的存在。

二是,分页的头部和尾部需要闭合起来

还有扩展于图片水印或者文字水印的需求

图片水印方法

java 复制代码
/**
 * 添加图片水印
 *
 * @param bytes pdf字节
 * @return
 */
public static byte[] appendImageWatermark(byte[] bytes) {
    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
        PdfReader reader = new PdfReader(bytes);
        PdfStamper stamper = new PdfStamper(reader, byteArrayOutputStream);

        // 加载水印图片
        URL url = PDFUtil.class.getClassLoader().getResource("fonts/bg.png");
        Image image = Image.getInstance(url);
        // 设置等比缩放 图片大小
        image.scalePercent(20);
        // 自定义大小
        // image.scaleAbsolute(200,100);
        // 设置旋转弧度
        image.setRotation(0);
        // 设置旋转角度
        image.setRotationDegrees(0);

        // 创建PdfGState对象并设置透明度
        PdfGState gState = new PdfGState();
        // 填充透明度
        gState.setFillOpacity(0.3f);
        // 描边透明度
        gState.setStrokeOpacity(0.3f);

        // PDF总页数
        int total = reader.getNumberOfPages() + 1;
        for (int i = 1; i < total; i++) {
            Rectangle pageRect = reader.getPageSizeWithRotation(i);
            PdfContentByte content = stamper.getOverContent(i);
            content.saveState();

            content.setGState(gState);

            // 设置图片水印
            // 获取pdf每页的长宽
            float width = pageRect.getWidth();
            float top = pageRect.getTop();
            // 获取缩放之后水印图片的长宽
            float scaledWidth = image.getScaledWidth();
            float scaledHeight = image.getScaledHeight();
            // 通过计算将水印添加到中间
            float x = (width - scaledWidth) / 2;
            float y = (top - scaledHeight) / 2;
            content.addImage(image, scaledWidth, 60, 0, scaledHeight, x, y);

            content.restoreState();


        }
        stamper.close();
        reader.close();
        return byteArrayOutputStream.toByteArray();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

}

文字水印方法

java 复制代码
/**
 * 添加文字水印
 *
 * @param bytes pdf字节
 * @param text  水印文字
 * @param size  文字大小
 * @return
 */
public static byte[] appendTextWatermark(byte[] bytes, String text, Integer size) {
    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
        PdfReader reader = new PdfReader(bytes);
        PdfStamper stamper = new PdfStamper(reader, byteArrayOutputStream);

        // 创建PdfGState对象并设置透明度
        PdfGState gState = new PdfGState();
        // 填充透明度
        gState.setFillOpacity(0.3f);
        // 描边透明度
        gState.setStrokeOpacity(0.3f);

        // 加载字体
        BaseFont baseFont = BaseFont.createFont("fonts/ArialUni.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

        // PDF总页数
        int total = reader.getNumberOfPages() + 1;
        for (int i = 1; i < total; i++) {
            Rectangle pageRect = reader.getPageSizeWithRotation(i);
            PdfContentByte content = stamper.getOverContent(i);
            content.saveState();

            // 获取pdf每页的长宽
            float width = pageRect.getWidth();
            float top = pageRect.getTop();
            // 通过计算将水印添加到中间
            float x = (width - (size * text.length())) / 2;
            float y = (top - size) / 2;

            // 设置字体水印
            content.beginText();

            content.setGState(gState);
            // 字体
            content.setFontAndSize(baseFont, size);
            // 颜色
            content.setColorFill(Color.BLACK);
            // 水印位置
            content.showTextAligned(Element.ALIGN_LEFT, text, x, y, 30);
            content.endText();
            content.restoreState();

        }
        stamper.close();
        reader.close();
        return byteArrayOutputStream.toByteArray();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

表格分页被切割问题-方式一

在ftl模板中的style标签中添加css样式

html 复制代码
tr {
    page-break-inside: avoid;
    page-break-after: auto;
}

同一个表格在分页时,会被自动添加上下边框

表格分页被切割问题-方式二

在ftl模板中的style标签中添加css样式,需要注意的是表格的标题需要使用thead标签包裹,表格其他行用tbody标签包裹

html 复制代码
<style>

table {
    page-break-inside: auto;
    -fs-table-paginate: paginate;
    border-spacing: 0;
}

tr {
    page-break-inside: avoid;
    page-break-after: auto;
}

</style>

<body>

<table>
    <thead>
    <tr>
        <th>姓名</th>
        <th>年龄</th>
        <th>性别</th>
    </tr>
    </thead>
    <tbody>

    <#if !data?? || (data?size==0)>
        <tr>
            <td colspan="3">无</td>
        </tr>
    <#else>
        <#list data as item>
            <tr>
                <td>${item.name}</td>
                <td>${item.age}</td>
                <td>${item.sex}</td>
            </tr>
        </#list>
    </#if>
    </tbody>
</table>

</body>

被分页时,表格的标题也会携带下来

相关推荐
就玩一会_1 分钟前
谷粒商城-消息队列Rabbitmq
java·rabbitmq·java-rabbitmq·谷粒商城
Viktor_Ye3 分钟前
实现金蝶云星空与钉钉数据无缝集成的技术方法
java·大数据·钉钉
程序员学姐12 分钟前
基于SpringBoot+Vue的高校社团管理系统
java·开发语言·vue.js·spring boot·后端·mysql·spring
.生产的驴14 分钟前
Docker Seata分布式事务保护搭建 DB数据源版搭建 结合Nacos服务注册
数据库·分布式·后端·spring cloud·docker·容器·负载均衡
南宫生35 分钟前
力扣-位运算-1【算法学习day.41】
java·学习·算法·leetcode
极客先躯42 分钟前
高级java每日一道面试题-2024年11月22日-JVM篇-说说堆和栈的区别?
java·jvm··
2401_857439691 小时前
企业OA管理系统:Spring Boot技术应用与优化
java·spring boot·后端
2401_857439691 小时前
Spring Boot OA:构建企业级办公自动化平台
java·spring boot·后端
咸芝麻鱼1 小时前
Django数据迁移出错,解决raise NodeNotFoundError问题
后端·python·django
paterWang2 小时前
小程序-基于java+SpringBoot+Vue的农场管理系统设计与实现
java·spring boot·小程序