18.1 概述
我们在后端系统中使用 iText 来创建和操作PDF文件的开源库。iText 是由Bruno Lowagie、Paulo Soares等人编写的。Ohloh报告称2001年以来26个不同的贡献者进行了1万多次提交,超过100多万行代码。
开发者可以用iText来:
- 从XML文件或数据库来动态生成PDF文档
- 为浏览器生成PDF文档
- 利用PDF的许多互动功能
- 添加书签、页码、水印、条形码等
- 分割、拼接和处理PDF页面
- 自动填写PDF表单
- 给PDF文件添加数字签名
通常有如下需求的项目会使用iText:
- 内容不是提前准备好的,而是根据用户输入或数据库的实时信息来计算、处理。
- 内容非常多,PDF文件无法手动生成。
- 在批处理过程中,需要在无人值守模式下创建文档。
- 需要对内容进行定制或个性化。如最终用户的名字需要被放在多个页面上。
18.2 生成PDF的基本方法
本节介绍在后端生成PDF的基本方法。注意我们在生成的过程中把数据都放在了内存中,并没有在磁盘上生成临时的文件。这种作法很重要,后端程序要尽量避免生成太多临时文件。
18.2.1 设置依赖
开发前先设置如下的依赖
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.2.0</version>
<type>pom</type>
</dependency>
18.2.2 PDF版Hello World
新建如下的控制器类:
package com.longser.union.cloud.controller;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@RequestMapping("/test/pdf")
public class PDFController {
@GetMapping("/hello")
public void getHelloPdf(HttpServletResponse response) throws IOException {
response.setHeader("content-Type", "application/pdf");
response.setHeader("Content-Disposition", "attachment;filename=hello.pdf");
PdfWriter writer = new PdfWriter(response.getOutputStream());
PdfDocument pdfDocument = new PdfDocument(writer);
Document document = new Document(pdfDocument);
document.add(new Paragraph("Hello World!"));
document.close();
}
}
重新启动应用程序后在浏览器中访问 http://localhost:8088/test/pdf/hello,可以下载新生成的PDF文件。
18.2.3 在PDF中的做表格
增加新的方法,生成一个包含表格的PDF:
@GetMapping("/table")
public void getPdfTable(HttpServletResponse response) throws IOException {
response.setHeader("content-Type", "application/pdf");
response.setHeader("Content-Disposition", "inline");
PdfWriter writer = new PdfWriter(response.getOutputStream());
PdfDocument pdfDocument = new PdfDocument(writer);
Document document = new Document(pdfDocument);
Table table = new Table(3);
Cell cell = new Cell(1, 3)
.setTextAlignment(TextAlignment.CENTER)
.add(new Paragraph("Cell with colspan 3"));
table.addCell(cell);
cell = new Cell(2, 1)
.add(new Paragraph("Cell with rowspan 2"))
.setVerticalAlignment(VerticalAlignment.MIDDLE);
table.addCell(cell);
table.addCell("Cell 1.1");
table.addCell(new Cell().add(new Paragraph("Cell 1.2")));
table.addCell(new Cell()
.add(new Paragraph("Cell 2.1"))
.setBackgroundColor(ColorConstants.LIGHT_GRAY)
.setMargin(5));
table.addCell(new Cell()
.add(new Paragraph("Cell 1.2"))
.setBackgroundColor(ColorConstants.LIGHT_GRAY)
.setFontColor(new DeviceRgb(0,255,255))
.setPadding(5));
document.add(table);
document.close();
}
重新启动应用程序后在浏览器中访问http://localhost:8088/getpdf-table,因为设置了`Content-Disposition`为`inline`可以直接在浏览器中看到新生成的PDF:
18.3 更多的PDF内容和格式
18.3.1 设置页面尺寸和格式
可以在生成 Document
对象时指定页面格式,在生成之后设定页面边距:
Document document = new Document(pdfDocument,PageSize.A5.rotate());
document.setMargins(20, 20, 40, 40);
18.3.2 设置PDF文件属性
在打开Document
对象之后,可以设置PDF文件的标题(Title)、作者(Author)、主题(Subject)、关键字(Keywords)、语言和创建日期等内容:
PdfDocumentInfo info = pdfDocument.getDocumentInfo();
info.setTitle("文件的标题");
info.setAuthor("David");
info.setSubject("文件的主题");
info.setKeywords("key word1, key word2");
info.setCreator("iText 7 tutorial example");
18.3.3 定义中英文字体
字体程序 | 编码 |
---|---|
STSong-Light | UniGB-UCS2-H |
MHei-Medium | UniCNS-UCS2-H |
MSung-Light | UniCNS-UCS2-H |
HeiseiKakuGo-W5 | UniJIS-UCS2-H |
HeiseiMin-W3 | UniJIS-UCS2-H |
HYGoThic-Medium | UniKS-UCS2-H |
HYSMyeongJo-Medium | UniKS-UCS2-H |
如果要在PDF中使用中文,需要显示地定义中文字体供使用,下面的代码使用依赖项itext-asian
中携带的中文字体,定义了两种不同的格式:
@GetMapping("/hello")
public void getHelloPdf(HttpServletResponse response) throws IOException {
response.setHeader("content-Type", "application/pdf");
response.setHeader("Content-Disposition", "inline");
PdfWriter writer = new PdfWriter(response.getOutputStream());
PdfDocument pdfDocument = new PdfDocument(writer);
Document document = new Document(pdfDocument);
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
PdfFont bold = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
Text title = new Text("Harry Potter").setFont(bold).setFontSize(20);
Text author = new Text("J. K. Rowling.").setFont(font).setFontSize(14);
Paragraph paragraph = new Paragraph().add(title).add(" by ").add(author);
document.add(paragraph);
document.add(new Paragraph("Hello Magical World!"));
PdfFont fontHei = PdfFontFactory.createFont("MHei-Medium",
"UniCNS-UCS2-H", FORCE_NOT_EMBEDDED);
PdfFont fontSong = PdfFontFactory.createFont("STSong-Light",
"UniGB-UCS2-H", FORCE_NOT_EMBEDDED);
title = new Text("《哈利波特》").setFont(fontHei).setFontSize(20);
author = new Text(" 作者:J. K. 罗琳").setFont(fontSong).setFontSize(14);
paragraph = new Paragraph().add(title).add(author);
document.add(paragraph);
PdfFont aliFont = PdfFontFactory.createFont("/Users/david/Downloads/AlibabaPuHuiTi-2-65-Medium.ttf",
PdfEncodings.IDENTITY_H);
paragraph = new Paragraph("你好,魔法世界!").setFont(aliFont).setFontSize(16);
document.add(paragraph);
document.close();
}
说明:
- 需要 import static com.itextpdf.kernel.font.PdfFontFactory.EmbeddingStrategy.* 其他 import 可用 IDEA 协助完成
- 指定额外字体时,路径为操作系统绝对路径
18.3.4 插入图片
下面的代码插入一个制定了物理路径的图片文件:
@GetMapping("/image")
public void getPdfImage(HttpServletResponse response) throws IOException {
response.setHeader("content-Type", "application/pdf");
response.setHeader("Content-Disposition", "inline");
PdfWriter writer = new PdfWriter(response.getOutputStream());
PdfDocument pdfDocument = new PdfDocument(writer);
Document document = new Document(pdfDocument);
Image jamsBond = new Image(ImageDataFactory.create("src/main/resources/static/007.jpg"));
Paragraph paragraph = new Paragraph("No time to die")
.add(jamsBond);
document.add(paragraph);
document.add(jamsBond);
document.close();
}
18.3.5 其它与内容有关的操作
iText 还可以设置密码、添加页面,添加水印、画图、添加目录、添加页眉页脚、设计嵌套表格、插入条码和二维码、操作页面、拆分或合并文件、操作附件等。详细的可以参见官方的示例:
- https://github.com/itext/i7js-jumpstart/tree/develop/src/main/java/tutorial
- https://github.com/itext/i7js-examples
18.4 根据模板生成PDF文件
尽管我们可以通过代码生成PDF文件的全部内容,但这样的作法过于繁琐。在实际的业务场景中,我们经常会根据数据查询或业务处理的结果生成固定格式的PDF文件,此时我们可以采用在预先定义的PDF模板填充内容的方法来简化代码逻辑。18.4.1 编制PDF模板文件
首先用Word软件编辑目标模板
然后保存为PDF文件
用Adobe Acrobat软件(或其他可以编辑PDF文件的软件)打开模板,点击“准备表单”功能
软件会自动识别分析文件内容,在可能存在表单项的是地方自动放置可编辑的表单组件
我们可以修改表单组件的名称、外观(字体、字号)和对齐方式等属性
我们把修改好的模板文件放到src/main/resources/static目录下。
18.4.2 填充数据生成功PDF
下面是用模板填充数据并生成PDF文件的代码(注意其中中文字体的设置方法)
@GetMapping("/filled")
public void getFilledTable(HttpServletResponse response) throws IOException {
String templatePath = "src/main/resources/static/TableTemplate.pdf";
Map<String, String> tableData = new HashMap<>(5);
tableData.put("table_id", "123456789");
tableData.put("table_name", "刘德华");
tableData.put("table_gender", "男");
tableData.put("table_birthday", "1992-01-01");
tableData.put("table_mobile", "13808881234");
response.setHeader("content-Type", "application/pdf");
response.setHeader("Content-Disposition", "inline");
PdfWriter writer = new PdfWriter(response.getOutputStream());
PdfReader reader = new PdfReader(templatePath);
PdfDocument pdfDocument = new PdfDocument(reader, writer);
PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDocument, true);
Map<String, PdfFormField> fieldMap = form.getFormFields();
for (String key : fieldMap.keySet()) {
PdfFormField formField = fieldMap.get(key);
if (formField == null) {
continue;
}
formField.setValue(tableData.get(key));
}
form.flattenFields();
pdfDocument.close();
}
18.5 将HTML页面转换为PDF
后端生成 PDF 的另外一个常见场景是把 HTML 内容或文件转换成 PDF。为实现这个功能,需要添加如下的依赖
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>4.0.0</version>
</dependency>
下面是一个调用 html2pdf 的简单示意
package com.longser.union.cloud.controller;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider;
import com.itextpdf.io.font.FontProgram;
import com.itextpdf.io.font.FontProgramFactory;
import com.itextpdf.layout.font.FontProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@RestController
@RequestMapping("/test/pdf/html")
public class HTML2PDF {
public static final String BASEURI = "./src/main/resources/static/htmlsamples";
private void setHeader(HttpServletResponse response, boolean inline) {
response.setHeader("content-Type", "application/pdf");
if(inline) {
response.setHeader("Content-Disposition", "inline");
} else {
response.setHeader("Content-Disposition", "attachment;filename=html.pdf");
}
}
@GetMapping("/string")
public void getString2Pdf(HttpServletResponse response, @RequestParam(
value = "inline", defaultValue = "true", required = false) boolean inline) throws IOException {
setHeader(response, inline);
String htmlBody = "<h1>Oh</h1><p>Hello world.</p>";
HtmlConverter.convertToPdf(htmlBody, response.getOutputStream());
}
@GetMapping("/image")
public void getStringImage2Pdf(HttpServletResponse response, @RequestParam(
value = "inline", defaultValue = "true", required = false) boolean inline) throws IOException {
setHeader(response, inline);
String htmlBody = "<p>Longser Technologies Ltd.</p><img src=\"images/yun-logo.png\">";
ConverterProperties properties = new ConverterProperties();
properties.setBaseUri(BASEURI);
HtmlConverter.convertToPdf(htmlBody, response.getOutputStream(), properties);
}
@GetMapping("/file")
public void getFile2Pdf(HttpServletResponse response, @RequestParam(
value = "inline", defaultValue = "true", required = false) boolean inline) throws IOException {
setHeader(response, inline);
String htmlPath = "./src/main/resources/static/htmlsamples/hello.html";
File htmlFile = new File(htmlPath);
InputStream inputStream = new FileInputStream(htmlFile);
ConverterProperties properties = new ConverterProperties();
FontProvider fontProvider = new DefaultFontProvider();
fontProvider.addFont("MHei-Medium", "UniCNS-UCS2-H");
// 如果用字体文件,用下面的方式
//FontProgram fontProgram = FontProgramFactory.createFont(fontFilePath);
//fontProvider.addFont(fontProgram);
properties.setFontProvider(fontProvider);
properties.setBaseUri(BASEURI);
HtmlConverter.convertToPdf(inputStream, response.getOutputStream(), properties);
}
}
下面是 hello.html 的内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<h1 style="color:#ff0000;">This is from a HTML file.</h1>
<p>北京中科朗思信息技术有限公司</p>
<img src="./images/yun-logo.png" />
</body>
</html>
更详细的使用方法请阅读官方教程 iText 7: Converting HTML to PDF with pdfHTML 。由于这个教程中代码的链接需要科学上网才能访问。如果无法访问,也可以去 GitHub 上阅读代码 https://github.com/itext/i7js-examples/tree/develop/src/main/java/com/itextpdf/samples/htmlsamples。
18.6 用Open PDF代替iText PDF
18.6.1 关于OpenPDF
iText 4.2.0之前的版本是在MPL和LGPL许可证下分发的,允许用户在闭源软件项目中使用。2009年底,iText第5版发布,其许可证被更换为Affero通用公共许可证第3版。 那些不愿意提供其源代码的项目,可以购买iText第5版的商业许可,或没有任何变化的继续使用iText的以前版本(其许可证更宽松)。然而,开发商Bruno Lowagie警告说,第5版之前的版本可能包含非LGPL授权的代码,因而以前版本的闭源项目的用户可能需要为侵犯著作权负责。虽然AGPL库可以链接到GPL的程序,但AGPL许可证与GPL许可证不兼容。
有人为了解决这些版权问题,推出了 OpenPDF 项目。它是一个完全免费的Java库,用于创建和编辑具有LGPL和MPL开源许可证的PDF文件。OpenPDF基于iText的一个分支。
18.6.2 OpenPDF依赖项
用Open PDF代替iText PDF非常简单。首先使用如下的依赖项目
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.26</version>
</dependency>
18.6.3 OpenPDF与iText PDF差别
在类方法和逻辑方面,OpenPDF 基本上与 iText 5近似,绝大多数 iText 5 的代码在 OpenPDF 中均可以工作,但有些细节需要调整:
- 修改import定义,把所有的
com.itextpdf.text.*
都修改成com.lowagie.text.*
- Rectangle.setBackgroundColor的参数改为
java.awt.Color
类型 - 用
Document.setDocumentLanguage
而不是Document.addLanguage
方法设置语言 - 使用
AcroFields.getAllFields
而不是AcroFields.getFields
获得所有的表单元素,并且获得的结果的各项顺序与实际的顺序(这也是前文我们使用form.getFieldItem(name).getTabOrder(0)这样代码的原因)。
显然,OpenPDF 的类方法和逻辑与 iText 7差别很大。
18.7 参考资料
下面是较为有作用的参考资料:
- itext官方网站
- iText Knowledge Base
- iText 示例
- iText 7: Jump-Start Tutorial for Java 教程 及 示例代码
- iText 7: Converting HTML to PDF with pdfHTML 教程 及 示例代码
版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。