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,大概就这样,剩下的大家自己去玩吧, 解散!!!

相关推荐
浅念同学3 分钟前
算法-常见数据结构设计
java·数据结构·算法
java小郭2 小时前
html的浮动作用详解
前端·html
水星记_2 小时前
echarts-wordcloud:打造个性化词云库
前端·vue
杰哥在此2 小时前
Java面试题:讨论持续集成/持续部署的重要性,并描述如何在项目中实施CI/CD流程
java·开发语言·python·面试·编程
强迫老板HelloWord3 小时前
前端JS特效第22波:jQuery滑动手风琴内容切换特效
前端·javascript·jquery
咖啡煮码3 小时前
深入剖析Tomcat(十五、十六) 关闭钩子,保证Tomcat的正常关闭
java·tomcat
C.C3 小时前
java IO流(1)
java·开发语言
续亮~4 小时前
9、程序化创意
前端·javascript·人工智能
黑头!4 小时前
Tomcat注册为服务之后 运行时提示JVM异常
java·jvm·tomcat
RainbowFish4 小时前
「Vue学习之路」—— vue的常用指令
前端·vue.js