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 设置依赖

开发前先设置如下的依赖

  1. <dependency>
  2. <groupId>com.itextpdf</groupId>
  3. <artifactId>itext7-core</artifactId>
  4. <version>7.2.0</version>
  5. <type>pom</type>
  6. </dependency>

18.2.2 PDF版Hello World

新建如下的控制器类:

  1. package com.longser.union.cloud.controller;
  2. import com.itextpdf.kernel.pdf.PdfDocument;
  3. import com.itextpdf.kernel.pdf.PdfWriter;
  4. import com.itextpdf.layout.Document;
  5. import com.itextpdf.layout.element.Paragraph;
  6. import org.springframework.web.bind.annotation.GetMapping;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RestController;
  9. import javax.servlet.http.HttpServletResponse;
  10. import java.io.IOException;
  11. @RestController
  12. @RequestMapping("/test/pdf")
  13. public class PDFController {
  14. @GetMapping("/hello")
  15. public void getHelloPdf(HttpServletResponse response) throws IOException {
  16. response.setHeader("content-Type", "application/pdf");
  17. response.setHeader("Content-Disposition", "attachment;filename=hello.pdf");
  18. PdfWriter writer = new PdfWriter(response.getOutputStream());
  19. PdfDocument pdfDocument = new PdfDocument(writer);
  20. Document document = new Document(pdfDocument);
  21. document.add(new Paragraph("Hello World!"));
  22. document.close();
  23. }
  24. }

重新启动应用程序后在浏览器中访问 http://localhost:8088/test/pdf/hello,可以下载新生成的PDF文件。

18.2.3 在PDF中的做表格

增加新的方法,生成一个包含表格的PDF:

  1. @GetMapping("/table")
  2. public void getPdfTable(HttpServletResponse response) throws IOException {
  3. response.setHeader("content-Type", "application/pdf");
  4. response.setHeader("Content-Disposition", "inline");
  5. PdfWriter writer = new PdfWriter(response.getOutputStream());
  6. PdfDocument pdfDocument = new PdfDocument(writer);
  7. Document document = new Document(pdfDocument);
  8. Table table = new Table(3);
  9. Cell cell = new Cell(1, 3)
  10. .setTextAlignment(TextAlignment.CENTER)
  11. .add(new Paragraph("Cell with colspan 3"));
  12. table.addCell(cell);
  13. cell = new Cell(2, 1)
  14. .add(new Paragraph("Cell with rowspan 2"))
  15. .setVerticalAlignment(VerticalAlignment.MIDDLE);
  16. table.addCell(cell);
  17. table.addCell("Cell 1.1");
  18. table.addCell(new Cell().add(new Paragraph("Cell 1.2")));
  19. table.addCell(new Cell()
  20. .add(new Paragraph("Cell 2.1"))
  21. .setBackgroundColor(ColorConstants.LIGHT_GRAY)
  22. .setMargin(5));
  23. table.addCell(new Cell()
  24. .add(new Paragraph("Cell 1.2"))
  25. .setBackgroundColor(ColorConstants.LIGHT_GRAY)
  26. .setFontColor(new DeviceRgb(0,255,255))
  27. .setPadding(5));
  28. document.add(table);
  29. document.close();
  30. }

重新启动应用程序后在浏览器中访问http://localhost:8088/getpdf-table,因为设置了`Content-Disposition`为`inline`可以直接在浏览器中看到新生成的PDF:
image.png

18.3 更多的PDF内容和格式

本节讨论更多添加PDF内容和格式的方法。

18.3.1 设置页面尺寸和格式

可以在生成 Document 对象时指定页面格式,在生成之后设定页面边距:

  1. Document document = new Document(pdfDocument,PageSize.A5.rotate());
  2. document.setMargins(20, 20, 40, 40);

18.3.2 设置PDF文件属性

在打开Document对象之后,可以设置PDF文件的标题(Title)、作者(Author)、主题(Subject)、关键字(Keywords)、语言和创建日期等内容:

  1. PdfDocumentInfo info = pdfDocument.getDocumentInfo();
  2. info.setTitle("文件的标题");
  3. info.setAuthor("David");
  4. info.setSubject("文件的主题");
  5. info.setKeywords("key word1, key word2");
  6. info.setCreator("iText 7 tutorial example");

image.png

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中携带的中文字体,定义了两种不同的格式:

  1. @GetMapping("/hello")
  2. public void getHelloPdf(HttpServletResponse response) throws IOException {
  3. response.setHeader("content-Type", "application/pdf");
  4. response.setHeader("Content-Disposition", "inline");
  5. PdfWriter writer = new PdfWriter(response.getOutputStream());
  6. PdfDocument pdfDocument = new PdfDocument(writer);
  7. Document document = new Document(pdfDocument);
  8. PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
  9. PdfFont bold = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
  10. Text title = new Text("Harry Potter").setFont(bold).setFontSize(20);
  11. Text author = new Text("J. K. Rowling.").setFont(font).setFontSize(14);
  12. Paragraph paragraph = new Paragraph().add(title).add(" by ").add(author);
  13. document.add(paragraph);
  14. document.add(new Paragraph("Hello Magical World!"));
  15. PdfFont fontHei = PdfFontFactory.createFont("MHei-Medium",
  16. "UniCNS-UCS2-H", FORCE_NOT_EMBEDDED);
  17. PdfFont fontSong = PdfFontFactory.createFont("STSong-Light",
  18. "UniGB-UCS2-H", FORCE_NOT_EMBEDDED);
  19. title = new Text("《哈利波特》").setFont(fontHei).setFontSize(20);
  20. author = new Text(" 作者:J. K. 罗琳").setFont(fontSong).setFontSize(14);
  21. paragraph = new Paragraph().add(title).add(author);
  22. document.add(paragraph);
  23. PdfFont aliFont = PdfFontFactory.createFont("/Users/david/Downloads/AlibabaPuHuiTi-2-65-Medium.ttf",
  24. PdfEncodings.IDENTITY_H);
  25. paragraph = new Paragraph("你好,魔法世界!").setFont(aliFont).setFontSize(16);
  26. document.add(paragraph);
  27. document.close();
  28. }

说明:

  • 需要 import static com.itextpdf.kernel.font.PdfFontFactory.EmbeddingStrategy.* 其他 import 可用 IDEA 协助完成
  • 指定额外字体时,路径为操作系统绝对路径

下面是生成的结果
image.png

18.3.4 插入图片

下面的代码插入一个制定了物理路径的图片文件:

  1. @GetMapping("/image")
  2. public void getPdfImage(HttpServletResponse response) throws IOException {
  3. response.setHeader("content-Type", "application/pdf");
  4. response.setHeader("Content-Disposition", "inline");
  5. PdfWriter writer = new PdfWriter(response.getOutputStream());
  6. PdfDocument pdfDocument = new PdfDocument(writer);
  7. Document document = new Document(pdfDocument);
  8. Image jamsBond = new Image(ImageDataFactory.create("src/main/resources/static/007.jpg"));
  9. Paragraph paragraph = new Paragraph("No time to die")
  10. .add(jamsBond);
  11. document.add(paragraph);
  12. document.add(jamsBond);
  13. document.close();
  14. }

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软件编辑目标模板
    image.png
    然后保存为PDF文件
    image.png
    用Adobe Acrobat软件(或其他可以编辑PDF文件的软件)打开模板,点击“准备表单”功能

image.png
软件会自动识别分析文件内容,在可能存在表单项的是地方自动放置可编辑的表单组件
image.png
我们可以修改表单组件的名称、外观(字体、字号)和对齐方式等属性

image.pngimage.png
image.png
我们把修改好的模板文件放到src/main/resources/static目录下。

18.4.2 填充数据生成功PDF

下面是用模板填充数据并生成PDF文件的代码(注意其中中文字体的设置方法)

  1. @GetMapping("/filled")
  2. public void getFilledTable(HttpServletResponse response) throws IOException {
  3. String templatePath = "src/main/resources/static/TableTemplate.pdf";
  4. Map<String, String> tableData = new HashMap<>(5);
  5. tableData.put("table_id", "123456789");
  6. tableData.put("table_name", "刘德华");
  7. tableData.put("table_gender", "男");
  8. tableData.put("table_birthday", "1992-01-01");
  9. tableData.put("table_mobile", "13808881234");
  10. response.setHeader("content-Type", "application/pdf");
  11. response.setHeader("Content-Disposition", "inline");
  12. PdfWriter writer = new PdfWriter(response.getOutputStream());
  13. PdfReader reader = new PdfReader(templatePath);
  14. PdfDocument pdfDocument = new PdfDocument(reader, writer);
  15. PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDocument, true);
  16. Map<String, PdfFormField> fieldMap = form.getFormFields();
  17. for (String key : fieldMap.keySet()) {
  18. PdfFormField formField = fieldMap.get(key);
  19. if (formField == null) {
  20. continue;
  21. }
  22. formField.setValue(tableData.get(key));
  23. }
  24. form.flattenFields();
  25. pdfDocument.close();
  26. }

下面是程序运行的结果:
image.png

18.5 将HTML页面转换为PDF

后端生成 PDF 的另外一个常见场景是把 HTML 内容或文件转换成 PDF。为实现这个功能,需要添加如下的依赖

  1. <dependency>
  2. <groupId>com.itextpdf</groupId>
  3. <artifactId>html2pdf</artifactId>
  4. <version>4.0.0</version>
  5. </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 参考资料

下面是较为有作用的参考资料:

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。