引言 在某些业务场景中,需要提供相关的电子凭证,比如网银/支付宝中转账的电子回单,签约的电子合同、证书等。方便用户查看,下载,打印。目前常用的解决方案是,把相关数据信息,生成对应的PDF文件返回给用户。之前有写过一篇博客关于JAVA实现HTML转PDF ,不同场景下的业务不同,现在需要使用PDF生成证书,这篇博客主要介绍iText的使用。
本博客项目地址:https://github.com/mx-go/java_pdf_demo
iText介绍 iText是著名的开放源码的站点sourceforge一个项目,是用于生成PDF文档的一个JAVA类库。通过iText不仅可以生成PDF或rtf的文档,而且可以将XML、HTML文件转化为PDF文件。
iText 官网:http://itextpdf.com/
iText 开发文档: http://developers.itextpdf.com/developers-home
iText目前有两套版本iText5和iText7。iText5应该是网上用的比较多的一个版本。iText5因为是很多开发者参与贡献代码,因此在一些规范和设计上存在不合理的地方。iText7是后来官方针对iText5的重构,两个版本差别还是挺大的。不过在实际使用中,一般用到的都比较简单,所以不用特别拘泥于使用哪个版本。比如我们在http://mvnrepository.com/中搜索iText,出来的都是iText5的依赖。
iText简单使用 添加依赖
1 2 3 4 5 6 <dependency > <groupId > com.itextpdf</groupId > <artifactId > itextpdf</artifactId > <version > 5.5.11</version > </dependency >
测试代码:JavaToPdf
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 package com.rainbowhorse.test;import com.itextpdf.text.Document;import com.itextpdf.text.DocumentException;import com.itextpdf.text.Paragraph;import com.itextpdf.text.pdf.PdfWriter;import java.io.FileNotFoundException;import java.io.FileOutputStream;public class JavaToPdf { private static final String DEST = "target/HelloWorld.pdf" ; public static void main (String[] args) throws FileNotFoundException, DocumentException { Document document = new Document(); PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(DEST)); document.open(); document.add(new Paragraph("hello world" )); document.close(); writer.close(); } }
运行结果
iText中文支持 iText默认是不支持中文的,因此需要添加对应的中文字体,比如黑体simhei.ttf
可参考文档:http://developers.itextpdf.com/examples/font-examples/using-fonts#1227-tengwarquenya1.java
测试代码:JavaToPdfCN
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 package com.rainbowhorse.test;import com.itextpdf.text.Document;import com.itextpdf.text.DocumentException;import com.itextpdf.text.Font;import com.itextpdf.text.FontFactory;import com.itextpdf.text.Paragraph;import com.itextpdf.text.pdf.BaseFont;import com.itextpdf.text.pdf.PdfWriter;import java.io.FileNotFoundException;import java.io.FileOutputStream;public class JavaToPdfCN { private static final String DEST = "target/HelloWorld_CN.pdf" ; private static final String FONT = "simhei.ttf" ; public static void main (String[] args) throws FileNotFoundException, DocumentException { Document document = new Document(); PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(DEST)); document.open(); Font font = FontFactory.getFont(FONT, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); document.add(new Paragraph("hello world,我是rainbowhorse。" , font)); document.close(); writer.close(); } }
运行结果
iText-HTML渲染 在一些比较复杂的PDF布局中,我们可以通过HTML去生成PDF
可参考文档:http://developers.itextpdf.com/examples/xml-worker-itext5/xml-worker-examples
添加依赖
1 2 3 4 5 6 <dependency > <groupId > com.itextpdf.tool</groupId > <artifactId > xmlworker</artifactId > <version > 5.5.11</version > </dependency >
添加模板:template.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <title > Title</title > <style > body { font-family : SimHei; } .red { color : red; } </style > </head > <body > <div class ="red" > 你好,rainbowhorse</div > </body > </html >
测试代码:JavaToPdfHtml
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 package com.rainbowhorse.test;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.charset.Charset;import com.itextpdf.text.Document;import com.itextpdf.text.DocumentException;import com.itextpdf.text.pdf.PdfWriter;import com.itextpdf.tool.xml.XMLWorkerFontProvider;import com.itextpdf.tool.xml.XMLWorkerHelper;import com.rainbowhorse.test.util.PathUtil;public class JavaToPdfHtml { private static final String DEST = "target/HelloWorld_CN_HTML.pdf" ; private static final String HTML = PathUtil.getCurrentPath() + "/template.html" ; private static final String FONT = "simhei.ttf" ; public static void main (String[] args) throws IOException, DocumentException { Document document = new Document(); PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(DEST)); document.open(); XMLWorkerFontProvider fontImp = new XMLWorkerFontProvider(XMLWorkerFontProvider.DONTLOOKFORFONTS); fontImp.register(FONT); XMLWorkerHelper.getInstance().parseXHtml(writer, document, new FileInputStream(HTML), null , Charset.forName("UTF-8" ), fontImp); document.close(); } }
运行结果
注意:
HTML中必须使用标准的语法,标签一定需要闭合。
HTML中如果有中文,需要在样式中添加对应字体的样式。
iText-HTML-Freemarker渲染 在实际使用中,HTML内容都是动态渲染的,因此我们需要加入模板引擎支持,可以使用FreeMarker/Velocity,这里使用FreeMarker举例。
添加FreeMarke依赖
1 2 3 4 5 6 <dependency > <groupId > org.freemarker</groupId > <artifactId > freemarker</artifactId > <version > 2.3.19</version > </dependency >
添加模板:template_freemarker.html
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 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <title > Title</title > <style > body { font-family : SimHei; } .blue { color : blue; } .pos { position : absolute; left : 100px ; top : 150px } </style > </head > <body > <div class ="blue pos" > 你好,${name}</div > </body > </html >
测试代码:JavaToPdfHtmlFreeMarker
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 package com.rainbowhorse.test;import java.io.ByteArrayInputStream;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.StringWriter;import java.io.Writer;import java.nio.charset.Charset;import java.util.HashMap;import java.util.Map;import com.itextpdf.text.Document;import com.itextpdf.text.DocumentException;import com.itextpdf.text.pdf.PdfWriter;import com.itextpdf.tool.xml.XMLWorkerFontProvider;import com.itextpdf.tool.xml.XMLWorkerHelper;import com.rainbowhorse.test.util.PathUtil;import freemarker.template.Configuration;import freemarker.template.Template;public class JavaToPdfHtmlFreeMarker { private static final String DEST = "target/HelloWorld_CN_HTML_FREEMARKER.pdf" ; private static final String HTML = "template_freemarker.html" ; private static final String FONT = "simhei.ttf" ; private static Configuration freemarkerCfg = null ; static { freemarkerCfg = new Configuration(); try { freemarkerCfg.setDirectoryForTemplateLoading(new File(PathUtil.getCurrentPath())); } catch (IOException e) { e.printStackTrace(); } } public static void main (String[] args) throws IOException, DocumentException { Map<String, Object> data = new HashMap<String, Object>(16 ); data.put("name" , "rainbowhorse" ); String content = JavaToPdfHtmlFreeMarker.freeMarkerRender(data, HTML); JavaToPdfHtmlFreeMarker.createPdf(content, DEST); } public static void createPdf (String content, String dest) throws IOException, DocumentException { Document document = new Document(); PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(dest)); document.open(); XMLWorkerFontProvider fontImp = new XMLWorkerFontProvider(XMLWorkerFontProvider.DONTLOOKFORFONTS); fontImp.register(FONT); XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(content.getBytes()), null , Charset.forName("UTF-8" ), fontImp); document.close(); } public static String freeMarkerRender (Map<String, Object> data, String htmlTmp) { Writer out = new StringWriter(); try { Template template = freemarkerCfg.getTemplate(htmlTmp); template.setEncoding("UTF-8" ); template.process(data, out); out.flush(); return out.toString(); } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException ex) { ex.printStackTrace(); } } return null ; } }
运行结果
目前为止,我们已经实现了iText通过HTML模板生成PDF的功能,但是实际应用中,我们发现iText并不能对高级的CSS样式进行解析,比如CSS中的position属性等,因此我们要引入新的组件。
Flying Saucer-CSS高级特性支持 Flying Saucer is a pure-Java library for rendering arbitrary well-formed XML (or XHTML) using CSS 2.1 for layout and formatting, output to Swing panels, PDF, and images.
Flying Saucer是基于iText的,支持对CSS高级特性的解析。
添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > org.xhtmlrenderer</groupId > <artifactId > flying-saucer-pdf</artifactId > <version > 9.1.5</version > </dependency > <dependency > <groupId > org.xhtmlrenderer</groupId > <artifactId > flying-saucer-pdf-itext5</artifactId > <version > 9.1.5</version > </dependency >
添加模板:template_freemarker_fs.html
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 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <title > Title</title > <style > @page { size:297mm 230mm ; @top-left { content :element (header-left); }; @top-right { content : element (header-right) }; @bottom-left { content : element (footer-left) }; @bottom-right { content : element (footer-right) }; } body { font-family : SimHei; } .color { color : green; } .pos { position : absolute; left : 200px ; top : 200px ; width : 200px ; font-size : 20px ; } </style > </head > <body > <img src ="logo.jpg" /> <div class ="color pos" > 你好,${name}</div > </body > </html >
测试代码:JavaToPdfHtmlFreeMarker:
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 package com.rainbowhorse.test.flyingsaucer;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.StringWriter;import java.io.Writer;import java.util.HashMap;import java.util.Map;import org.xhtmlrenderer.pdf.ITextFontResolver;import org.xhtmlrenderer.pdf.ITextRenderer;import com.itextpdf.text.DocumentException;import com.itextpdf.text.pdf.BaseFont;import com.rainbowhorse.test.util.PathUtil;import freemarker.template.Configuration;import freemarker.template.Template;public class JavaToPdfHtmlFreeMarker { private static final String DEST = "target/HelloWorld_CN_HTML_FREEMARKER_FS.pdf" ; private static final String HTML = "template_freemarker_fs.html" ; private static final String FONT = "simhei.ttf" ; private static final String LOGO_PATH = "file:/" + PathUtil.getCurrentPath() + "/" ; private static Configuration freemarkerCfg = null ; static { freemarkerCfg = new Configuration(); try { freemarkerCfg.setDirectoryForTemplateLoading(new File(PathUtil.getCurrentPath())); } catch (IOException e) { e.printStackTrace(); } } public static void main (String[] args) throws IOException, DocumentException, com.lowagie.text.DocumentException { Map<String, Object> data = new HashMap<String, Object>(16 ); data.put("name" , "rainbowhorse" ); String content = JavaToPdfHtmlFreeMarker.freeMarkerRender(data, HTML); JavaToPdfHtmlFreeMarker.createPdf(content, DEST); } public static String freeMarkerRender (Map<String, Object> data, String htmlTmp) { Writer out = new StringWriter(); try { Template template = freemarkerCfg.getTemplate(htmlTmp); template.setEncoding("UTF-8" ); template.process(data, out); out.flush(); return out.toString(); } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException ex) { ex.printStackTrace(); } } return null ; } public static void createPdf (String content, String dest) throws IOException, DocumentException, com.lowagie.text.DocumentException { ITextRenderer render = new ITextRenderer(); ITextFontResolver fontResolver = render.getFontResolver(); fontResolver.addFont(FONT, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); render.setDocumentFromString(content); render.getSharedContext().setBaseURL(LOGO_PATH); render.layout(); render.createPDF(new FileOutputStream(dest)); } }
运行结果
在某些场景下,HTML中的静态资源是在本地,我们可以使用render.getSharedContext().setBaseURL()加载文件资源,注意资源URL需要使用文件协议 “file://”。
对于生成的pdf页面大小,可以用css的@page属性设置。
PDF转图片 在某些场景中,我们可能只需要返回图片格式的电子凭证,我们可以使用Jpedal组件,把PDF转成图片。
添加依赖
1 2 3 4 5 6 <dependency > <groupId > org.jpedal</groupId > <artifactId > jpedal-lgpl</artifactId > <version > 4.74b27</version > </dependency >
测试代码:JavaToPdfImgHtmlFreeMarker
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 package com.rainbowhorse.test.flyingsaucer;import java.awt.image.BufferedImage;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.StringWriter;import java.io.Writer;import java.util.HashMap;import java.util.Map;import javax.imageio.ImageIO;import org.jpedal.PdfDecoder;import org.jpedal.exception.PdfException;import org.jpedal.fonts.FontMappings;import org.xhtmlrenderer.pdf.ITextFontResolver;import org.xhtmlrenderer.pdf.ITextRenderer;import com.itextpdf.text.DocumentException;import com.itextpdf.text.pdf.BaseFont;import com.rainbowhorse.test.util.PathUtil;import freemarker.template.Configuration;import freemarker.template.Template;public class JavaToPdfImgHtmlFreeMarker { private static final String DEST = "target/HelloWorld_CN_HTML_FREEMARKER_FS_IMG.png" ; private static final String HTML = "template_freemarker_fs.html" ; private static final String FONT = "simhei.ttf" ; private static final String LOGO_PATH = "file://" + PathUtil.getCurrentPath() + "/logo.png" ; private static final String IMG_EXT = "png" ; private static Configuration freemarkerCfg = null ; static { freemarkerCfg = new Configuration(); try { freemarkerCfg.setDirectoryForTemplateLoading(new File(PathUtil.getCurrentPath())); } catch (IOException e) { e.printStackTrace(); } } public static void main (String[] args) throws IOException, DocumentException, com.lowagie.text.DocumentException { Map<String, Object> data = new HashMap<String, Object>(16 ); data.put("name" , "rainbowhorse" ); String content = JavaToPdfImgHtmlFreeMarker.freeMarkerRender(data, HTML); ByteArrayOutputStream pdfStream = JavaToPdfImgHtmlFreeMarker.createPdf(content); ByteArrayOutputStream imgSteam = JavaToPdfImgHtmlFreeMarker.pdfToImg(pdfStream.toByteArray(), 2 , 1 , IMG_EXT); FileOutputStream fileStream = new FileOutputStream(new File(DEST)); fileStream.write(imgSteam.toByteArray()); fileStream.close(); } public static String freeMarkerRender (Map<String, Object> data, String htmlTmp) { Writer out = new StringWriter(); try { Template template = freemarkerCfg.getTemplate(htmlTmp); template.setEncoding("UTF-8" ); template.process(data, out); out.flush(); return out.toString(); } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException ex) { ex.printStackTrace(); } } return null ; } public static ByteArrayOutputStream createPdf (String content) { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); ITextRenderer render = new ITextRenderer(); ITextFontResolver fontResolver = render.getFontResolver(); try { fontResolver.addFont(FONT, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); } catch (com.lowagie.text.DocumentException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } render.setDocumentFromString(content); render.getSharedContext().setBaseURL(LOGO_PATH); render.layout(); try { render.createPDF(outStream); return outStream; } catch (com.lowagie.text.DocumentException e) { e.printStackTrace(); } finally { try { outStream.close(); } catch (IOException e) { e.printStackTrace(); } } return null ; } public static ByteArrayOutputStream pdfToImg (byte [] bytes, float scaling, int pageNum, String formatName) { PdfDecoder pdfDecoder = new PdfDecoder(true ); FontMappings.setFontReplacements(); pdfDecoder.scaling = scaling; ByteArrayOutputStream out = new ByteArrayOutputStream(); try { pdfDecoder.openPdfArray(bytes); BufferedImage img = pdfDecoder.getPageAsImage(pageNum); ImageIO.write(img, formatName, out); } catch (PdfException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return out; } }
输出结果
Jpedal支持将指定页PDF生成图片,pdfDecoder.scaling设置图片的分辨率(不同分辨率下文件大小不同) ,支持多种图片格式,具体更多可自行研究。
总结 对于电子凭证的技术方案,总结如下:
HTML模板+model数据,通过freemarker进行渲染,便于维护和修改。
渲染后的HTML流,可通过Flying Saucer组件生成HTML文件流,或者生成HTML后再转成jpg文件流。
在Web项目中,对应的文件流,可以通过ContentType设置,在线查看/下载,不需通过附件服务。