业务需求

项目需要用到导出pdf格式的列表,正常情况下是导出excel表格的,但是产品要求要导出pdf,组长叫我尝试一下itextpdf和thymeleaf。
捣鼓了几天研究出来了,但是还没有具体运用上线。

教程

最终效果如下图
效果图

准备

POM依赖

先导入相关的pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- itext7html转pdf  -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>3.0.2</version>
</dependency>

<!-- 中文字体支持 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>7.1.13</version>
</dependency>

<!--freemarker模板-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>

<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>

Handler

这里主要是加一些适配器,比如页眉、页码、水印等

页眉相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HeaderH implements IEventHandler {
String header;
public HeaderH(String header) {
this.header = header;
}
@Override
public void handleEvent(Event event) {
PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
PdfDocument pdf = docEvent.getDocument();
PdfPage page = docEvent.getPage();
if (pdf.getPageNumber(page) == 1) return;
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(
page.getLastContentStream(), page.getResources(), pdf);
Canvas canvas = new Canvas(pdfCanvas, pdf, pageSize);
canvas.showTextAligned(header,
pageSize.getWidth() / 2,
pageSize.getTop() - 30, TextAlignment.CENTER);
}
}

页码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

public class PageXofY implements IEventHandler {
protected PdfFormXObject placeholder;
protected int totalPageCount;
protected float side = 20;
protected float x = 750;
protected float y = 25;
protected float space = 4.5f;
protected float descent = 3;

public PageXofY(PdfDocument pdf, int totalPageCount) {
placeholder =
new PdfFormXObject(new Rectangle(0, 0, side, side));
this.totalPageCount = totalPageCount;
}

@Override
public void handleEvent(Event event) {
PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
PdfDocument pdf = docEvent.getDocument();
PdfPage page = docEvent.getPage();
int pageNumber = pdf.getPageNumber(page);
Rectangle pageSize = page.getPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(
page.getLastContentStream(), page.getResources(), pdf);
Canvas canvas = new Canvas(pdfCanvas, pdf, pageSize);
Paragraph p = new Paragraph()
.add("Page")
.add(String.valueOf(StrPool.C_SPACE))
.add(String.valueOf(pageNumber))
.add(String.valueOf(StrPool.C_SPACE))
.add("of")
.add(String.valueOf(StrPool.C_SPACE))
.add(String.valueOf(totalPageCount));
canvas.showTextAligned(p, x, y, TextAlignment.RIGHT);
pdfCanvas.addXObject(placeholder, x + space, y - descent);
pdfCanvas.release();
}
}

水印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

/**
* 水印
*/
public class WaterMarkEventHandler implements IEventHandler
{

/**
* 水印内容
*/
private String waterMarkContent;

/**
* 一页中有几列水印
*/
private int waterMarkX;

/**
* 一页中每列有多少水印
*/
private int waterMarkY;

public WaterMarkEventHandler(String waterMarkContent)
{
this(waterMarkContent, 5, 5);
}

public WaterMarkEventHandler(String waterMarkContent, int waterMarkX, int waterMarkY)
{
this.waterMarkContent = waterMarkContent;
this.waterMarkX = waterMarkX;
this.waterMarkY = waterMarkY;
}

@Override
public void handleEvent(Event event)
{

PdfDocumentEvent documentEvent = (PdfDocumentEvent) event;
PdfDocument document = documentEvent.getDocument();
PdfPage page = documentEvent.getPage();
Rectangle pageSize = page.getPageSize();

PdfFont pdfFont = null;
try
{
pdfFont = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H", false);
}
catch (IOException e)
{
e.printStackTrace();
}

PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), document);

Paragraph waterMark = new Paragraph(waterMarkContent).setOpacity(0.5f);
Canvas canvas = new Canvas(pdfCanvas, pageSize)
.setFontColor(WebColors.getRGBColor("lightgray"))
.setFontSize(16)
.setFont(pdfFont);

for (int i = 0; i < waterMarkX; i++)
{
for (int j = 0; j < waterMarkY; j++)
{
canvas.showTextAligned(waterMark, (150 + i * 300), (160 + j * 150), document.getNumberOfPages(), TextAlignment.CENTER, VerticalAlignment.BOTTOM, 120);
}
}
canvas.close();
}
}

Utils

FreeMarkerUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class FreeMarkerUtils
{

private static Template getTemplate(String template_path, String templateFileName)
{
Configuration configuration = new Configuration();
Template template = null;
try {
configuration.setDirectoryForTemplateLoading(new File(template_path));
configuration.setObjectWrapper(new DefaultObjectWrapper());
configuration.setDefaultEncoding("UTF-8"); //设置编码格式
//模板文件
template = configuration.getTemplate(templateFileName + ".ftl");
} catch (IOException e) {
e.printStackTrace();
}
return template;
}

public static void genteratorFile(Map<String, Object> input, String template_path, String templateFileName, String savePath, String fileName)
{
Template template = getTemplate(template_path, templateFileName);
File filePath = new File(savePath);
if (!filePath.exists()) {
filePath.mkdirs();
}
String filename = savePath + "\\" + fileName;
File file = new File(filename);
if (!file.exists()) {
file.delete();
}
Writer writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(filename), "UTF-8");
template.process(input, writer);
} catch (Exception e) {
e.printStackTrace();
}
}

public static String genterator(Map<String, Object> variables, String template_path, String templateFileName) throws Exception
{
Template template = getTemplate(template_path, templateFileName);
StringWriter stringWriter = new StringWriter();
BufferedWriter writer = new BufferedWriter(stringWriter);
template.setEncoding("UTF-8");
template.process(variables, writer);
String htmlStr = stringWriter.toString();
writer.flush();
writer.close();
return htmlStr;
}
}

HtmlToPdfUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
public class HtmlToPdfUtils {

/**
* html转pdf
*
* @param inputStream 输入流
* @param waterMark 水印
* @param fontPath 字体路径,ttc后缀的字体需要添加<b>,0<b/>
* @param outputStream 输出流
* @date : 2022/11/15 14:07
*/
public static void convertToPdf(InputStream inputStream, String waterMark, String fontPath, OutputStream outputStream) throws IOException {
// 有两个pdfWriter和pdfDocument是因为为了解决这个页码的问题,
// 原先页码是没办法获取到总的页码数量,经过一顿骚操作改造可以了,但是不知道会不会有隐藏风险
PdfWriter pdfWriter = new PdfWriter(outputStream);
PdfWriter pdfWriter1 = new PdfWriter(outputStream);
PdfDocument pdfDocument = new PdfDocument(pdfWriter);
PdfDocument pdfDocument1 = new PdfDocument(pdfWriter1);

// 设置为A4大小并设置横向
// PageSize pageSize = PageSize.A4.rotate();
pdfDocument.setDefaultPageSize(PageSize.A4.rotate());
pdfDocument1.setDefaultPageSize(PageSize.A4.rotate());
// 如果是纵向就用下面这个
// pdfDocument.setDefaultPageSize(PageSize.A4);
// pdfDocument1.setDefaultPageSize(PageSize.A4);

// 添加水印
pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE, new WaterMarkEventHandler(waterMark));


// 添加中文字体支持
ConverterProperties properties = new ConverterProperties();
FontProvider fontProvider = new FontProvider();

// 添加自定义字体,
if (!StringUtils.isEmpty(fontPath)) {
// 下面是使用的鸿蒙字体
String HARMONYOS_SANS_SC_REGULAR = "D:\\lot\\code\\study\\springboot-restful-starter\\src\\main\\resources\\fonts\\HARMONYOS_SANS_SC_REGULAR.TTF";
String HARMONYOS_SANS_SC_BOLD = "D:\\lot\\code\\study\\springboot-restful-starter\\src\\main\\resources\\fonts\\HARMONYOS_SANS_SC_BOLD.TTF";
String HARMONYOS_SANS_SC_BLACK = "D:\\lot\\code\\study\\springboot-restful-starter\\src\\main\\resources\\fonts\\HARMONYOS_SANS_SC_BLACK.TTF";
PdfFont ttfFontOne = PdfFontFactory.createFont(HARMONYOS_SANS_SC_REGULAR, PdfEncodings.IDENTITY_H, false);
PdfFont ttfFontTwo = PdfFontFactory.createFont(HARMONYOS_SANS_SC_BOLD, PdfEncodings.IDENTITY_H, false);
PdfFont ttfFontThird = PdfFontFactory.createFont(HARMONYOS_SANS_SC_BLACK, PdfEncodings.IDENTITY_H, false);


fontProvider.addFont(ttfFontOne.getFontProgram());
fontProvider.addFont(ttfFontTwo.getFontProgram());
fontProvider.addFont(ttfFontThird.getFontProgram());
properties.setFontProvider(fontProvider);
properties.setCharset("utf-8");

}


properties.setFontProvider(fontProvider);
// 读取Html文件流,查找出当中的&nbsp;或出现类似的符号空格字符
Map<String, Object> map = readInputStrem(inputStream);
// 需要先生成document,而不是直接生成pdf文件(因直接生成pdf文件会关闭流)
Document document = HtmlConverter.convertToDocument((String) map.get("html"), pdfDocument1, properties);

document.flush();
PageXofY event = new PageXofY(pdfDocument, document.getPdfDocument().getNumberOfPages());

pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE, event);


inputStream = (InputStream) map.get("inputStream");


try {
if (inputStream != null) {
// 生成pdf文档
HtmlConverter.convertToPdf(inputStream, pdfDocument, properties);
return;
} else {
log.error("转换失败!");
}

} catch (Exception e) {
e.printStackTrace();
} finally {

try {
pdfWriter.close();
pdfDocument.close();
} catch (IOException e) {
throw new RuntimeException(e);
}


}
}

/**
* 读取HTML 流文件,并查询当中的&nbsp;或类似符号直接替换为空格
*
* @param inputStream
* @return
*/
private static Map<String, Object> readInputStrem(InputStream inputStream) {
HashMap<String, Object> returnMap = new HashMap<>(2);

// 定义一些特殊字符的正则表达式 如:
String regEx_special = "\\&[a-zA-Z]{1,10};";
try {
//<1>创建字节数组输出流,用来输出读取到的内容
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//<2>创建缓存大小
byte[] buffer = new byte[1024]; // 1KB
//每次读取到内容的长度
int len = -1;
//<3>开始读取输入流中的内容
while ((len = inputStream.read(buffer)) != -1) { //当等于-1说明没有数据可以读取了
baos.write(buffer, 0, len); //把读取到的内容写到输出流中
}
//<4> 把字节数组转换为字符串
String content = baos.toString();
//<5>关闭输入流和输出流
// inputStream.close();
baos.close();
// log.info("读取的内容:{}", content);
// 判断HTML内容是否具有HTML的特殊字符标记
Pattern compile = Pattern.compile(regEx_special, Pattern.CASE_INSENSITIVE);
Matcher matcher = compile.matcher(content);
String replaceAll = matcher.replaceAll("");

// log.info("替换后的内容:{}", replaceAll);
// 将字符串转化为输入流返回
InputStream stringStream = getStringStream(replaceAll);
returnMap.put("inputStream", stringStream);
returnMap.put("html", replaceAll);
//<6>返回结果
return returnMap;
} catch (Exception e) {
e.printStackTrace();
log.error("错误信息:{}", e.getMessage());
return null;
}
}

/**
* 将一个字符串转化为输入流
*
* @param sInputString 字符串
* @return
*/
public static InputStream getStringStream(String sInputString) {
if (sInputString != null && !sInputString.trim().equals("")) {
try {
ByteArrayInputStream tInputStringStream = new ByteArrayInputStream(sInputString.getBytes());
return tInputStringStream;
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}

}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@Slf4j
public class TestHtmlToPdf {
// 模板路径
public final static String TEMP = "D:\\lot\\code\\study\\springboot-restful-starter\\src\\main\\resources\\templates\\";

/**
* 模板所需的数据
*
* @return 数据
*/
public static Map<String, Object> getContent() {
// 从数据库中获取数据, 出于演示目的, 这里数据不从数据库获取, 而是直接写死
Map input = new HashMap();
input.put("title", "测试title");
input.put("logo", "");
input.put("accountId", "MC" + UUID.randomUUID(false));
input.put("accountName", "MC" + UUID.randomUUID(false));
input.put("dateOfIssue", DateUtil.now());
List<Map<String, String>> tableData = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Map<String, String> rowData = new HashMap<>();
rowData.put("tableData1", "这是第" + (i + 1) + "行的第一个表格数据行的第一个表格数据行的第一个表格数据行的第一个表格数据行的第一个表格数据行的第一个表格数据");
rowData.put("tableData2", "这是第" + (i + 1) + "行的第二个表格数据行的第一个表格数据行的第一个表格数据行的第一个表格数据行的第一个表格数据行的第一个表格数据行的第一个表格数据");
tableData.add(rowData);
}
input.put("tableData", tableData);


input.put("accountId", UUID.randomUUID());
input.put("DateOfIssue", DateUtil.today());

// 准备美国人的姓和名的数组
String[] firstNames = {"James", "John", "Robert", "Michael", "William"};
String[] lastNames = {"Smith", "Johnson", "Williams", "Jones", "Brown"};

// 使用RandomUtil随机选择姓和名
String randomFirstName = RandomUtil.randomEle(firstNames);
String randomLastName = RandomUtil.randomEle(lastNames);

// 组合姓名
String fullName = randomFirstName + " " + randomLastName;
input.put("accountName", fullName);

List<Map<String, Object>> rows = new ArrayList<>();
double y = 0;
for (int i = 0; i < 500; i++) {
Random r = new Random();
DecimalFormat df = new DecimalFormat("#.##");
String[] currencyArr = {"USD", "CNY", "EURO"};
String[] platformArr = {"Shopify", "WooCommerce", "Magento"};
String[] shipArr = {"AFL SHIPPING", "BFL SHIPPING", "CFL SHIPPING"};
String[] typeArr = {"Deposit", "Withdraw", "Refund"};
int minAmount = 20;
int maxAmount = 1000;

String date = "2024-04-" + r.nextInt(30);
String demo1 = "DemoDemoDemoDemoDemoDemoDemo" + RandomUtil.randomNumbers(10);
String demo2 = "Demo" + RandomUtil.randomNumbers(Integer.valueOf(RandomUtil.randomNumbers(1)));
String accNumber = String.valueOf(r.nextLong()).substring(1, 14);
String currency = currencyArr[r.nextInt(currencyArr.length)];
String platform = platformArr[r.nextInt(platformArr.length)];
String name = "Ma Wang" + RandomUtil.randomNumbers(10);
String ship = shipArr[r.nextInt(shipArr.length)];
String quantity = String.valueOf(r.nextInt(30));
String type = typeArr[r.nextInt(typeArr.length)];
String amount = df.format(minAmount + (maxAmount - minAmount) * r.nextDouble());


Map<String, Object> row = new HashMap<>();
row.put("y", y);
row.put("part1", date);
row.put("part2", demo1);
row.put("part3", demo2);
row.put("part4", accNumber);
row.put("part5", currency);
row.put("part6", platform);
row.put("part7", name);
row.put("part8", ship);
row.put("part10", currency);
row.put("part11", quantity);
row.put("part12", currency);
row.put("part13", amount);
row.put("part9", type);
rows.add(row);
y += 15;

}
input.put("rows", rows);


return input;
}


public static void main(String[] args) throws IOException {
extracted("thread-12");
}

private static void extracted(String filename) throws IOException {
long startTime = System.currentTimeMillis();
String temp = "test";
// 指定模板渲染值并生成html文件至指定位置
FreeMarkerUtils.genteratorFile(getContent(),
TEMP,
temp,
TEMP,
temp + ".html");

// 需转换的html文件名称
String htmlFile = temp + ".html";
// 转换好pdf存储名称
String pdfFile = filename + ".pdf";
// 自定义水印
String waterMarkText = "JYX";
// 读取需转换的html文件
// 读取需转换的html文件
InputStream inputStream = new FileInputStream(TEMP + htmlFile);
// 写出pdf存储位置
OutputStream outputStream = new FileOutputStream(TEMP + pdfFile);
String FONT_TTF_PATH = "D:\\lot\\code\\springboot-restful-starter\\src\\main\\resources\\fonts\\HARMONYOS_SANS_SC_REGULAR.TTF";

// 开始转换html生成pdf文档
HtmlToPdfUtils.convertToPdf(inputStream, waterMarkText, FONT_TTF_PATH, outputStream);
log.info("转换结束,耗时:{}ms", System.currentTimeMillis() - startTime);
}
}

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<!DOCTYPE html>
<html>
<head>
<style>
.a4-wrapper {
width: 297mm;
height: 210mm;
padding: 20mm;
box-sizing: border-box;
background: white;
}
body {
margin: 20px;
font-family: Arial, sans-serif;
}

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

th {
border-bottom: 1px solid #d3d3d3;
padding: 10px;
text-align: left;
margin-bottom: 20px;
}

td {
border: none;
padding: 10px;
text-align: left;
}

.logo {
display: block;
margin-left: auto;
margin-right: auto;
width: 50%;
}

.title {
text-align: center;
margin-bottom: 20px;
}

.account-info {
margin-left: 10%;
}

.date {
margin-right: 10%;
text-align: right;
}

.page {
page-break-after: always;
}
</style>
</head>
<body>
<div class="header">
<div class="account-info">
<img src="${logo!"#"}" class="logo" alt="Logo"/>
<h1 class="title">${title!"Title"}</h1>
<p>Account ID: ${accountId!"#"}</p>
<p>Account Name: ${accountName!"#"}</p>
</div>
<div class="date">
<p>Date of issue: ${dateOfIssue!"#"}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>固定表头1</th>
<th>固定表头2</th>
</tr>
</thead>
<tbody>
<#list tableData as row>
<tr>
<td style="word-break: break-all; max-width: 100px;">${row.tableData1}</td>
<td style="word-break: break-all; max-width: 100px;">${row.tableData2}</td>
</tr>
</#list>
</tbody>
</table>
</body>
</html>