参考文献
调研历程
首先查阅资料,了解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 Saucer
将HTML
转换为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); iTextRenderer.createPDF(os); os.close(); }
输出带有中文内容的PDF
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); 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
的值需要使用下图HashMap
的Key
,不然无法渲染出中文
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 ;
结合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 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 >