flying-saucer + iText + Freemarker實現pdf的導出, 支持中文、css以及圖片
前言
項目中有個需求,需要將合同內容導出成pdf。上網查閱到了 iText , iText 是一個生成PDF文檔的開源Java庫,能夠動態的從XML或者數據庫生成PDF,同時還可以對文檔進行加密,權限控制,並且還支持Java/C#等,但是iText本身提供的HTML解析器還是不夠強大,許多HTML標簽和屬性無法識別,更悲催的是簡單的CSS它不認識,排版調整樣式讓人頭大。那麽有沒有什麽方式能夠支持css呢,又查閱到了 flying-saucer, flying-saucer也是導出PDF的一種解決方案,並且是基於iText的開源API,並且實現了CSS解析器,能夠很好的支持CSS2.1,以及少量的CSS。最終解決方案定為: flying-saucer + iText + Freemarker。
具體實現
流程如下
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.yzb.lee</groupId> <artifactId>itextpdf</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>itextpdf Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.20</version> </dependency> <dependency> <groupId>com.itextpdf.tool</groupId> <artifactId>xmlworker</artifactId> <version>5.5.1</version> </dependency> <!-- 支持中文 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency> <!-- 支持css樣式渲染 --> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf-itext5</artifactId> <version>9.0.3</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>itextpdf</finalName> </build> </project>
1、html內容的輸出
模版文件fileTemplate.html
<html> <head> <title>${title}</title> <!-- link鏈接應該寫文件服務器地址, 出於演示,這裏用的localhost --> <link type="text/css" rel="stylesheet" href="http://localhost:8080/itextpdf/css/pdf.css" /> <style> @page { size: 8.5in 11in; @ bottom-center { content : "page " counter( page ) " of " counter( pages ); } } </style> </head> <body> <h1>Just a blank page.</h1> <div style="page-break-before: always;"> <div align="center"> <h1>${title}</h1> <!-- src鏈接應該寫文件服務器地址, 出於演示,這裏用的localhost --> <img alt="加載中..." src="http://localhost:8080/itextpdf/images/aloner.jpg" /> </div> <table> <tr> <td><b>Name</b></td> <td><b>Age</b></td> <td><b>Sex</b></td> </tr> <#list userList as user> <tr> <td>${user.name}</td> <td>${user.age}</td> <td><#if user.sex = 1> male <#else> female </#if></td> </tr> </#list> </table> </div> <div> <a href="https://www.baidu.com/" target="_blank">百度</a> </div> </body> </html>View Code
動態數據的獲取
public Map<String, Object> getContent() throws IOException { // 從數據庫中獲取數據, 出於演示目的, 這裏數據不從數據庫獲取, 而是直接寫死 Map<String, Object> variables = new HashMap<String, Object>(3); List<User> userList = new ArrayList<User>(); User tom = new User("Tom", 19, 1); User amy = new User("Amy", 28, 0); User leo = new User("Leo", 23, 1); userList.add(tom); userList.add(amy); userList.add(leo); variables.put("title", "用戶列表"); variables.put("userList", userList); return variables; }
動態數據的綁定,html內容的輸出
/** * Generate html string. * * @param template * the name of freemarker teamlate. * @param variables * the data of teamlate. * @return htmlStr * @throws Exception */ public static String generate(String template, Map<String, Object> variables) throws Exception { Configuration config = FreemarkerConfiguration.getConfiguation(); Template tp = config.getTemplate(template); StringWriter stringWriter = new StringWriter(); BufferedWriter writer = new BufferedWriter(stringWriter); tp.setEncoding("UTF-8"); tp.process(variables, writer); String htmlStr = stringWriter.toString(); writer.flush(); writer.close(); return htmlStr; }
2、pdf的導出
private void generatePdf(String htmlStr, OutputStream out) throws IOException, DocumentException { //final ServletContext servletContext = getServletContext(); Document document = new Document(PageSize.A4, 30, 30, 30, 30); document.setMargins(30, 30, 30, 30); PdfWriter writer = PdfWriter.getInstance(document, out); document.open(); // html內容解析 HtmlPipelineContext htmlContext = new HtmlPipelineContext( new CssAppliersImpl(new XMLWorkerFontProvider() { @Override public Font getFont(String fontname, String encoding, float size, final int style) { Font font = null; if (fontname == null) { //字體 String fontCn = getChineseFont(); BaseFont bf; try { //註意這裏有一個,1 bf = BaseFont.createFont(fontCn+",1", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); font = new Font(bf, size, style); } catch (DocumentException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return font; } })) { @Override public HtmlPipelineContext clone() throws CloneNotSupportedException { HtmlPipelineContext context = super.clone(); try { ImageProvider imageProvider = this.getImageProvider(); context.setImageProvider(imageProvider); } catch (NoImageProviderException e) { } return context; } }; // 圖片解析 htmlContext.setImageProvider(new AbstractImageProvider() { // String rootPath = servletContext.getRealPath("/"); @Override public String getImageRootPath() { return ""; } @Override public Image retrieve(String src) { if (StringUtils.isEmpty(src)) { return null; } try { // String imageFilePath = new File(rootPath, src).toURI().toString(); Image image = Image.getInstance(src); image.setAbsolutePosition(400, 400); if (image != null) { store(src, image); return image; } } catch (Throwable e) { e.printStackTrace(); } return super.retrieve(src); } }); htmlContext.setAcceptUnknown(true).autoBookmark(true) .setTagFactory(Tags.getHtmlTagProcessorFactory()); // css解析 CSSResolver cssResolver = XMLWorkerHelper.getInstance() .getDefaultCssResolver(true); cssResolver.setFileRetrieve(new FileRetrieve() { @Override public void processFromStream(InputStream in, ReadingProcessor processor) throws IOException { try (InputStreamReader reader = new InputStreamReader(in, CHARSET_NAME)) { int i = -1; while (-1 != (i = reader.read())) { processor.process(i); } } catch (Throwable e) { } } // 解析href @Override public void processFromHref(String href, ReadingProcessor processor) throws IOException { // InputStream is = servletContext.getResourceAsStream(href); URL url = new URL(href); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5 * 1000); InputStream is = conn.getInputStream(); try (InputStreamReader reader = new InputStreamReader(is, CHARSET_NAME)) { int i = -1; while (-1 != (i = reader.read())) { processor.process(i); } } catch (Throwable e) { e.printStackTrace(); } } }); HtmlPipeline htmlPipeline = new HtmlPipeline(htmlContext, new PdfWriterPipeline(document, writer)); Pipeline<?> pipeline = new CssResolverPipeline(cssResolver, htmlPipeline); XMLWorker worker = null; worker = new XMLWorker(pipeline, true); XMLParser parser = new XMLParser(true, worker, Charset.forName(CHARSET_NAME)); try (InputStream inputStream = new ByteArrayInputStream( htmlStr.getBytes())) { parser.parse(inputStream, Charset.forName(CHARSET_NAME)); } document.close(); }
3、生成的pdf
1508383793597.pdf
註意點
1、博客中的代碼不是一個完整工程,只依賴博客中的代碼是運行不起來的;
2、文件路徑的獲取,本地文件與遠程文件的獲取是有區別的, 另外本地文件的獲取又存在多種方式;
3、完整工程地址:itextpdf,仔細閱讀readme.txt, 工程中存在多個版本, 而本博客對應的是版本4;
4、推薦將SIMSUN.TTC放到工程中, 這就不依賴操作系統了, 可移植性更強;
參考
獲取java項目根目錄
freemarker+Flying sauser +Itext 整合生成PDF
flying-saucer + iText + Freemarker實現pdf的導出, 支持中文、css以及圖片