來源:https://my.oschina.net/lujianing/blog/894365
1.背景
在某些業務場景中,需要提供相關的電子憑證,比如網銀/支付寶中轉賬的電子回單,簽約的電子合同等。方便用戶查看,下載,打印。目前常用的解決方案是,把相關數據信息,生成對應的pdf文件返回給用戶。
本文源碼:http://git.oschina.net/lujianing/java_pdf_demo
2.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的依賴。
來個最簡單的例子:
添加依賴:
<!-- 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
注意:setDirectoryForTemplateLoading(new File(PathUtil.getCurrentPath())) 指定的是目錄,所以如果你的服務器是linux/centos的話,需要指定為,例如:/root/pdfmodel/freemarker.html這樣指定。
如果你的項目生成的是jar包,就不能這樣指定,應該改為:setClassForTemplateLoading(當前文件.class, "/pdfmodel"); 其中pdfmodel已經被編譯成jar里面的東西了
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屬性等,因此我們要引入新的組件
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 可以用於 頁面自動化 , 網絡監測 , 網頁截屏 ,以及 無界面測試 等。
具體方法可自行查詢。
10.中文支持(中文空白解決)
首先需要添加中文字庫,也就是你的頁面中用到的所有字體:
ITextFontResolver fontResolver = renderer.getFontResolver(); fontResolver.addFont("C:/Windows/Fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); fontResolver.addFont("C:/Windows/Fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); fontResolver.addFont("C:/Windows/Fonts/simkai.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
注意:頁面中字體不能使用中文,需要使用英文名稱,而且是大小寫敏感的!例如宋體的英文名稱是 SimSun(注意不是simsun!,首字母都是大寫的)
錯誤寫法:font-family:宋體 或者 font-family:simsun
正確寫法:font-family:SimSun 或者 font-family:SimHei
如果生成的pdf中文不顯示或者亂碼,請確認如下信息:
-
確保頁面中所有內容都指定了字體,最好能指定 body {font-family:....},以防止漏網之魚。
-
確保上述所有字體均通過addFont加入,字體名稱錯誤或者字體不存在會拋出異常,很方便,但是沒導入的字體不會有任何提示。
-
確保字體名稱正確,不使用中文,大小寫正確。
-
確保html標簽都正確,簡單的方法是所有內容都去掉,隨便寫幾個中文看看能否正常生成,如果可以,在認真檢查html標簽,否則再次檢查上述幾條。
還有就是中文換行的問題了,帶有中文而且文字較多存在換行情況時,需要給table加入樣式:
table-layout:fixed,然后表格中的td使用%還指定td的寬度。
https://www.cnblogs.com/reese-blogs/p/5546806.html