参考文献

调研历程

  • 首先查阅资料,了解Java中现有PDF的框架
    • iText
      • iText 是一个功能强大的 PDF 处理库,可以用于创建、修改和提取 PDF 文档的内容,支持文本、图像、表格等元素的处理.它提供了丰富的 API,可以满足各种 PDF 处理需求.
    • Apache PDF Box
      • Apache PDF Box 是 Apache 软件基金会的一个开源 Java 库,用于操作 PDF 文档.它支持创建、修改和提取 PDF 文档的内容,以及数字签名和加密等功能.
    • JFreeReport
      • JFreeReport 是一个用于生成报表的 Java 库,它支持创建复杂的、高度定制的报表,包括图表、表格、文本等元素,并且可以将报表输出为 PDF 格式.
    • PJX
      • PJX 是一个用于创建 PDF 文档的 Java 库,它提供了简单易用的 API,可以用来生成包含文本、图像、表格等内容的 PDF 文件.
    • FOP
      • FOP(Formatting Objects Processor)是 Apache XML 图形化项目的一部分,用于将 XML 数据转换为 PDF、PostScript 等格式的输出.它支持 XSL-FO(XSL Formatting Objects)标准,可以通过 XML 样式表定义 PDF 输出的样式和布局.
    • gnujpdf
      • gnujpdf 是一个用于创建 PDF 文档的 Java 库,它提供了简单的 API,可以用来生成包含文本、图像等内容的 PDF 文件.
    • Connla
      • Connla 是一个用于创建 PDF 报告的 Java 库,它提供了丰富的 API 和模板,可以用来生成高度定制的 PDF 报告. Connla 支持文本、图表、表格等元素的添加,并且可以通过模板定义报告的样式和布局.

iText License协议说明

  • iText 0.x-2.x/iTextSharp 3.x-4.x
    • 更新时间是2000-2009
    • 使用的是MPL和LGPL双许可协议
    • 最近的更新是2009年,版本号是iText 2.1.7/iTextSharp 4.1.6.0
    • 此时引入包的GAV版本如下:
1
2
3
4
5
<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
  • iText 5.x和iTextSharp 5.x
    • 更新时间是2009-2016, 公司化运作,并标准化和提高性能
    • 开始使用AGPLv3协议
      • 只有个人用途和开源的项目才能使用itext这个库,否则是需要收费的
    • iTextSharp被设计成iText库的.NET版本,并且与iText版本号同步,iText 5.0.0和iTextSharp5.0.0同时发布
    • 新功能不在这里面增加,但是官方会修复重要的bug
    • 此时引入包的GAV版本如下:
1
2
3
4
5
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
  • iText 7.x
    • 更新时间是2016到现在
    • AGPLv3协议
    • 完全重写,重点关注可扩展性和模块化
    • 不适用iTextSharp这个名称,都统称为iText,有Java和.Net版本
    • JDK 1.7+
    • 此时引入包的GAV版本如下:
1
2
3
4
5
6
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.2.2</version>
<type>pom</type>
</dependency>

著作权归@pdai所有 原文链接:https://pdai.tech/md/spring/springboot/springboot-x-file-pdf-itext.html

使用Flying SaucerHTML转换为PDF

Flying Saucer使用说明

New releases of Flying Saucer are distributed through Maven. The available artifacts are:

  • org.xhtmlrenderer:flying-saucer-core - Core library and Java2D rendering
  • org.xhtmlrenderer:flying-saucer-pdf - PDF output using OpenPDF (ex. iText 2.x)
  • org.xhtmlrenderer:flying-saucer-pdf-itext5 - PDF output using iText 5.x (iText 5 is EOL)
  • org.xhtmlrenderer:flying-saucer-pdf-openpdf - not supported anymore (replaced by flying-saucer-pdf)
  • org.xhtmlrenderer:flying-saucer-swt - SWT output
  • org.xhtmlrenderer:flying-saucer-log4j - Logging plugin for log4j

Flying Saucer from version 9.5.0, requires Java 11 or later. Flying Saucer from version 9.6.0, requires Java 17 or later.

使用示例

  • 介于iText5.x之后采用AGPLv3协议,在选型的时候为了避免风险,使用基于OpenPDF来操作PDF

依赖

  • 项目使用Java 11根据Flying Saucer使用说明,需要版本需要 >=9.5.0 9.60<
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-core</artifactId>
<version>9.5.0</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.5.0</version>
</dependency>

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void simpleUse(String filePath, String outFileName) throws IOException, DocumentException {
// 输出文件
File output = new File(outFileName);
// 输入文件
final Path path = Paths.get(filePath);
final String content = Files.readString(path);
// 渲染器
ITextRenderer iTextRenderer = new ITextRenderer();
// 将输入文件内容设置到渲染器中
iTextRenderer.setDocumentFromString(content);
// 设置布局
iTextRenderer.layout();
OutputStream os = new FileOutputStream(output);
// 创建PDF
iTextRenderer.createPDF(os);
os.close();
}

输出带有中文内容的PDF

  • 输出带有中文内容的PDF,需要在将输入内容添加到渲染器(ITextRenderer)中之前添加支持中文的字体文件到渲染器中,同时需要再HTML中设置样式

    1
    font-family: SimSun;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void simpleUse(String filePath, String outFileName) throws IOException, DocumentException {
// 输出文件
File output = new File(outFileName);
// 输入文件
final Path path = Paths.get(filePath);
final String content = Files.readString(path);
// 渲染器
ITextRenderer iTextRenderer = new ITextRenderer();
iTextRenderer.getFontResolver().addFont("/Users/holelin/Downloads/simsun.ttc", "Identity-H",false);

// 将输入文件内容设置到渲染器中
iTextRenderer.setDocumentFromString(content);
// 设置布局
iTextRenderer.layout();
OutputStream os = new FileOutputStream(output);
// 创建PDF
iTextRenderer.createPDF(os);
os.close();
}
  • 参数 "Identity-H"false 分别表示字体子集的标识和是否嵌入字体的设置
    • "Identity-H"
      • "Identity-H" 表示字体子集的标识。在 PDF 中,字体可以嵌入文档中,也可以以子集的形式嵌入。子集嵌入意味着仅将文档中实际使用到的字符添加到 PDF 文件中,而不是整个字体文件。而 "Identity-H" 表示对字体进行子集化处理,并使用 Identity-H 编码来进行字形映射。Identity-H 编码是一种直接将 Unicode 编码映射到字形的方式,通常用于支持非西方语言(如中文、日文等)。
    • false
      • false 表示不嵌入字体。当设置为 false 时,即表示不将字体文件嵌入到生成的 PDF 文件中,而是在 PDF 中引用外部字体文件。这通常用于减小 PDF 文件的大小,特别是当使用的字体文件较大时,可以选择不嵌入字体以降低文件大小。

使用阿里普惠体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void htmlToPdf(String xhtml, String outFileName) throws IOException, DocumentException {
File output = new File(outFileName);
ITextRenderer iTextRenderer = new ITextRenderer();
boolean embedded = false;
final String encoding = "Identity-H";
iTextRenderer.getFontResolver().addFont("/fonts/fonts/AlibabaPuHuiTi-3-35-Thin.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-45-Light.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-55-Regular.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-55-RegularL3.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-65-Medium.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-75-SemiBold.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-85-Bold.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-95-ExtraBold.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-105-Heavy.ttf", encoding, embedded);
iTextRenderer.getFontResolver().addFont("/fonts/AlibabaPuHuiTi-3-115-Black.ttf", encoding, embedded);
iTextRenderer.setDocumentFromString(xhtml);
iTextRenderer.layout();
OutputStream os = new FileOutputStream(output);
iTextRenderer.createPDF(os);
os.close();
}
  • 上述代码将10种字体均加入到渲染器中,可根据使用情况添加一种或多种字体

  • 同时需要在HTML中设置font-family,font-family的值需要使用下图HashMapKey,不然无法渲染出中文

    img

    1
    2
    // 此次以使用Medium示例
    font-family: "Alibaba PuHuiTi 3.0 65 Medium";
  • 若加入多种同一类型的字体到渲染器,在HTML中可以使用

    1
    2
    3
    font-family: 'Alibaba PuHuiTi 3.0';
    font-style: normal;
    font-weight: 500;
    • 通过指定不同的weight来使用不同的字体

结合Freemarker模版来生成PDF

  • HTML内容的变量使用Freemarker语法进行替换
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
package cn.holelin.controller;

import cn.holelin.domain.AccountProofModel;
import com.lowagie.text.pdf.BaseFont;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xhtmlrenderer.pdf.ITextRenderer;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.ArrayList;

@Slf4j
@RestController
@RequiredArgsConstructor
public class PdfGenerationController {

private final Configuration configuration;

@PostMapping("/pdf/preview")
public ResponseEntity<byte[]> downloadPdfWithFixedHeaderAndFooter() {
AccountProofModel accountProofModel = AccountProofModel.builder()
.generationDate(LocalDate.now().toString())
.memberName("Hole Lin")
.memberAddress("中国江苏省苏州市")
.accountNo("88888888888888")
.bankName("ICBC")
.bankSwiftCode("ABCDEFG")
.bankAddress("中国江苏省苏州市工业园区")
.countryName("中国")
.build();
final ArrayList<String> strings = new ArrayList<>();
accountProofModel.setImageList(strings);
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
// 不建议直接创建Template实例,开销比较大,可以直接通过Configuration实例获取,有缓存机制
Template template = configuration.getTemplate("template.ftl");
String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, accountProofModel);
ITextRenderer renderer = new ITextRenderer();
final ClassPathResource classPathResource = new ClassPathResource("/fonts/simsun.ttc");
// 如果内容有中文则需要添加支持中文的字体
renderer.getFontResolver().addFont(classPathResource.getPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
renderer.setDocumentFromString(content);
renderer.layout();
renderer.createPDF(os);
renderer.finishPDF();
} catch (Exception e) {
log.error("Fail to generate pdf: {}", e.getMessage(), e);
return ResponseEntity.internalServerError().body(null);
}

HttpHeaders respHeaders = new HttpHeaders();
respHeaders.setContentType(MediaType.APPLICATION_PDF);
respHeaders.setContentDisposition(ContentDisposition.inline().filename("accountProof.pdf", StandardCharsets.UTF_8).build());
return new ResponseEntity<>(os.toByteArray(), respHeaders, HttpStatus.OK);
}
}
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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Running Headers and Footers</title>
<style>
@page {
size: A4;
margin: 40mm 10mm 50mm 10mm;

@top-left {
content: element(headerLeft);
}

@bottom-center {
content: element(footerCenter);
}
}

* {
padding: 0;
margin: 0;
}

body {
font-family: SimSun;
}

.headerLeft {
position: running(headerLeft);
}

.titleWrapper > div {
margin: 2px 0;
}

.footerCenter {
text-align: center;
position: running(footerCenter);
}

.footerTipsWrapper {
color: #C1A97D;
margin-top: 10px;
border-top: 2px solid #EFE7DA;
}

.footerTipsWrapper > div {
font-size: 12px;
margin-top: 12px;
}

.contentWrapper {
margin-top: -10px;
}

.paddingWrapper {
padding: 10px;
}

.accountIntroduction {
margin-top: 60px;
background-color: #EFE7DA;
border: 1px solid #EFE7DA;
border-radius: 10px;
}

.accountDetailsWrapper {
margin-top: 50px;
border: 3px solid #EFE7DA;
border-radius: 10px;
}

.subTitle {
font-weight: bold;
border-bottom: 2px solid #EFE7DA;
padding-bottom: 10px;
}

.accountDetails > div {
margin-top: 8px;
}
</style>
</head>
<body>
<div class="footerCenter">
<div class="footerTipsWrapper">
<div>www.aletaplanet.com | account@aletaplanet.com</div>
<div>MPHK Management Company Limited | Suite 615, 6/F, Ocean Centre, Harbour City, Tsim Sha Tsui, Tsim Sha Tsui,
Kowloon |<br/>
License No.: 21-10-03068
</div>
</div>
</div>

<div class="contentWrapper">
<div class="titleWrapper paddingWrapper">
<div><b>Proof of Account Details</b></div>
<div>Generated on: ${generationDate}</div>
</div>
<div class="tips paddingWrapper">To whom it may concern,</div>
<div class="accountIntroduction paddingWrapper">
<div><b>Personal account of ${memberName}</b></div>
<div style="margin-top: 10px;word-break: break-word">
This letter confirms the below account details allow ${memberName} residing at ${memberAddress} to receive payments into his/ her AP-1 Account:
</div>
</div>
<div class="accountDetailsWrapper paddingWrapper">
<div class="subTitle">Business account details</div>
<div class="accountDetails">
<div>Account Name: ${memberName}</div>
<div>Account Number: ${accountNo}</div>
<div>Bank Name: ${bankName}</div>
<div>Bank SWIFT/BIC: ${bankSwiftCode}</div>
<div>Bank Country: ${countryName}</div>
<div>Bank Address: ${bankAddress}</div>
</div>
</div>
</div>
</body>
</html>