CSDN博客频道搬家功能改版正式上线! “移动开发者大会参会感悟”有奖征文 CSDN博客频道推出TAG功能
iText和flying saucer结合生成pdf的技术
目录(?)[+]
- 使用flyingsaucer将网页转换为pdf之中文问题彻底解决
- 利用 iText 实现 PDF 报表下载 (没有用flying saucer)
下面是我自己利用flying saucer技术生成pdf文档的实现代码:
Servlet方式:
html代码:
- <%@ page language="java" import="java.util.*" pageEncoding="ISO-8859-1"%>
- <%
- String path = request.getContextPath();
- String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
- %>
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
- <html>
- <head>
- <base href="<%=basePath%>">
- <title>Html2PdfServlet</title>
- <meta http-equiv="pragma" content="no-cache">
- <meta http-equiv="cache-control" content="no-cache">
- <meta http-equiv="expires" content="0">
- <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
- <meta http-equiv="description" content="This is my page">
- <!--
- <link rel="stylesheet" type="text/css" href="styles.css">
- -->
- </head>
- <body>
- <form action="http://localhost:8081/createpdf/servlet/html2PdfServlet" method="get" >
- <table>
- <tr>
- <td>
- <input type="text" id="username" name="username" value="" />
- </td>
- <td>
- <input type="submit" id="submit" name="submit" value="submit" />
- </td>
- </tr>
- </table>
- </form>
- </body>
- </html>
java代码:
- package com.test;
- import java.io.IOException;
- import java.io.OutputStream;
- import javax.servlet.ServletContext;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import org.xhtmlrenderer.pdf.ITextFontResolver;
- import org.xhtmlrenderer.pdf.ITextRenderer;
- import com.lowagie.text.pdf.BaseFont;
- public class Html2PdfServlet extends HttpServlet {
- private static final long serialVersionUID = 1L;
- public void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- //pageContext.getServletContext().getRealPath("/")
- ServletContext sc = request.getSession().getServletContext();
- String path = sc.getRealPath(""); //值为D:\apache-tomcat-6.0.26\webapps\createpdf
- System.out.println("原path: " + path);
- //把路径中的反斜杠转成正斜杠
- path = path.replaceAll("\\\\", "/"); //值为D:/apache-tomcat-6.0.26/webapps/createpdf
- System.out.println(path);
- String path2 = sc.getRealPath("/");
- System.out.println("path2: " + path2);
- System.out.println(Thread.currentThread().getContextClassLoader().getResource(""));
- System.out.println("request.getRequestURI: " + request.getRequestURI());
- //获取使用的端口号
- System.out.println(request.getLocalPort());
- String path3 = request.getContextPath();
- String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path3+"/";
- System.out.println("basepath: " + basePath);
- response.setContentType("application/pdf");
- //response.setHeader("Content-Disposition", "attachment; filename=WebReport.pdf");
- response.setHeader("Content-Disposition", "inline; filename=WebReport.pdf");
- StringBuffer html = new StringBuffer();
- //组装成符合W3C标准的html文件,否则不能正确解析
- html.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
- html.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">")
- .append("<head>")
- .append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />")
- .append("<style type=\"text/css\" mce_bogus=\"1\">body {font-family: SimSun;}</style>")
- .append("<style type=\"text/css\">img {width: 700px;}</style>")
- .append("</head>")
- .append("<body>");
- html.append("<center><h1>统计报表</h1></center>");
- html.append("<center>");
- html.append("<img src=\"images/chart.jpg\"/>");
- html.append("</center>");
- html.append("</body></html>");
- // parse our markup into an xml Document
- try {
- ITextRenderer renderer = new ITextRenderer();
- /**
- * 引入了新的jar包,不用再导入字体了
- ITextFontResolver fontResolver = renderer.getFontResolver();
- fontResolver.addFont("C:/Windows/fonts/simsun.ttc",
- BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
- */
- renderer.setDocumentFromString(html.toString());
- // 解决图片的相对路径问题
- //renderer.getSharedContext().setBaseURL("file:/C:/Documents and Settings/dashan.yin/workspace/createpdf/WebRoot/images");
- //renderer.getSharedContext().setBaseURL("file:/D:/apache-tomcat-6.0.26/webapps/createpdf/images");
- renderer.getSharedContext().setBaseURL("file:/" + path + "/images");
- renderer.layout();
- OutputStream os = response.getOutputStream();
- renderer.createPDF(os);
- os.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- doGet(request, response);
- }
- }
Struts1形式:
html代码:
- <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
- <%@ page import="Config.application.*" %>
- <%@ include file="/pages/common/global.jsp" %>
- <%@ taglib uri="/WEB-INF/tlds/birt.tld" prefix="birt"%>
- <script type="text/javascript">
- function interactivity() {
- var url = contextName + "/viewAction.do?method=viewLinkResult";
- var startDate_s = $("#startDate_s").val();
- var endDate_s = $("#endDate_s").val();
- var serviceInstance = $("#serviceInstance").val();
- var serviceInstance = encodeURIComponent(serviceInstance);
- var status = $("#status").val();
- if(startDate_s){
- url += "&startDate_s=" + startDate_s;
- }
- if(endDate_s){
- url += "&endDate_s=" + endDate_s;
- }
- if(serviceInstance){
- url += "&serviceInstance=" + serviceInstance;
- }
- if(status){
- url += "&status=" + status;
- }
- url += "&random=" + Math.random();
- //alert(url);
- retrieveURL(url,'viewResult');
- //tipsWindown("详细信息","url:get?" + url,"800","700","true","","true","text");
- }
- function viewreturn() {
- var url = contextName + "/viewAction.do?method=viewReturnResult";
- //时间参数设空
- document.getElementById("startDate_s").value = "";
- document.getElementById("endDate_s").value = "";
- url += "&random=" + Math.random();
- retrieveURL(url,'viewResult');
- //tipsWindown("详细信息","url:get?" + url,"800","700","true","","true","text");
- }
- </script>
- <html:form action="/viewAction.do?method=viewExportPDF">
- <table class="form_t" style="width:100%">
- <tr>
- <td>
- <input type="submit" id="submit" name="submit" value="导出" class="button4C"/>
- </td>
- </tr>
- </table>
- </html:form>
- <html:form action="/viewAction.do?method=viewResult" >
- <table class="form_t" style="width:100%">
- <tr>
- <th class="tablelogo" colspan="3">
- 统计时间
- </th>
- </tr>
- <tr>
- <td style="width: 175px; text-align: right;">
- 开始时间
- </td>
- <td>
- <input type="text" name="startDate_s" id="startDate_s"
- class="Wdate"
- onclick="WdatePicker({dateFmt:'yyyy-MM-dd HH:mm:ss',maxDate:'#F{$dp.$D(\'endDate_s\')||\'%y-%M-%d %H:{%m-1}:%s\'}'})" />
- </td>
- <td>
- <input type="hidden" name="serviceInstance" id="serviceInstance" value=""/>
- <input type="hidden" name="status" id="status" value=""/>
- </td>
- </tr>
- <tr>
- <td style="width: 175px; text-align: right;">
- 结束时间
- </td>
- <td>
- <input type="text" name="endDate_s" id="endDate_s" class="Wdate"
- onclick="WdatePicker({dateFmt:'yyyy-MM-dd HH:mm:ss',minDate:'#F{$dp.$D(\'startDate_s\')}',maxDate:'%y-%M-%d %H:%m:%s'})" />
- </td>
- <td>
- </td>
- </tr>
- <tr>
- <td style="padding-left: 175px;" colspan="2">
- <input type="button" value="查看" class="button4C"
- onclick="javascript:viewForm(this.form,'viewResult');" />
- <input type="button" value="返回" class="button4C"
- onclick="javascript:viewreturn();" />
- </td>
- <td>
- </td>
- </tr>
- </table>
- </html:form>
- <table width="100%">
- <tr>
- <td id="viewResult" width="100%"></td>
- </tr>
- </table>
java代码:
- public void viewExportPDF(ActionMapping mapping, ActionForm form,
- HttpServletRequest request, HttpServletResponse response)
- throws Exception {
- ServletContext sc = request.getSession().getServletContext();
- String rootpath = sc.getRealPath(""); //值为D:\apache-tomcat-6.0.26\webapps\webmonitor
- //把路径中的反斜杠转成正斜杠
- rootpath = rootpath.replaceAll("\\\\", "/"); //值为D:/apache-tomcat-6.0.26/webapps/webmonitor
- //临时存储目录
- String pdfPathName = rootpath + "/WebReport.pdf";
- ArrayList<String> list = new ArrayList<String>();
- for(int i=0;i<outstreamlist.size();i++) {
- list.add(outstreamlist.get(i));
- }
- //调用方法
- boolean flag = createPdf(list, pdfPathName, request, response);
- if (flag == true) {
- //要实现另存为下载,必须满足两个条件:导入commons-upload.jar包,表单提交
- try {
- OutputStream out = response.getOutputStream();
- byte by[] = new byte[1024];
- File fileLoad = new File(pdfPathName);
- response.reset();
- response.setContentType("application/pdf");
- response.setHeader("Content-Disposition",
- "attachment; filename=WebReport.pdf");
- long fileLength = fileLoad.length();
- String length1 = String.valueOf(fileLength);
- response.setHeader("Content_Length", length1);
- FileInputStream in = new FileInputStream(fileLoad);
- int n;
- while ((n = in.read(by)) != -1) {
- out.write(by, 0, n);
- }
- in.close();
- out.flush();
- } catch(Exception e) {
- e.printStackTrace();
- }
- } else {
- renderText(response, ERR_MESSAGE);
- }
- return ;
- }
- //生成pdf
- public boolean createPdf(ArrayList<String> list, String pdfPathName,
- HttpServletRequest request, HttpServletResponse response)
- throws Exception {
- /**
- * 用rootpath指定目录也可以生成pdf文件,只不过不能在myeclipse的左边导航窗口中看不到而已
- * 左边导航窗口对应C盘目录下的workspace目录下程序
- * 用rootpath指定的目录是D盘Tomcat目录
- */
- ServletContext sc = request.getSession().getServletContext();
- String rootpath = sc.getRealPath(""); //值为D:\apache-tomcat-6.0.26\webapps\webmonitor
- //把路径中的反斜杠转成正斜杠
- rootpath = rootpath.replaceAll("\\\\", "/"); //值为D:/apache-tomcat-6.0.26/webapps/webmonitor
- boolean flag = false;
- String outputFile = pdfPathName;
- //指定目录导出文件
- OutputStream os = new FileOutputStream(outputFile);
- ITextRenderer renderer = new ITextRenderer();
- StringBuffer html = new StringBuffer();
- //组装成符合W3C标准的html文件,否则不能正确解析
- html.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
- html.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">")
- .append("<head>")
- .append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />")
- .append("<style type=\"text/css\" mce_bogus=\"1\">body {font-family: SimSun;}</style>")
- .append("<style type=\"text/css\">img {width: 500px;}</style>")
- .append("<style type=\"text/css\">table {font-size:13px;}</style>")
- .append("</head>")
- .append("<body>");
- html.append("<center>");
- html.append("<h1>统计报表</h1>");
- for(int i=0;i<list.size();i++) {
- html.append("<div>" + list.get(i) + "</div>");
- }
- html.append("</center>");
- html.append("</body></html>");
- try {
- renderer.setDocumentFromString(html.toString());
- // 解决图片的相对路径问题,图片路径必须以file开头
- renderer.getSharedContext().setBaseURL("file:/" + rootpath);
- renderer.layout();
- renderer.createPDF(os);
- os.close();
- flag = true;
- } catch (Exception e) {
- flag = false;
- e.printStackTrace();
- }
- return flag;
- }
在Java世界,要想生成PDF,方案不少。最近一直在和这个东西打交道,所以简单做一个小结吧。
在此之前,先来勾画一下我心中比较理想的一个解决方案。在企业应用中,碰到的比较多的PDF的需求,可能是针对某个比较典型的具备文档特性的内容,导出成为PDF进行存档。由于我们现在往往使用一些开源框架,诸如ssh来构建我们的应用,所以我们相对熟悉的方案是针对具体的业务逻辑设计实体,使用开源框架来实现我们的业务逻辑。而PDF的导出,最好不要破坏现有的程序框架,甚至能复用我们业务逻辑层的代码。因为如果把PDF作为一种特殊的表现形式的话,实际上它有点类似模板。最佳的情况,是我们能够通过编写某种模板,把PDF的大概样子确定下来,然后把数据和模板做一次整合,得到最后的结果
带着这个目标,开始在网上搜索解决方案。也找到了一些方案,下面简单小结一下:
Jasper Report
看到的市面上采用的最多的方案,是Jasper Report。相关的文档也很多,不过很杂,需要完全掌握,我认为还是有些坡度和时间的。这个时间和坡度我认为主要来自于对iReport这个IDE的反复尝试,对里面的每个属性的摸索。
Jasper Report的设计思路,本身是不违反我上面所说的初衷的。因为我们的努力方向是先生成模板,然后得到数据,最后将两者整合得到结果。但是Jasper Report的问题在于,其生成模板的方式过于复杂,即使有IDE的帮助,我们还是需要对其中的众多规则有所了解才行,否则就会给调试带来极大的麻烦。
所以,我认为Jasper Report是一个半调子方案,这种强依赖于IDE进行可视化编辑的方式令我很不爽。同时,由此带来的诸多的限制,相信也让很多使用者颇为头疼。在经历了一番痛苦的挣扎后,决定放弃使用这种方案。
iText
其实Jasper Report是基于iText的。于是有的人会说,那么直接使用iText不是一种倒退么?的确,直接使用iText似乎就需要直接使用原生的API进行编程了。不过幸好iText其实提供了一些方便的API,通过使用这些API,我们可以直接将HTML代码转化成iText可识别的Document对象,从而导出PDF文档。
java代码:
- import java.io.FileOutputStream;
- import java.io.FileReader;
- import java.util.ArrayList;
- import com.lowagie.text.Document;
- import com.lowagie.text.Element;
- import com.lowagie.text.html.simpleparser.HTMLWorker;
- import com.lowagie.text.html.simpleparser.StyleSheet;
- import com.lowagie.text.pdf.PdfWriter;
- public class MainClass {
- public static void main(String[] args) throws Exception {
- Document document = new Document();
- StyleSheet st = new StyleSheet();
- st.loadTagStyle("body", "leading", "16,0");
- PdfWriter.getInstance(document, new FileOutputStream("html2.pdf"));
- document.open();
- ArrayList p = HTMLWorker.parseToList(new FileReader("example.html"), st);
- for (int k = 0; k < p.size(); ++k)
- document.add((Element) p.get(k));
- document.close();
- }
- }
这是从网上找到的一个例子。从代码中,我们可以看到,iText本身提供了一个简单的HTML的解析器,它可以把HTML转化成我们需要的PDF的document。
有了这个东西,基本上我的目标就能达成一大半了。接下来我的任务就是根据实际情况去编写HTML代码,然后扔进这个方法,就OK了。而真正的HTML代码,我们则可以在这里使用真正的模板技术,Freemarker或者Velocity去生成我们所需要的内容。当然,这已经是我们熟门熟路的东西了。
正当我觉得这个方案基本能符合我的要求的时候,我也同样找到了它的很多弱项:
1. 无法识别很多HTML的tag和attribute(应该是iText的HTMLParser不够强大)
2. 无法识别CSS
如果说第一点我还可以勉强接受的话,那么第二点我就完全不能接受了。无法识别简单的CSS,就意味着HTML失去了最基本的活力,也无法根据实际要求调整样式。
所以这种方案也必然无法成为我的方案。
flying sauser
在这种情况下,我几乎已经燃起了自己编写一个支持CSS解析的HTML Parser的想法。幸好,在一个非常偶然的情况下,我在google中搜到了这样一个开源项目,它能够满足我的一切需求。这就是flying sauser,项目主页是:https://xhtmlrenderer.dev.java.net/
项目的首页非常吸引人:An XML/XHTML/CSS 2.1 Renderer。这不正是我要的东西么?
仔细再看里面的文档:
完美了。这东西能解析HTML和CSS,而且能输出成image,PDF等格式。哇!我们来看看sample代码(代码丑陋,不过已经能说明问题了):
java代码:
- /*
- * ITextRendererTest.java *
- * Copyright 2009 Shanghai TuDou.
- * All rights reserved.
- */
- package itext;
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.OutputStream;
- import org.xhtmlrenderer.pdf.ITextFontResolver;
- import org.xhtmlrenderer.pdf.ITextRenderer;
- import com.lowagie.text.pdf.BaseFont;
- /**
- * TODO class description *
- *
- * @author pcwang
- *
- * @version 1.0, 上午11:03:26 create $Id$
- */
- public class ITextRendererTest {
- public static void main(String[] args) throws Exception {
- String inputFile = "conf/template/test.html";
- String url = new File(inputFile).toURI().toURL().toString();
- String outputFile = "firstdoc.pdf";
- OutputStream os = new FileOutputStream(outputFile);
- ITextRenderer renderer = new ITextRenderer();
- renderer.setDocument(url);
- // 解决中文支持问题
- ITextFontResolver fontResolver = renderer.getFontResolver();
- fontResolver.addFont("C:/Windows/Fonts/arialuni.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
- // 解决图片的相对路径问题
- renderer.getSharedContext().setBaseURL("file:/D:/Work/Demo2do/Yoda/branch/Yoda%20-%20All/conf/template/");
- renderer.layout();
- renderer.createPDF(os);
- os.close();
- }
- }
运行,成功!实在太简单了!API帮你完成了一切!
有了这个东西,我们就可以将PDF的生成流程变成这样:
1) 编写Freemarker或者Velocity模板,打造HTML,勾画PDF的样式(请任意使用CSS)
2) 在你的业务逻辑层引入Freemarker的引擎或者Velocity的引擎,并将业务逻辑层中可以获取的数据和模板,使用引擎生成最终的内容
3) 将我上面的sample代码做简单封装后,调用,生成PDF
这样,我想作为一个web程序员来说,上面的3点,都不会成为你的绊脚石。你可以轻松驾驭PDF了。
在Flying Saucer的官方文档中,有一些Q&A,可以解决读者们大部分的问题。包括PDF的字体、PDF的格式、Image如何处理等等。大家可以尝试着去阅读。
还有一篇文章,好像是作者写的,非常不错:http://today.java.net/pub/a/today/2007/06/26/generating-pdfs-with-flying-saucer-and-itext.html
Freemarker+Flying sauser +Itext 整合生成PDF
Freemarker、Flying sauser 、Itext ,这三个框架的作用就不详细介绍了,google一下就知道了。
Itext提供了很多底层的API,让我们可以用java代码画一个pdf出来,但是很不灵活,布局渲染代码都hard code 进java类里面了。
当需求发生改变时,哪怕只需要更改一个属性名,我们都要重新修改那段代码,很不符合开放关闭的原则。想到用模版来做渲染,但自己实现起来比较繁琐,然后google了下,找到了用freemarker做模版,Flying sauser 照着模版做渲染,让Itext做输出生成PDF的方案。
freemarker和itext都比较熟悉了,Flying sauser 第一次听说,看完官方的user guide(http://flyingsaucerproject.github.com/flyingsaucer/r8/guide/users-guide-R8.html)后,自己着手做了个demo实践:
测试数据模型:
java代码:
- package com.jeemiss.pdfsimple.entity;
- public class User {
- private String name;
- private int age;
- private int sex;
- /**
- * Constructor with all fields
- *
- * @param name
- * @param age
- * @param sex
- */
- public User(String name, int age, int sex) {
- super();
- this.name = name;
- this.age = age;
- this.sex = sex;
- }
- /////////////////////// getter and setter ///////////////////////////////////////////
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- public int getSex() {
- return sex;
- }
- public void setSex(int sex) {
- this.sex = sex;
- }
- }
java代码:
- package com.jeemiss.pdfsimple.freemarker;
- import freemarker.template.Configuration;
- public class FreemarkerConfiguration {
- private static Configuration config = null;
- /**
- * Static initialization.
- *
- * Initialize the configuration of Freemarker.
- */
- static{
- config = new Configuration();
- config.setClassForTemplateLoading(FreemarkerConfiguration.class, "template");
- }
- public static Configuration getConfiguation(){
- return config;
- }
- }
html生成器:
java代码:
- package com.jeemiss.pdfsimple.generator;
- import java.io.BufferedWriter;
- import java.io.StringWriter;
- import java.util.Map;
- import com.jeemiss.pdfsimple.freemarker.FreemarkerConfiguration;
- import freemarker.template.Configuration;
- import freemarker.template.Template;
- public class HtmlGenerator {
- /**
- * 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;
- }
- }
pdf生成器:
java代码:
- package com.jeemiss.pdfsimple.generator;
- import java.io.ByteArrayInputStream;
- import java.io.OutputStream;
- import javax.xml.parsers.DocumentBuilder;
- import javax.xml.parsers.DocumentBuilderFactory;
- import org.w3c.dom.Document;
- import org.xhtmlrenderer.pdf.ITextRenderer;
- public class PdfGenerator {
- /**
- * Output a pdf to the specified outputstream
- *
- * @param htmlStr the htmlstr
- * @param out the specified outputstream
- * @throws Exception
- */
- public static void generate(String htmlStr, OutputStream out)
- throws Exception {
- DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
- Document doc = builder.parse(new ByteArrayInputStream(htmlStr.getBytes()));
- ITextRenderer renderer = new ITextRenderer();
- renderer.setDocument(doc, null);
- renderer.layout();
- renderer.createPDF(out);
- out.close();
- }
- }
用来做测试的ftl模版,用到部分css3.0的属性来控制pdf强制分页和输出分页信息
html代码:
- <html>
- <head>
- <title>${title}</title>
- <style>
- table {
- width:100%;border:green dotted ;border-width:2 0 0 2
- }
- td {
- border:green dotted;border-width:0 2 2 0
- }
- @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>
- </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>
- </body>
- </html>
最后写个测试用例看看:
java代码:
- package com.jeemiss.pdfsimple.test;
- import java.io.FileOutputStream;
- import java.io.OutputStream;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import org.junit.Test;
- import com.jeemiss.pdfsimple.entity.User;
- import com.jeemiss.pdfsimple.generator.HtmlGenerator;
- import com.jeemiss.pdfsimple.generator.PdfGenerator;
- public class TestCase
- {
- @Test
- public void generatePDF() {
- try{
- String outputFile = "C:\\sample.pdf";
- Map<String,Object> variables = new HashMap<String,Object>();
- 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", "User List");
- variables.put("userList", userList);
- String htmlStr = HtmlGenerator.generate("sample.ftl", variables);
- OutputStream out = new FileOutputStream(outputFile);
- PdfGenerator.generate(htmlStr, out);
- }catch(Exception ex){
- ex.printStackTrace();
- }
- }
- }
到C盘下打开sample.pdf ,看看输出的结果:
flying saucer 使用中的一些问题 (java导出pdf)
flying saucer(源代码托管在githubhttps://github.com/flyingsaucerproject/flyingsaucer)是java导出pdf的一种解决方案,最早是从downpour老大的文章里看到它:http://www.iteye.com/topic/509417,感觉比之前的iText好用许多,它可以解析css,即我将页面先设置好,然后传递给它,它既可以给我生成一个pdf出来,跟页面一样,当时感觉很酷,于是就研究了一下,现在项目中也用到了,效果还不错。
优点很明显,之前也提到了,可以解析css,这样很方便,大大的减少了工作量。pdf加水印也变得很简单——只需为body设置一个background-image即可。
说说使用中需要注意的一些问题吧:
[list=1]
老外做的东西,没有考虑到中文问题。默认提供的包里,中文不会换行,有人修改了源代码,解决了这个问题,重新编译好的包在附件里,可以下载。需要注意的是,在官网提供的jar包里,有两个包,一个是core-renderer.jar,另一个是core-renderer-minimal.jar。引用时,只需引用前者就行。有人曾经说用这个重新编译后的包替换了原来的包之后,不起作用,原因就在此。
中文换行包下载地址:http://community.csdn.net/detail/shanliangliuxing
另外,想要中文换行,如果是table,那么table 的style必须加上这句话
html代码:
- style="table-layout:fixed; word-break:break-strict;"
css路径问题
在一个java project里,使用相对css路径是可以的,效果也都不错。但在java web project里,使用css相对路径是不可以的(最起码这里困扰了我很久,差点就放弃flying saucer了)。例如,我有一个模板叫addOne.jsp,里面引用到了某个css,就应该这样写(windows)
js代码:
- <link href="file:///D|/project/WebContent/commons/css/module-pdf.css" rel="stylesheet" type="text/css" />
只有这样写了之后,它才能找到这个css,很诡异。每次换了机器之后都要改路径,很麻烦。
downpour老大在它那篇文章里提到了怎样处理中文字体的,他可能高估了许多人的水平。其实说起来,很简单,就两点:一是在java代码里引用字体,二是在页面上引用字体。
引用字体:
java代码:
- // 解决中文支持问题
- ITextFontResolver fontResolver = renderer.getFontResolver();
- fontResolver.addFont("C:/Windows/Fonts/arialuni.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
这里引用了arialuni.ttf字体,它位于C盘windows/fonts文件夹下,将它引用后,还需要在页面上使用这个字体
html代码:
- <body style="font-family:'Arial Unicode MS'">
这里的Arial Unicode MS 即刚才 的arialuni.ttf字体的名字,换了其它字体后要注意修改这里的名称。这样才可以在pdf中显示中文。
许多人有这样一个问题——按照以上两个步骤做了之后,页面中还是没有中文,这时,请检查你引用的css文件,其中一定设置了其它字体,只需将它去掉即可
缺点:
我在使用中发现,flying saucer不支持富文本,如果用到了KindEditor此类富文本编辑器,
还要将其中的内容转化成pdf,那对flying saucer来说就是个灾难。会报一堆错误,目前我还没有找到解决方案。还好这次项目中不是必须使用富文本编辑器,对于有此类需求的同学来说,请慎重选择flying saucer。另外,flying saucer严格遵守html规则,一个小小的错误,都会导致它报错。诸如
html代码:
- <td colspan="2""2">
此类的html代码在jsp中是不会有问题的,可是flying saucer却会报错,曾经这个问题导致我花了一小时时间来寻找问题所在。不过很难说这到底是缺点还是优点
最后贴一个较完整的例子:
我使用spring mvc,在controller里
java代码:
- @RequestMapping("/pdf/{projectId}")
- public ModelAndView generatePdf(HttpServletRequest request,
- HttpServletResponse response, @PathVariable
- String projectId) {
- Project project = this.projectService.getProjectById(projectId);
- ModelAndView mav = new ModelAndView();
- if (project == null) {
- mav.setViewName("forward:/error/page-not-found");
- return mav;
- }
- //中文需转义
- String pdfName = "pdfName";
- response.setHeader("Content-disposition", "attachment;filename="+pdfName;
- response.setContentType("application/pdf");
- OutputStream os = response.getOutputStream();
- ITextRenderer renderer = new ITextRenderer();
- //指定模板地址
- renderer.setDocument("http://localhost/project/preview/"+projectId);
- ITextFontResolver fontResolver = renderer.getFontResolver();
- if (StringUtils.isOSWindow())
- fontResolver.addFont("C:/Windows/Fonts/ARIALUNI.TTF",
- BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
- else
- fontResolver.addFont("/usr/share/fonts/TTF/ARIALUNI.TTF",
- BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
- renderer.layout();
- renderer.createPDF(os);
- os.close();
- return null;
- }
- @RequestMapping("/preview/{projectId}")
- public ModelAndView pdf(@PathVariable
- String projectId) {
- Project project = this.projectService.getProjectById(projectId);
- ModelAndView mav = new ModelAndView();
- if (project == null) {
- mav.setViewName("forward:/error/page-not-found");
- return mav;
- }
- mav.setViewName("pdf");
- mav.addObject("project",project);
- return mav;
- }
jsp页面如下
html代码:
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <title>title</title>
- <link href="file:///D|/project/WebContent/commons/css/print-pdf.css" rel="stylesheet" type="text/css" />
- </head>
- <body style="font-family:'Arial Unicode MS'">
- <table border="1" cellspacing="0" cellpadding="0" class="table" style="table-layout:fixed; word-break:break-strict;">
- <tr>
- <td rowspan="9" width="4%" class="tc">项目单位基本信息</td>
- <td colspan="2" style="width:160px">(1)项目单位名称 </td>
- <td colspan="2"><%=StringUtils.getValueString(user.getDeptName()) %></td>
- </tr>
- </table>
- </body>
使用flyingsaucer将网页转换为pdf之中文问题彻底解决
前几天遇到个导出pdf的需求,在网络上查找了一下java导出pdf的方案.多数人推荐使用iText,研究了一下,感觉直接写pdf的方法太笨,可维护性差,一旦pdf格式要变化改起来很费劲.还有一个方案,可以先预先定义一个pdf作为模板文件,然后用业务数据进行填空.是个不错的方案,只可惜不适合我的需求.需求中有些行是动态加行的,这个方案无法实现.后来发现有可以将网页直接转成pdf的开源包flyingsaucer(中文名:灰碟),逐将注意力转移到这上面,发现是个不错的选择.只要写网页就可以了,而且pdf格式变化维护起来也方便,代码也会比较干净.只是它对中文支持的不好,但这不是无法解决的.下面就来说说这个flyingsaucer.
java代码:
- cb.setFontAndSize(_font.getFontDescription().getFont(), _font.getSize2D() / _dotsPerPoint);
改成:
java代码:
- 01.try {
- 02. cb.setFontAndSize(BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED), _font.getSize2D()/_dotsPerPoint);
- 03. } catch (Exception e) {
- 04. }
Ok,改了以后我们终于可以看到pdf里有中文了.但是别高兴的太早哦,问题并没有完全解决.如果一段标签中有且只有中文字符的时候,导出pdf后内容便会消失.比如<div>中文</div>,这样的代码将什么也输出不了,而<div>中文a</div>则会将标签内容全部输出.通过测试我们发现,纯中文是无法输出的,但是加上一点点英文、数字或符号就可以输出了.有同学可能要说我们把纯中文后面加上空格不就行了?我只能说很不幸,加空格是不管用的.如果你的页面上纯中文的地方可以随便让你加字母/数字/符号,那可以不必往下看了.但是我觉得大多数的人恐怕不会这么干的,即使我们想客户也不让啊.那就要解决这个问题.
java代码:
- 01.ITextFontResolver fontResolver = renderer.getFontResolver();
- 02.fontResolver.addFont("C:/WINDOWS/Fonts/ARIALUNI.TTF", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
于是我照做了.杯具的是情况没有任何好转.即使有好转这个绝对路径的字体引入方式也很让人不舒服吧.所以这个不是我们想要的解决方案.
java代码:
- 01.if (char1 < 128 || (char1 >= 160 && char1 <= 255))
- 02. total += widths[char1];
- 03.else
- 04. total += widths[PdfEncodings.winansi.get(char1)];
这几行改为:
java代码:
- ·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001.if (char1 < 128 || (char1 >= 160 && char1 <= 255))
- 02. total += widths[char1];
- 03.else if ((char1 >= 19968) && (char1 <= 40869)) // 如果是中文字符加宽度500
- 04. total += 500;
- 05.else
- 06. total += widths[PdfEncodings.winansi.get(char1)];
再次测试,通过.至此,使用flyingsaucer将网页导出成pdf的中文问题总算解决了.可是总觉得这个解决的方法有点不太正宗,因为修改了父类嘛.但又没有找到其他正宗的解决方案,只能先这样解决一下了.发出此文,只当抛砖引玉,如果有哪位高人有更好的解决方案请不吝赐教啊.
附件提供修改了的flyingsaucer-R8的两个jar包: core-renderer.jar和iText2.0.8.jar另有一个iText亚洲语言包.
下载修改源码后的jar包地址:http://download.csdn.net/detail/shanliangliuxing/3640286
利用 iText 实现 PDF 报表下载 (没有用flying saucer)
很久没更新 blog 了,工作和一些事情占用了大部分精力,实在是身不由己。今天终于有空整理一下最近用到的东西。
有个朋友的项目需要用到 PDF 报表下载,之前我只做过 Excel 的,相信再做一次 PDF 的下载一定很有趣吧。在网上找了一大圈,似乎 iText 比较符合我的要求,而且这个工具很早很早以前就有了,生命力很旺盛。进入 iText 的主页(http://www.lowagie.com/iText/),发现作者很勤劳,最近2个月都有新版本发布。哪知道现在高兴得太早了,一堆问题接踵而至。
下载倒是很简单,一个iText-2.1.4.jar搞定,然后去找入门文档,进了文档页面,一股浓郁的商业气氛迎面而来,这里只提供了部分文档,除非去买"iText in Action",随后被踢到 iText by Example 页面。好吧,既然这是老牌工具了,肯定有不少中文资料吧,找了一圈,没发现什么和生成并下载相关的 out of box 文档,很多都是经验性的总结和进阶文章。无奈又啃 iText by Example,在这里找到些有用的资源,iText in a Web Application正是我要找的,不过这个例子很简单。通过 Google 之后,又发现要下载一个 CJK 的包(iTextAsian.jar)才能正确显示中文,好吧我去找。很幸运的是在 iText by Example 里找到了这个 jar 的 link,兴致勃勃的跑去下载,结果这是个无效链接,最后在 sourceForge 上才找到,不容易啊。解决了这些问题,想必能够安稳的使用了吧,由于这个项目比较急,没什么耐心一个个的翻阅 iText by Example,想找点捷径,据说 iText 可以从 html 直接生成 PDF,窃喜!找了 apache common 的 httpclient,动态模拟 http 请求来抓 html,根据控制台的 print,的确把 html 抓到了,然后开始转换到 PDF,先解决了中文显示问题,可是后面的问题解决不了了,html 的 table 和 div 这些,转换到 PDF 都走样了... ...
很不爽,看来还是只有老老实实的啃 iText by Example实在点。这次稍微耐心点,一点点的看,首先搞清楚了它的 Font 设置,然后是 Table 和 Cell 的关系,经过反复调试,有点效果了。把代码贴出来,做个标记吧。以免以后又抓狂。
java代码:
- 1 package org.rosenjiang.servlet;
- 2
- 3 import java.awt.Color;
- 4 import java.io.IOException;
- 5 import java.util.HashMap;
- 6 import java.util.List;
- 7 import java.util.Map;
- 8
- 9 import javax.servlet.ServletException;
- 10 import javax.servlet.http.HttpServlet;
- 11 import javax.servlet.http.HttpServletRequest;
- 12 import javax.servlet.http.HttpServletResponse;
- 13
- 14 import org.springframework.web.context.WebApplicationContext;
- 15 import org.springframework.web.context.support.WebApplicationContextUtils;
- 16
- 17 import org.rosenjiang.service.UserService;
- 18 import com.lowagie.text.Document;
- 19 import com.lowagie.text.DocumentException;
- 20 import com.lowagie.text.Font;
- 21 import com.lowagie.text.Paragraph;
- 22 import com.lowagie.text.pdf.BaseFont;
- 23 import com.lowagie.text.pdf.PdfPCell;
- 24 import com.lowagie.text.pdf.PdfPTable;
- 25 import com.lowagie.text.pdf.PdfWriter;
- 26
- 27 /*
- 28 * ReportServlet
- 29 * @author rosen jiang
- 30 * @since 2008-12
- 31 */
- 32 public class ReportServlet extends HttpServlet {
- 33
- 34 /**
- 35 * Return a PDF document for download.
- 36 *
- 37 */
- 38 public void doGet (HttpServletRequest request, HttpServletResponse response)
- 39 throws IOException, ServletException {
- 40 String account_id = request.getParameter("account_id");
- 41 String search_date_from = request.getParameter("search_date_from");
- 42 String to = request.getParameter("to");
- 43 WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
- 44 UserService userService = (UserService)ctx.getBean("userService");
- 45 List<Map<String, Object>> list = userService.getAccountActivity(account_id, search_date_from, to);
- 46 // create PDF document
- 47 Document document = new Document();
- 48 try {
- 49 //set response info
- 50 response.setContentType("application/x-msdownload;charset=UTF-8");
- 51 response.setHeader("Content-Disposition","attachment;filename=report.pdf");
- 52 //open output stream
- 53 PdfWriter.getInstance(document, response.getOutputStream());
- 54 // open PDF document
- 55 document.open();
- 56 // set chinese font
- 57 BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
- 58 Font f2 = new Font(bfChinese, 2, Font.NORMAL);
- 59 Font f6 = new Font(bfChinese, 6, Font.NORMAL);
- 60 Font f8 = new Font(bfChinese, 8, Font.NORMAL);
- 61 Font f10 = new Font(bfChinese, 10, Font.NORMAL);
- 62 Font f12 = new Font(bfChinese, 12, Font.BOLD);
- 63 //set title
- 64 document.add(new Paragraph("金融报表", f12));
- 65 //<br>
- 66 document.add(new Paragraph(" ",f6));
- 67 //set sub title
- 68 document.add(new Paragraph("账户信息", f10));
- 69 //<br>
- 70 document.add(new Paragraph(" ", f2));
- 71 //process business data
- 72 if(list.size()>0 && list.get(0).get("bankbook_no")!=null){
- 73 float openBalance = 0;
- 74 //create table with 7 columns
- 75 PdfPTable table = new PdfPTable(7);
- 76 //100% width
- 77 table.setWidthPercentage(100);
- 78 table.setHorizontalAlignment(PdfPTable.ALIGN_LEFT);
- 79 //create cells
- 80 PdfPCell cell = new PdfPCell();
- 81 //set color
- 82 cell.setBackgroundColor(new Color(213, 141, 69));
- 83 cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
- 84 //
- 85 cell.setPhrase(new Paragraph("交易日", f8));
- 86 table.addCell(cell);
- 87 cell.setPhrase(new Paragraph("类型", f8));
- 88 table.addCell(cell);
- 89 cell.setPhrase(new Paragraph("备注", f8));
- 90 table.addCell(cell);
- 91 cell.setPhrase(new Paragraph("ID", f8));
- 92 table.addCell(cell);
- 93 cell.setPhrase(new Paragraph("票号", f8));
- 94 table.addCell(cell);
- 95 cell.setPhrase(new Paragraph("合计", f8));
- 96 table.addCell(cell);
- 97 cell.setPhrase(new Paragraph("余额", f8));
- 98 table.addCell(cell);
- 99 //create another cell
- 100 PdfPCell newcell = new PdfPCell();
- 101 newcell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
- 102
- 103 Map<String, Object> map = new HashMap<String, Object>();
- 104 for(int i = 0; i < list.size(); i++){
- 105 map = list.get(i);
- 106 String cashInout = map.get("cash_inout").toString();
- 107 newcell.setPhrase(new Paragraph(map.get("trade_date").toString(), f8));
- 108 table.addCell(newcell);
- 109 newcell.setPhrase(new Paragraph(map.get("bankbook_type").toString(), f8));
- 110 table.addCell(newcell);
- 111 newcell.setPhrase(new Paragraph(map.get("memo").toString(), f8));
- 112 table.addCell(newcell);
- 113 newcell.setPhrase(new Paragraph(map.get("account_id").toString(), f8));
- 114 table.addCell(newcell);
- 115 newcell.setPhrase(new Paragraph(map.get("ticket_no").toString(), f8));
- 116 table.addCell(newcell);
- 117 newcell.setPhrase(new Paragraph(map.get("amount").toString(), f8));
- 118 table.addCell(newcell);
- 119 newcell.setPhrase(new Paragraph(openBalance+"", f8));
- 120 table.addCell(newcell);
- 121 if(cashInout.equals("I")){
- 122 openBalance = openBalance + Float.valueOf(map.get("amount").toString());
- 123 }else if(cashInout.equals("O")){
- 124 openBalance = openBalance - Float.valueOf(map.get("amount").toString());
- 125 }
- 126 }
- 127 //print total column
- 128 newcell.setPhrase(new Paragraph("合计"+openBalance, f8));
- 129 table.addCell("");
- 130 table.addCell("");
- 131 table.addCell("");
- 132 table.addCell("");
- 133 table.addCell("");
- 134 table.addCell("");
- 135 table.addCell(newcell);
- 136 document.add(table);
- 137 }else{
- 138 PdfPTable table = new PdfPTable(1);
- 139 table.setWidthPercentage(100);
- 140 table.setHorizontalAlignment(PdfPTable.ALIGN_LEFT);
- 141 PdfPCell cell = new PdfPCell(new Paragraph("暂无数据"));
- 142 cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
- 143 table.addCell(cell);
- 144 document.add(table);
- 145 }
- 146 }
- 147 catch(DocumentException de) {
- 148 de.printStackTrace();
- 149 System.err.println("document: " + de.getMessage());
- 150 }finally{
- 151 // close the document and the outputstream is also closed internally
- 152 document.close();
- 153 }
- 154 }
- 155 }
执行结果:
代码结构清晰,本来也没什么东西,就是通过 Spring 调用 service 方法,获取数据后按照 iText 结构输出即可。不过代码里面有个很愚蠢的动作:document.add(new Paragraph(" ",f6)),主要是找不到如何输出空白行,所以只好出此下策。如果哪位有解法,请告知一下。
做技术的确不能太着急,慢慢来,总会找到出口的。
一定要学会看官方文档,因为有些技术网上的资料太少,简单的还能满足需求,复杂一点的就得需要自己慢慢去研究官方文档了
官方文档一般都是英文的,所以,英文水平还要过的去才行,说不作要求,最起码要能读懂。
flying saucer的官方文档:
http://code.google.com/p/flying-saucer/
官方文档中关于生成pdf的demo:
http://today.java.net/pub/a/today/2007/06/26/generating-pdfs-with-flying-saucer-and-itext.html
相关例子源文件下载:
https://svn.atlassian.com/svn/public/atlassian/vendor/xhtmlrenderer-8.0/tags/8.3-atlassian/demos/samples/src/