Maven依靠


<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;标签,可处理中文乱码问题


body {
    /*处理中文乱码*/
    font-family: SimSun, serif;
    /*主动换行*/
    word-break: break-all;
}

页眉和页脚

其实页眉和页脚能够经过定义的ftl模板来实现


<!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模板页面


<!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自行下载


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);
            }
        }
    }
}

调用测验


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生成pdf,并处理表格切割

图片水印办法


/**
 * 增加图片水印
 *
 * @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);
    }
}

文字水印办法


/**
 * 增加文字水印
 *
 * @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款式


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

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

Java生成pdf,并处理表格切割

表格分页被切割问题-办法二

在ftl模板中的style标签中增加css款式,需求注意的是表格的标题需求运用thead标签包裹,表格其他行用tbody标签包裹

<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>

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

Java生成pdf,并处理表格切割