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

写在前面

这篇是继我的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判断对象是否为空

阅读我的其他文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • FreeMarker的模板文件并不比HTML页面复杂多少,FreeMarker模板文件主要由如下4个部分组成: 1...
    年轻小伙程序员阅读 2,759评论 0 5
  • 特别说明 当前博客平台账号已废弃,如果有使用细节问题请前往我新博客平台进行讨论交流。 个人博客平台 HuRuWo的...
    五谷观精分道长阅读 11,855评论 3 55
  • 10.26,周六。气温十度,有风,细雨!上完下午的地图编制课,回到宿舍开始下片。网速几十k。有点着急,正好舍友回来...
    麦苗青青阅读 238评论 0 1
  • 上传图片 //后台播放音频设置 //让app支持接受远程控制事件 //设置app支持接受远程控制事件,其实就是在d...
    且行且珍惜_iOS阅读 827评论 0 6
  • 自从我换了个小电脑,每天晚上都会坐下来认真地更博。我喜欢我的小电脑,喜欢新浪清丽的界面,更喜欢我平凡琐碎拉拉杂杂的...
    朴之阅读 366评论 0 0