1.背景
在某些业务场景中,须要提供相关的电子凭证,好比网银/支付宝中转帐的电子回单,签约的电子合同等。方便用户查看,下载,打印。目前经常使用的解决方案是,把相关数据信息,生成对应的pdf文件返回给用户。css
本文源码:http://git.oschina.net/lujianing/java_pdf_demohtml
2.iText
iText是著名的开放源码的站点sourceforge一个项目,是用于生成PDF文档的一个java类库。经过iText不只能够生成PDF或rtf的文档,并且能够将XML、Html文件转化为PDF文件。 前端
iText 官网:http://itextpdf.com/java
iText 开发文档: http://developers.itextpdf.com/developers-homegit
iText目前有两套版本iText5和iText7。iText5应该是网上用的比较多的一个版本。iText5由于是不少开发者参与贡献代码, 所以在一些规范和设计上存在不合理的地方。iText7是后来官方针对iText5的重构,两个版本差异仍是挺大的。不过在实际使用中,通常用到的都比较 简单,因此不用特别拘泥于使用哪一个版本。好比咱们在http://mvnrepository.com/中搜索iText,出来的都是iText5的依赖。github
来个最简单的例子:web
添加依赖:apache
<!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.11</version> </dependency>
测试代码:JavaToPdf浏览器
package com.lujianing.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; /** * Created by lujianing on 2017/5/7. */ 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(); } }
运行结果:服务器
3.iText-中文支持
iText默认是不支持中文的,所以须要添加对应的中文字体,好比黑体simhei.ttf
可参考文档:http://developers.itextpdf.com/examples/font-examples/using-fonts#1227-tengwarquenya1.java
测试代码:JavaToPdfCN
package com.lujianing.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; /** * Created by lujianing on 2017/5/7. */ 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 f1 = FontFactory.getFont(FONT, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); document.add(new Paragraph("hello world,我是鲁家宁", f1)); document.close(); writer.close(); } }
输出结果:
4.iText-Html渲染
在一些比较复杂的pdf布局中,咱们能够经过html去生成pdf
可参考文档:http://developers.itextpdf.com/examples/xml-worker-itext5/xml-worker-examples
添加依赖:
<!-- https://mvnrepository.com/artifact/com.itextpdf.tool/xmlworker --> <dependency> <groupId>com.itextpdf.tool</groupId> <artifactId>xmlworker</artifactId> <version>5.5.11</version> </dependency>
添加模板:template.html
<!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"> 你好,鲁家宁 </div> </body> </html>
测试代码:JavaToPdfHtml
package com.lujianing.test; 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.lujianing.test.util.PathUtil; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; /** * Created by lujianing on 2017/5/7. */ 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 { // step 1 Document document = new Document(); // step 2 PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(DEST)); // step 3 document.open(); // step 4 XMLWorkerFontProvider fontImp = new XMLWorkerFontProvider(XMLWorkerFontProvider.DONTLOOKFORFONTS); fontImp.register(FONT); XMLWorkerHelper.getInstance().parseXHtml(writer, document, new FileInputStream(HTML), null, Charset.forName("UTF-8"), fontImp); // step 5 document.close(); } }
输出结果:
须要注意:
1.html中必须使用标准的语法,标签必定须要闭合
2.html中若是有中文,须要在样式中添加对应字体的样式
5.iText-Html-Freemarker渲染
在实际使用中,html内容都是动态渲染的,所以咱们须要加入模板引擎支持,可使用FreeMarker/Velocity,这里使用FreeMarker举例
添加FreeMarke依赖:
<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.19</version> </dependency>
添加模板:template_freemarker.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <title>Title</title> <style> body{ font-family:SimHei; } .blue{ color: blue; } </style> </head> <body> <div class="blue"> 你好,${name} </div> </body> </html>
测试代码:JavaToPdfHtmlFreeMarker
package com.lujianing.test; 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.lujianing.test.util.PathUtil; import freemarker.template.Configuration; import freemarker.template.Template; 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; /** * Created by lujianing on 2017/5/7. */ 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(); //freemarker的模板目录 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(); data.put("name","鲁家宁"); String content = JavaToPdfHtmlFreeMarker.freeMarkerRender(data,HTML); JavaToPdfHtmlFreeMarker.createPdf(content,DEST); } public static void createPdf(String content,String dest) throws IOException, DocumentException { // step 1 Document document = new Document(); // step 2 PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(dest)); // step 3 document.open(); // step 4 XMLWorkerFontProvider fontImp = new XMLWorkerFontProvider(XMLWorkerFontProvider.DONTLOOKFORFONTS); fontImp.register(FONT); XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(content.getBytes()), null, Charset.forName("UTF-8"), fontImp); // step 5 document.close(); } /** * freemarker渲染html */ 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属性等,所以咱们要引入新的组件
若中文变量仍是不显示,则在pom.xml里添加以下:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<!--增长的配置,过滤ttf文件的匹配-->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<configuration>
<encoding>UTF-8</encoding>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>ttf</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
备注:工具类的方法 PathUtil.getCurrentPath() = "src/main/resources/",(这个对新手有用)
6.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高级特性的解析。
添加依赖:
<!-- https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf --> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf</artifactId> <version>9.1.5</version> </dependency> <!-- https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf-itext5 --> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf-itext5</artifactId> <version>9.1.5</version> </dependency>
添加模板:template_freemarker_fs.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <title>Title</title> <style> body{ font-family:SimHei; } .color{ color: green; } .pos{ position:absolute; left:200px; top:5px; width: 200px; font-size: 10px; } </style> </head> <body> <img src="logo.png" width="600px"/> <div class="color pos"> 你好,${name} </div> </body> </html>
测试代码:JavaToPdfHtmlFreeMarker
package com.lujianing.test.flyingsaucer; import com.itextpdf.text.DocumentException; import com.itextpdf.text.pdf.BaseFont; import com.lujianing.test.util.PathUtil; import freemarker.template.Configuration; import freemarker.template.Template; import org.xhtmlrenderer.pdf.ITextFontResolver; import org.xhtmlrenderer.pdf.ITextRenderer; 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; /** * Created by lujianing on 2017/5/7. */ 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()+"/logo.png"; private static Configuration freemarkerCfg = null; static { freemarkerCfg =new Configuration(); //freemarker的模板目录 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(); data.put("name","鲁家宁"); String content = JavaToPdfHtmlFreeMarker.freeMarkerRender(data,HTML); JavaToPdfHtmlFreeMarker.createPdf(content,DEST); } /** * freemarker渲染html */ 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); // 解析html生成pdf render.setDocumentFromString(content); //解决图片相对路径的问题 render.getSharedContext().setBaseURL(LOGO_PATH); render.layout(); render.createPDF(new FileOutputStream(dest)); } }
输出结果:
在某些场景下,html中的静态资源是在本地,咱们可使用render.getSharedContext().setBaseURL()加载文件资源,注意资源URL须要使用文件协议 "file://"。
对于生成的pdf页面大小,能够用css的@page属性设置。
7.PDF转图片
在某些场景中,咱们可能只须要返回图片格式的电子凭证,咱们可使用Jpedal组件,把pdf转成图片
添加依赖:
<!-- https://mvnrepository.com/artifact/org.jpedal/jpedal-lgpl --> <dependency> <groupId>org.jpedal</groupId> <artifactId>jpedal-lgpl</artifactId> <version>4.74b27</version> </dependency>
测试代码:JavaToPdfImgHtmlFreeMarker
package com.lujianing.test.flyingsaucer; import com.itextpdf.text.DocumentException; import com.itextpdf.text.pdf.BaseFont; import com.lujianing.test.util.PathUtil; import freemarker.template.Configuration; import freemarker.template.Template; 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 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; /** * Created by lujianing on 2017/5/7. */ 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(); //freemarker的模板目录 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(); data.put("name","鲁家宁"); 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(); } /** * freemarker渲染html */ 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; } /** * 根据模板生成pdf文件流 */ 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(); } // 解析html生成pdf 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; } /** * 根据pdf二进制文件 生成图片文件 * * @param bytes pdf二进制 * @param scaling 清晰度 * @param pageNum 页数 */ public static ByteArrayOutputStream pdfToImg(byte[] bytes, float scaling, int pageNum,String formatName) { //推荐的方法打开PdfDecoder PdfDecoder pdfDecoder = new PdfDecoder(true); FontMappings.setFontReplacements(); //修改图片的清晰度 pdfDecoder.scaling = scaling; ByteArrayOutputStream out = new ByteArrayOutputStream(); try { //打开pdf文件,生成PdfDecoder对象 pdfDecoder.openPdfArray(bytes); //bytes is byte[] array with PDF //获取第pageNum页的pdf 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设置图片的分辨率(不一样分辨率下文件大小不一样) ,支持多种图片格式,具体更多可自行研究
8.总结
对于电子凭证的技术方案,总结以下:
1.html模板+model数据,经过freemarker进行渲染,便于维护和修改
2.渲染后的html流,可经过Flying Saucer组件生成pdf文件流,或者生成pdf后再转成jpg文件流
3.在Web项目中,对应的文件流,能够经过ContentType设置,在线查看/下载,不需经过附件服务
9.纯前端解决方案
还有一种解决方案是使用PhantomJS
git地址: https://github.com/ariya/phantomjs
PhantomJS 是一个基于 WebKit 的服务器端 JavaScript API。它全面支持web而不需浏览器支持,其快速,原生支持各类Web标准: DOM 处理, CSS 选择器, JSON, Canvas, 和 SVG。 PhantomJS 能够用于 页面自动化 , 网络监测 , 网页截屏 ,以及 无界面测试 等。
具体方法可自行查询。