Web

我的Web开发实战总结(二)

Posted by wblearn on February 26, 2017

写在前面

这篇是继我的Web开发实战总结(一)的第二篇文章,在此篇里,我主要总结一下如何把Web页面上的报表或列表数据转换成pdf文件下载到本地。其中涉及到的知识我也会提出来供大家交流学习。ok,开始吧~

先来看看效果


Web页面上的列表数据

上图就是Web页面上的列表数据,将其右侧生成pdf之后的效果如下:


生成的pdf文件

实现思路

这里我提出两种实现思路:

1.利用Jacob将EXCEL转成PDF
2.利用iText将HTML 转为 PDF

1.利用Jacob将EXCEL转成PDF
一开始我用的这种思路,主要是因为有生成EXCEL的功能了,想着只要利用jacob再讲EXCEL转成PDF即可,但是后来放弃了。虽然jacob可以生成pdf,word,excel等,但经过本人的实操,问题多多,还要放dll文件到bin目录下。

首先,Dispatch.call(sheet,"Activate");指定活动sheet,这个是没有问题,设置哪一个就打哪一个,但是也只是当前的一个,其他的没有显示,对于有多个SHEET的EXCEL 怎么能这一次全部转到一个PDF上?实现是可以实现:遍历sheet保存多个pdf文件,通过itextpdf再将这多个PDF合成一个,不过效率偏低。

其次,jacob是对EXCEL中的每个单元格操作的,像上面的PDF中有图片读取很不方便,就算能打出图片也可能会很模糊,而且复杂的EXCEL更是无能为力。所以我建议大家使用第二种利用iText将HTML 转为 PDF,我也是用的第二种思路实现的。

2.利用iText将HTML 转为 PDF
这个思路就是我此篇要重点要讲的,将html转成PDF,首先html有图片,还有各种数据,那么怎么将图片和各种数据填充到html里面呢?这个问题非常好,有童鞋会说,将他们追加拼接到html里,我只想说:大兄弟,别呀,这样太蠢了。这里我们可以利用 freemarker,首先创建一个FreeMarker模板文件(*.ftl),在这个文件中加入FreeMarker表达式,这些表达式就好比jsp中的jstl标签一样,我们在程序中将数据传递给此文件中即可,在客户端显示时会被真实的数据替换。下面开始实现过程了。

利用iText将HTML 转为 PDF

1.准备好生成pdf所需的jar包

  • CORE 包:主要是itext相关的一些核心itext.jar
  • XML 包:xmlworker是一个基于iText的xml生成pdf工具

  • freemarker包:将模板转换成html的jar包(此jar包也能将模板转换成excel,word等)

这里我将它们打包免费分享出来,下载地址:itext生成pdf所需的jar包

2.创建ftl模板文件


创建一个FreeMarker模板文件(.ftl),在这个文件中加入FreeMarker表达式,这些表达式就好比jsp中的jstl标签一样,我们在程序中将数据传递给此文件中即可,在客户端显示时会被真实的数据替换。说白了,ftl模板文件就是在html里加入了FreeMarker表达式,所以里面的内容基本跟html一样,我们可以先创建html文件,修改完成后再将文件后缀改成.ftl即可。*本文.ftl模板如下:

arDraftBillPreview.ftl

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <#--ftl模板-->
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <#--一定要特别注意字体,很多字体部分中文不支持-->
  <body screen_capture_injected="true" ryt11773="1">
     <div id="header">
        <div>![](${imgPath})</div>
        <div id="text" sytle="font-size:30px;width:445px;height:45px;margin:0px auto;border:0px solid #ccc;"><p>STATEMENT OF ACCOUNT(Draft)</p></div>
    </div>

    <div>
        <table style="width: 90%;margin:0px auto;font-size:11px;">
        <tr>
            <td><font style="font-weight:bold">Customer:</font><span style="font-family:宋体">${data.name}</span></td>
            <td><font style="font-weight:bold">SOA#:</font>${data.number}</td>
            <td style="width: 2%;"></td>
            <td><font style="font-weight:bold">Currency:</font>${data.bill}</td> 
        </tr>
        <tr>
            <td><font style="font-weight:bold">Issud by:</font>UNI-TOP ALRLINES CO.,LTD</td>
            <td><font style="font-weight:bold">Period:</font>${data.stDate} ~ ${data.edDate}</td>
        </tr>
        </table>
    </div>
    <div>
     <table border="0" style="font-size:10px;text-align:center;border-collapse:collapse;">
        <tr style="background-color:#FDCDCB">
               <td style="border:1px solid #ccc;">NO</td>
               <td style="border:1px solid #ccc;">Flight Date</td>
               <td style="border:1px solid #ccc;">Flight No</td>
               <td style="border:1px solid #ccc;">Prefi x No</td>
               <td style="border:1px solid #ccc;">AWB NO</td>
               <td style="border:1px solid #ccc;">Origin</td>
               <td style="border:1px solid #ccc;">VIA</td>
               <td style="border:1px solid #ccc;">Dest</td>
               <td style="border:1px solid #ccc;">Gross Weight(KG)</td>
               <td style="border:1px solid #ccc;">Chargeable Weight(KG)</td>
               <td style="border:1px solid #ccc;">Unit Price</td>
               <td style="border:1px solid #ccc;">Air Freight Charge</td>
               <td style="border:1px solid #ccc;">Transfer Charge</td>
               <td style="border:1px solid #ccc;">Other Charge</td>
               <td style="border:1px solid #ccc;">Total</td>
               <td style="border:1px solid #ccc;">Other Charge Remark</td>
        </tr>
        <tr style="background-color:#FDCDCB">
            <td style="font-family:宋体;border:1px solid #ccc;">序号</td>
            <td style="font-family:宋体;border:1px solid #ccc;">航班日期</td>
            <td style="font-family:宋体;border:1px solid #ccc;">航班号</td>
            <td style="font-family:宋体;border:1px solid #ccc;">货单前缀</td>
            <td style="font-family:宋体;border:1px solid #ccc;">货单号</td>
            <td style="font-family:宋体;border:1px solid #ccc;">货源地</td>
            <td style="font-family:宋体;border:1px solid #ccc;">经停站</td>
            <td style="font-family:宋体;border:1px solid #ccc;">目的地</td>
            <td style="font-family:宋体;border:1px solid #ccc;">始发地(重)</td>
            <td style="font-family:宋体;border:1px solid #ccc;">结算重量</td>
            <td style="font-family:宋体;border:1px solid #ccc;">单价</td>
            <td style="font-family:宋体;border:1px solid #ccc;">空运费</td>
            <td style="font-family:宋体;border:1px solid #ccc;">国外转运费</td>
            <td style="font-family:宋体;border:1px solid #ccc;">其他杂费</td>
            <td style="font-family:宋体;border:1px solid #ccc;">共计</td>
            <td style="font-family:宋体;border:1px solid #ccc;">备注</td>
        </tr>

    <#list dataList as bill>  
          <tr>  
                    <td style="border:1px solid #ccc;">${bill.no}</td>  
                    <td style="border:1px solid #ccc;">${bill.flightDate}</td>  
                    <td style="border:1px solid #ccc;">${bill.flightNo}</td>  
                    <td style="border:1px solid #ccc;">${bill.prefixNo}</td>  
                    <td style="border:1px solid #ccc;">${bill.aWBNO}</td> 
                    <td style="border:1px solid #ccc;">${bill.origin}</td> 
                    <td style="border:1px solid #ccc;"></td> 
                    <td style="border:1px solid #ccc;">${bill.dest}</td> 
                    <td style="border:1px solid #ccc;">${bill.grossWeight}</td> 
                    <td style="border:1px solid #ccc;">${bill.chargeableWeight}</td> 
                    <td style="border:1px solid #ccc;">${bill.unit}</td> 
                    <td style="border:1px solid #ccc;">${bill.airFreightCharge}</td>  
                    <td style="border:1px solid #ccc;">${bill.transferCharge}</td>  
                    <td style="border:1px solid #ccc;">${bill.otherCharge}</td>  
                    <td style="border:1px solid #ccc;">${bill.total}</td>  
                    <td style="font-family:宋体;border:1px solid #ccc;">${bill.remark!}</td>  
           </tr>
    </#list>
    <#if total??>  
           <tr>  
                    <td style="border:1px solid #ccc;"></td>  
                    <td style="border:1px solid #ccc;"></td>  
                    <td style="border:1px solid #ccc;"></td>  
                    <td style="border:1px solid #ccc;"></td>  
                    <td style="border:1px solid #ccc;"></td> 
                    <td style="border:1px solid #ccc;"></td> 
                    <td style="border:1px solid #ccc;"></td> 
                    <td style="font-family:宋体;color:red;border: 1px solid #ccc;">总计:</td> 
                    <td style="color:red;border:1px solid #ccc;">${total.grossWeight}</td> 
                    <td style="color:red;border:1px solid #ccc;">${total.chargeableWeight}</td> 
                    <td style="color:red;border:1px solid #ccc;"></td> 
                    <td style="color:red;border:1px solid #ccc;">${total.airFreightCharge}</td>  
                    <td style="color:red;border:1px solid #ccc;">${total.transferCharge}</td>  
                    <td style="color:red;border:1px solid #ccc;">${total.otherCharge}</td>  
                    <td style="color:red;border:1px solid #ccc;">${total.total}</td>  
                    <td style="color:red;border:1px solid #ccc;"></td>  
            </tr>
    </#if>  
        </table>
        </div>
  </body>
</html>

以上代码在myeclipse中预览的效果如下:


注意:如果使用不存在的freemarker指令,FreeMarker不会使用模板输出,而是产生一个错误消息。其次,在写ftl模板的时候,因为xmlworker支持的CSS样式极少,所以模板内容要尽量简单。对于DOCTYPE和html标签的约束页比较严格。对于一个标签中含有中文、数字或英文的时候,很可能会出现问题。这是因为xmlworker在渲染PDF的时候是以html的标签为单位的。我发现有些字体下部分中文生成pdf不会显示。另外,对于freemarker模板语言不熟悉的童鞋,我会在文末贴出一些参考资料。

3.向ftl模板文件中填充数据,同时将其生成html
在业务处理层,将数据传递个ftl ,同时解析ftl模板生成html

//将需要在客户端浏览器中显示的业务数据放在一个map中,传递给FreeMarker   
                Map<String, Object> map = new HashMap<String, Object>();  
                map.put("imgPath", imgPath);
                map.put("data",data);
                map.put("dataList", dataList);  
                map.put("total", total);  
                TemplateParseUtil.parse(templatePath, "arDraftBillPreview.ftl", htmlPath, map);

解析ftl模板生成html(此方法与生成Excel,xml等通用),这里我写了一个工具类
TemplateParseUtil.java

public class TemplateParseUtil {
    /**
     * 解析模板生成html(此方法与生成Excel,xml等通用)
     * @param templateDir  ftl模板目录
     * @param templateName ftl模板名称
     * @param htmlPath 生成的html文件路径
     * @param data 数据参数
     * @throws IOException
     * @throws TemplateException
     */
    public static void parse(String templateDir,String templateName,String htmlPath,Map<String,Object> data) throws IOException, TemplateException {
        //初始化工作
        Configuration cfg = new Configuration();
        //设置默认编码格式为UTF-8
        cfg.setDefaultEncoding("UTF-8"); 
        //全局数字格式
        cfg.setNumberFormat("0.00");
        //设置模板文件位置
        cfg.setDirectoryForTemplateLoading(new File(templateDir));
        cfg.setObjectWrapper(new DefaultObjectWrapper());
        //加载模板
        Template template = cfg.getTemplate(templateName,"utf-8");
        OutputStreamWriter writer = null;
        try{
            //填充数据至html
            writer = new OutputStreamWriter(new FileOutputStream(htmlPath),"UTF-8");
            template.process(data, writer);
            writer.flush();
        }finally{
            writer.close();
        }    
    }
}

4.利用itext将生成的html渲染生成PDF

步骤基本如下:

 // 1.新建document对象
        Document document = new Document();
// 2.建立一个书写器(Writer)与document对象关联,通过书写器(Writer)可以将文档写入到磁盘中。
// 创建 PdfWriter 对象 第一个参数是对文档对象的引用,第二个参数是文件的实际名称,在该名称中还会给出其输出路径。
        PdfWriter writer = PdfWriter.getInstance(document, new       FileOutputStream("D:/test.pdf"));
// 3.打开文档
        document.open();
 // 4.添加一个内容段落
      document.add(new Paragraph("Hello World!"));
 // 5.关闭文档
     document.close();

本文中的利用itext生成PDF的代码如下:

                Document document = new Document(PageSize.A4, 10, 50, 10, 50);
                // step 2
                PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(servletPath+DEST));
                // step 3
                document.open();
                // step 4
                XMLWorkerHelper.getInstance().parseXHtml(writer, document,
                        new FileInputStream(htmlPath), Charset.forName("UTF-8"));
                // step 5
                document.close();

5.完整处理逻辑
可能写到这里代码有点分散,这里将上面3、4步骤的代码完整逻辑贴出来,让大家看的清晰明白点:

/**
     * 
     * 草稿账单pdf导出
     */
    @RequestMapping("/exportPDFDraftPreview")
    public @ResponseBody ControllerResult<?> exportPDFDraftPreview(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ExecuteController e = new ExecuteControllerHandle() {
            @Override
            public ControllerResult<?> dowith(HttpServletRequest request,HttpServletResponse response, Object... params) throws Exception {
                /**
                 * 具体导出步骤:先查询出具体的表头信息,然后再查询出具体的列表数据。最后整合放在具体的类里
                 */
                // 请求参数绑定
                DraftBillResponse info = ControllerUtils.bindParams(request,DraftBillResponse.class);//返回一个草稿账单号
                info.setMoneytype(info.get币种());
                DraftBill topInfo = new DraftBill();
                topInfo.set币种(info.get币种());
                topInfo.set草稿账单编号(info.get草稿账单编号());
                // 业务层调用
                IFinancialManagementMgr mgr = FinancialManagementFactory.createIFinancialManagementMgrMgrImpl();

                List<DraftBill> bill =  mgr.queryManuscriptPreview(topInfo);//查询表头

                List<DraftBillResponse> list = mgr.queryDraftPreview(info); //查询列表数据


                String servletPath = request.getSession().getServletContext().getRealPath("/");//请求的服务器
                String templatePath = servletPath+"template";//模板目录
                String htmlPath = servletPath+"tempFile\\"+info.get草稿账单编号()+".html";//生成的html地址
                String imgPath = servletPath+"images\\exlTop.png";//模板中的图片
                String DEST = "tempFile\\"+info.get草稿账单编号()+".pdf";//将要生成的pdf


                /*DraftBill data = new DraftBill();
                data = bill.get(0);*/
                //进行英文转换,防止无法识别

                List<BillList>  dataList = new ArrayList<BillList>();
                BillList listData = null;
                BillList total = new BillList();
                BillExport data = new BillExport(bill.get(0).get代理人(),bill.get(0).get草稿账单编号(),bill.get(0).get币种(),bill.get(0).get账单周期起(),bill.get(0).get账单周期止());

                double num1 = 0,num2 = 0,num3 = 0,num4 = 0,num5 = 0,num6 = 0;  //地重,始发重量,空运费,转运费,杂费,共计


                for(DraftBillResponse res : list){//集合

                  double 地重 =     Double.valueOf(res.get始发地重量());  //设置统计数据
                  double 始发重 =     Double.valueOf(res.get结算重量());
                  double 空运费 =     Double.valueOf(res.get运费());
                  double 转运费 =     Double.valueOf(res.get国外联运费());
                  double 杂费 =     Double.valueOf(res.get杂费());
                  double 统计 =     Double.valueOf(res.get运费())+ Double.valueOf(res.get国外联运费())+Double.valueOf(res.get杂费());
                  num1 = num1 + 地重;
                  num2 = num2 + 始发重;
                  num3 = num3 + 空运费;
                  num4 = num4 + 转运费;
                  num5 = num5 + 杂费;
                  num6 = num6 + 统计;
                    listData = new BillList(res.get序号(), res.get航班日期(), res.get航班号(), res.get货单前缀(),  //设置列表数据
                            res.get货单号(), res.get货源地(), "", res.get目的站(), res.get始发地重量(),
                            res.get结算重量(), res.get空运费费率(), res.get运费(), 
                              res.get国外联运费(),res.get杂费(), String.valueOf(CommonUtil.toDecimalFormat(统计)),
                            res.get备注());

                    dataList.add(listData);
                }

                //将统计的数据放进实体
                total.setGrossWeight(String.valueOf(CommonUtil.toDecimalFormat(num1)));
                total.setChargeableWeight(String.valueOf(CommonUtil.toDecimalFormat(num2)));
                total.setAirFreightCharge(String.valueOf(CommonUtil.toDecimalFormat(num3)));
                total.setTransferCharge(String.valueOf(CommonUtil.toDecimalFormat(num4)));
                total.setOtherCharge(String.valueOf(CommonUtil.toDecimalFormat(num5)));
                total.setTotal(String.valueOf(CommonUtil.toDecimalFormat(num6)));

              //将需要在客户端浏览器中显示的业务数据放在一个map中,传递给FreeMarker   
                Map<String, Object> map = new HashMap<String, Object>();  
                map.put("imgPath", imgPath);
                map.put("data",data);
                map.put("dataList", dataList);  
                map.put("total", total);  
                TemplateParseUtil.parse(templatePath, "arDraftBillPreview.ftl", htmlPath, map);

                Document document = new Document(PageSize.A4, 10, 50, 10, 50);
                // step 2
                PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(servletPath+DEST));
                // step 3
                document.open();
                // step 4
                XMLWorkerHelper.getInstance().parseXHtml(writer, document,
                        new FileInputStream(htmlPath), Charset.forName("UTF-8"));
                // step 5
                document.close();

                return ControllerUtils.buildSimpleResult(true, DEST);
            }
        };

在上面的程序中,包括PDF上的图片,表头及表身数据都传给ftl模板中了,在生成PDF之前,都会先生成一个.html的文件到tempFile的文件夹下,如下:


写在最后

其实整个过程都比较简单,难就难在一开始你不知道用那种方式去实现,这种时候我建议你都试试,毕竟一个东西你试过之后才知道好不好,适不适合。还有一点就是,对于你不知道的东西,网上一般都有很多参考资料,一定要善于利用搜索引擎学习。关于学习,就三点:坚持,坚持,坚持。

下面列出一些相关链接供大家参考:
iText入门
动态jsp页面转PDF输出到页面
最简单 iText 的 PDF 生成方案(含中文解决方案)HTML 转为 PDF
ftl 入门
Freemarker 最简单的例子程序
FreeMarker 例子
freemarker生成excel、word、html、xml实例教程
freemarker判断对象是否为空

阅读我的其他文章