最近公司有个需求,就是想根据一个模板生成一个pdf文档,当即我就想到了freemarker
这个远古老东西,毕竟freemarker
在模板渲染方面还是非常有优势的。
准备依赖:
<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
的配置
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框架类似于beetl
、thymeleaf
、jsp
、Velocity
等模板引擎
JSP就不用说了吧,基本上开发Java的基本上都会了解开发过
<!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实体数据
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)数据
import lombok.Data;
@Data
public class TableData {
private String date;
private String desc;
private String name;
private String fee;
}
然后我们处理我们处理逻辑的代码
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
<!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
<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
<#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
<#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>
核心处理逻辑
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,大概就这样,剩下的大家自己去玩吧, 解散!!!