freemarker生成pdf,同时pdf插入页脚,以及数据量大时批量处理

最近公司有个需求,就是想根据一个模板生成一个pdf文档,当即我就想到了freemarker这个远古老东西,毕竟freemarker在模板渲染方面还是非常有优势的。

准备依赖:

xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>html2pdf</artifactId>
            <version>3.0.5</version>
        </dependency>
        <!--pdf 支持中文(默认不支持)-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13</version>
        </dependency>

我这里不想选freemarker版本,直接用spring集成的省事。

配置一下freemarker的配置

java 复制代码
import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@org.springframework.context.annotation.Configuration
public class FreemarkerConfig {

	// 我这里为了省事,不想创建那么多的Configuration,而且创建Configuration太多不好
    @Bean(name = "cfg")
    public Configuration freemarkerConfigurer() throws IOException {
    	// 选择版本,不同版本对不同的模板语法或者模板转换也会有差异,如果你css 样式比较新,建议选高版本准没错
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        // 选择你存放模板的位置
        final ClassPathResource classPathResource = new ClassPathResource("templates");
        cfg.setDirectoryForTemplateLoading(classPathResource.getFile());
        cfg.setDefaultEncoding("UTF-8");
        // 模板异常处理
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        return cfg;
    }
}

然后我们准备下我们的ftl模板【freemarker的模板文件】----pdf.ftl

freemarker框架类似于beetlthymeleafjspVelocity等模板引擎

JSP就不用说了吧,基本上开发Java的基本上都会了解开发过

html 复制代码
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<style>
    .logo {
        width: 320px;
        height: 80px;
    }
    .user-info {
        padding: 10px 10px;
        background: RGB(221, 235, 247);
    }

    .label {
        width: 150px;
        padding: 0 20px 0 0;
        text-align: left;
    }

    .time {
        width: 100px;
        margin-right: 10px;
        text-align: center;
    }

    .label, .time {
        display: inline-block;
    }

    .range-time {
        margin: 20px 0;
    }

    .table-data {
        width: 100%;
    }
    table{
        border-collapse: collapse;
    }
    .header > th {
        text-align: left;
        height: 50px;
    }
    .divider-line {
        margin: 20px 0;
        height: 3px;
        background: #000;
    }
    .desc {
        padding: 15px 10px;
    }
    .date {
        width: 100px;
    }
    .fee {
        width: 50px;
    }
    .name {
        width: 140px;
    }
    .name, .fee, .date {
        padding: 0 10px;
    }
    .download-date {
        text-align: right;
    }
    .bottom-footer-tip {
        width: 100%;
        margin-top: 300px;
        font-size: 12px;
        transform: scale(.9);
    }
</style>
<body>

<div class="download-date">Download on 2022/2/2</div>
<div class="range-time">
    <span class="time">${startTime}</span>
    <span>to</span>
    <span class="time">${endTime}</span>
</div>

<table class="table-data">
    <tr class="header">
        <th>Date</th>
        <th>Name</th>
        <th>desc</th>
        <th>fee</th>
    </tr>
    <tr class="divider-line">
        <th></th>
        <th></th>
        <th></th>
        <th></th>
    </tr>
    <#list list as item>
        <tr>
            <td class="date">${item.date}</td>
            <td class="name">${item.name}</td>
            <td class="desc">
                <div>${item.desc}</div>
            </td>
            <td class="fee">${item.fee}</td>
        </tr>
    </#list>
    <tr class="divider-line">
        <th></th>
        <th></th>
        <th></th>
        <th></th>
    </tr>
</table>
</body>
</html>

这里ftl的语法,我就不多做解释了,我这里附上freemarker官方文档,感兴趣的自己去学习一下。

然后准备下我们的代码处理逻辑

首先是PDF实体数据

java 复制代码
import lombok.Data;

import java.util.List;

@Data
public class PDFData {
    private String logo;
    private String name;
    private String address;
    private String startTime;
    private String endTime;
    private List<TableData> list;
}

然后是关联(table)数据

java 复制代码
import lombok.Data;

@Data
public class TableData {
    private String date;
    private String desc;
    private String name;
    private String fee;
}

然后我们处理我们处理逻辑的代码

java 复制代码
import com.alibaba.fastjson.JSONObject;
import com.example.web.pojo.TableData;
import com.example.web.pojo.PDFData;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider;
import com.itextpdf.kernel.colors.DeviceRgb;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.layout.property.TextAlignment;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

@Component
public class FreemarkerExecution {
	
	// 准备下字体文件
    private static String FONT = "./src/main/resources/templates/AlibabaPuHuiTi-3-65-Medium.ttf";
    // 后续转pdf时的配置
    private static ConverterProperties converterProperties = new ConverterProperties();
    private static String base64LogoData = null;
    {
        FontProvider dfp = new DefaultFontProvider();
        //添加字体库
        dfp.addFont(FONT);
        //设置解析属性
        converterProperties.setFontProvider(dfp);
        converterProperties.setCharset("utf-8");
        try {
        	// 有一个logo处理,因为一般服务器渲染的话一般建议将部分图片处理成base64然后放进来,或者大家看看其他的方式
            base64LogoData = imgToBase64(new FileInputStream("./src/main/resources/templates/logo.png"));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    @Resource(name = "cfg")
    private Configuration configuration;

	// 普通处理逻辑
    public void converterHTML() {
    	// 这个会将处理后的html语法输出至 命令行窗口,可以手动创建一个html文件,然后把结果复制进去直接打开查看
        try (Writer out = new OutputStreamWriter(System.out)) {
        	// 获取数据
            final PDFData pdfData = getData();
            Template temp = configuration.getTemplate("pdf.ftl");
            // 直接写出文件
            temp.process(pdfData, out);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {

        }
    }

    private void writeToPDF(Template template, Map<String, Object> dataModel) {
        try {
            final File file = new File("D:/pdf/test.html");
            template.process(dataModel, new OutputStreamWriter(new FileOutputStream(file)));
            final File pdfFile = new File("D:/pdf/test.pdf");
            HtmlConverter.convertToPdf(file, pdfFile, converterProperties);
            PdfReader reader = new PdfReader(new File("D:/pdf/test.pdf"));
            PdfWriter writer = new PdfWriter(new FileOutputStream("D:/pdf/test_1.pdf"));
            PdfDocument pdfDocument = new PdfDocument(reader, writer);
            // 页大小
            final PageSize pageSize = pdfDocument.getDefaultPageSize();
            // 页数
            final int numberOfPages = pdfDocument.getNumberOfPages();
            for (int i = 1; i <= numberOfPages; i++) {
                PdfPage page = pdfDocument.getPage(i);
                final PdfDocument pdfDoc = page.getDocument();
                final Document document = new Document(pdfDoc);
                final Paragraph paragraph = new Paragraph("Page" + i)
                        .setFont(PdfFontFactory.createFont(FONT))
                        .setFontColor(new DeviceRgb(0, 0, 0))
                        .setFixedPosition(i, 0, 10, pageSize.getWidth())
                        .setFontSize(10)
                        .setTextAlignment(TextAlignment.CENTER);
                document.add(paragraph);
            }
            pdfDocument.close();
            reader.close();
            writer.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private PDFData getData() throws FileNotFoundException {
        PDFData data = new PDFData();
        data.setName("重生之我是蔡徐坤");
        data.setAddress("蔡徐坤蔡徐坤喜欢唱跳rap篮球");
        data.setStartTime("01-Feb-22");
        data.setEndTime("28-Feb-22");
        data.setLogo("data:image/png;base64," + base64LogoData);
        final LocalDateTime nowTime = LocalDateTime.now();
        final List<TableData> arr = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            final TableData tableData = new TableData();
            tableData .setDesc(i % 2 == 0 ? "交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大" : "交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大交会毫不我交会毫不我哒哒哒哒哒哒多多多多多多多交会毫不我电话电话大");
            tableData .setName("蔡徐坤" + i);
            tableData .setFee(1000 + i + "");
            tableData .setDate(nowTime.plusDays(-i).format(DateTimeFormatter.ofPattern("YYYY/MM/dd")));
            arr.add(tableData);
        }
        data.setList(arr);
        return data;
    }

    private String imgToBase64(InputStream inputStream) {
        byte[] data = null;
        try {
            data = new byte[inputStream.available()];
            inputStream.read(data);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.getEncoder().encodeToString(data);
    }
}

这里会又有一个问题出现,就是我们一般处理PDF的时候,数据不可能一次性处理到内存中,因为我们服务器内存等问题,假如我们有100w数据,肯定不能一次性查出来,由此我们就需要批量处理,这里我们可以将模板拆分开,重复的数据放一个模板文件,然后后续进行模板的组装。

首先,我将一个ftl模板文件拆成了三个 👉 header.ftl,content.ftl,footer.ftl,然后再由一个主的核心ftl模板来组装这几个模板。

思路:准备上述模板文件,然后渲染模板后继续解析成ftl模板文件,然后读取选然后的ftl模板文件,然后转成html,最后通过html文件处理成pdf文件。

这里content.ftl是批量的数据,因为不能一次读取大量数据,所以这里content.ftl要单独处理一下。

Main.ftl
html 复制代码
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<style>
    .logo {
        width: 320px;
        height: 80px;
    }

    .user-info {
        padding: 10px 10px;
        background: RGB(221, 235, 247);
    }

    .label {
        width: 150px;
        padding: 0 20px 0 0;
        text-align: left;
    }

    .time {
        width: 100px;
        margin-right: 10px;
        text-align: center;
    }

    .label, .time {
        display: inline-block;
    }

    .range-time {
        margin: 20px 0;
    }

    .table-data {
        width: 100%;
    }

    table {
        border-collapse: collapse;
    }

    .header > th {
        text-align: left;
        height: 50px;
    }

    .divider-line {
        margin: 20px 0;
        height: 3px;
        background: #000;
    }

    .desc {
        padding: 15px 10px;
    }

    .date {
        width: 100px;
    }

    .name {
        width: 50px;
    }

    .fee{
        width: 140px;
    }

    .fee, .name , .date {
        padding: 0 10px;
    }

    .download-date {
        text-align: right;
    }

    .bottom-footer-tip {
        width: 100%;
        margin-top: 300px;
        font-size: 12px;
        transform: scale(.9);
    }
</style>
<body>


${headerPath}
<table class="table-data">
    <tr class="header">
        <th>Date</th>
        <th>Desc</th>
        <th>Name</th>
        <th>Fee</th>
    </tr>
    <tr class="divider-line">
        <th></th>
        <th></th>
        <th></th>
        <th></th>
    </tr>
    <#list contentPathList as content>
        ${content}
    </#list>
    <tr class="divider-line">
        <th></th>
        <th></th>
        <th></th>
        <th></th>
    </tr>
</table>
${footerPath}

<#--<#include "header.ftl">-->
<#--<#include "content.ftl">-->
<#--<#include "footer.ftl">-->

</body>
</html>
header.ftl
html 复制代码
<img src="${logo}" class="logo">
<div class="download-date">Download on 2022/2/2</div>
<div class="user-info">
    <div>
        <div class="label">User:</div>
        <span>${name}</span>
    </div>
    <div>
        <div class="label">Address:</div>
        <span>${address}</span>
    </div>
</div>

<div class="range-time">
    <span class="time">${startTime}</span>
    <span>to</span>
    <span class="time">${endTime}</span>
</div>
content.ftl
html 复制代码
    <#list list as item>
        <tr>
            <td class="date">${item.date}</td>
            <td class="desc">
                <div>${item.desc}</div>
            </td>
            <td class="fee">${item.fee}</td>
            <td class="name">${item.name}</td>
        </tr>
    </#list>
footer.ftl
html 复制代码
    <#list list as item>
        <tr>
            <td class="date">${item.date}</td>
            <td class="desc">
                <div>${item.desc}</div>
            </td>
            <td class="fee">${item.fee}</td>
            <td class="name">${item.name}</td>
        </tr>
    </#list>
核心处理逻辑
java 复制代码
    void allTemplatesWriteToPDF() {
        // 所有子模板
        final List<String> ftlNameList = new ArrayList<>();
        try {
        	// 读取对应的模板文件
            final Template template = getTemplate("content.ftl");
            final Template headerTemplate = getTemplate("header.ftl");
            final Template footerTemplate = getTemplate("footer.ftl");
            final PDFData data = getData();
            // 先处理头部和尾部
            headerTemplate.process(data, new FileWriter("D:/pdf/content/header.ftl"));
            footerTemplate.process(data, new FileWriter("D:/pdf/content/footer.ftl"));
            // mock 模拟数据库查询10次
            for (int i = 0; i < 10; i++) {
                // 组装10条数据,算上10次循环一共100条数据
                final PDFData pdfData = getData();
                String fileName = "D:/pdf/content/content" + i + ".ftl";
                final FileWriter writer = new FileWriter(fileName);
                // 存储最后框架模板的数据,这里是存储了freemarker include 语法连接所有的需要组装的数据模板名称
                ftlNameList.add("<#include \"" + fileName.substring(fileName.lastIndexOf("/") + 1, fileName.lastIndexOf(".")) + ".ftl \"/>");
                // 生成对应的模板文档
                template.process(pdfData, writer);
                writer.flush();
                writer.close();
            }
            // 获取所有子模板
            final Configuration cdf = new Configuration(Configuration.VERSION_2_3_22);
            cdf.setDirectoryForTemplateLoading(new File("D:/pdf/content"));
            cdf.setDefaultEncoding("UTF-8");
            cdf.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
            final Template mainTemplate = getTemplate("main.ftl");
            final HashMap<Object, Object> map = new HashMap<>();
            map.put("headerPath", "<#include \"" + "header.ftl\"/>");
            map.put("contentPathList", ftlNameList);
            map.put("footerPath", "<#include \"" + "footer.ftl\"/>");
            mainTemplate.process(map, new FileWriter("D:/pdf/content/main.ftl"));
            final Template cdfTemplate = cdf.getTemplate("main.ftl");
            cdfTemplate.process(null, new FileWriter("D:/pdf/content/main.html"));
            final File pdfFile = new File("D:/pdf/test_new.pdf");
            HtmlConverter.convertToPdf(new File("D:/pdf/content/main.html"), pdfFile, converterProperties);
            PdfReader reader = new PdfReader(pdfFile);
            PdfWriter writer = new PdfWriter(new FileOutputStream("D:/pdf/test_new_1.pdf"));
            PdfDocument pdfDocument = new PdfDocument(reader, writer);
            // 页大小
            final PageSize pageSize = pdfDocument.getDefaultPageSize();
            // 页数
            final int numberOfPages = pdfDocument.getNumberOfPages();
            // 这里是处理页脚数据
            for (int i = 1; i <= numberOfPages; i++) {
                PdfPage page = pdfDocument.getPage(i);
                final PdfDocument pdfDoc = page.getDocument();
                final Document document = new Document(pdfDoc);
                final Paragraph paragraph = new Paragraph("Page" + i)
                        .setFont(PdfFontFactory.createFont(FONT))
                        .setFontColor(new DeviceRgb(0, 0, 0))
                        .setFixedPosition(i, 0, 10, pageSize.getWidth())
                        .setFontSize(10)
                        .setTextAlignment(TextAlignment.CENTER);
                document.add(paragraph);
            }
            pdfDocument.close();
            reader.close();
            writer.close();
        } catch (IOException | TemplateException e) {
            e.printStackTrace();
        }
    }

    private Template getTemplate(String name) throws IOException {
        return configuration.getTemplate(name);
    }

OK,大概就这样,剩下的大家自己去玩吧, 解散!!!

相关推荐
CoderYanger23 分钟前
C.滑动窗口-求子数组个数-越长越合法——2799. 统计完全子数组的数目
java·c语言·开发语言·数据结构·算法·leetcode·职场和发展
C++业余爱好者27 分钟前
Java 提供了8种基本数据类型及封装类型介绍
java·开发语言·python
想用offer打牌29 分钟前
RocketMQ如何防止消息丢失?
java·后端·架构·开源·rocketmq
皮卡龙31 分钟前
Java常用的JSON
java·开发语言·spring boot·json
PineappleCoder1 小时前
还在重复下载资源?HTTP 缓存让二次访问 “零请求”,用户体验翻倍
前端·性能优化
拉不动的猪1 小时前
webpack编译中为什么不建议load替换ast中节点删除consolg.log
前端·javascript·webpack
李姆斯1 小时前
Agent时代下,ToB前端的UI和交互会往哪走?
前端·agent·交互设计
利刃大大1 小时前
【JavaSE】十三、枚举类Enum && Lambda表达式 && 列表排序常见写法
java·开发语言·枚举·lambda·排序
float_六七1 小时前
Java反射:万能遥控器拆解编程
java·开发语言
han_hanker2 小时前
java 异常类——详解
java·开发语言