java中利用freemarker生成样式比较复杂的word

这两天接到一个需求,要在系统中生成word版的需求规格说明书,领导给了个之前的样本给我,要求挺高,必须和给的样本基本一样。

基本样式主要有多级标题、动态图片、页眉页脚等,如下(内容部分因为隐私就不贴出来了):

当时的第一想法是用POI做,花了两个小时时间,果断放弃,POI 功能实现起来也挺简单,但是让我头疼的是样式,比如说行距、缩进、页眉页脚等。因为之前做过freemarker生成PDF、html之类的,所以选择freemarker。

主要步骤如下:

一、把word模板另存为xml格式

     

我这边是另存为 word2003 XML格式的,网上的说法是如果另存为wordXML的话,会存在2003的兼容性,这个我没去验证,但是我也试了下,如果另存为wordXML格式的话,两种生成模板的方式会略有不同,这里只介绍word2003XML这种方式,

二、生成模板后用notepade++之类的工具打开可以发现里面主要的数据结构都在里面,我这里有部分数据是固定的,所以只需要动态生成部分数据,如下:

    

这种多级标题需要生成的结构是:

这种数据结构应该都能看懂吧,一个递归搞定,图片问题的稍候再说,

另外再说一句,如果刚开始是另存为wordXML格式的模板,那么数据结构和这种完全不一样,比这个简单的多,但是在生成图片的时候会多操作一点,这个有兴趣的自己去尝试下吧(个人已经试过,比这种简单,但是因为在网上查看说有兼容性问题,所以比较担心,就没用)。

三、后台生成数据,这块不想详细说,说到底就是通过递归或者多层循环的方式做成需要的数据结构就行,简单的上点代码吧

      递归的:      

public static JSONArray treeRecursionDataList(List<Map<String,Object>> treeList, String parentId) {
    JSONArray childMenu = new JSONArray();
    for (Object object : treeList) {
        JSONObject jsonMenu = JSONObject.parseObject(JSON.toJSONString(object));
        String menuId = jsonMenu.getString("id");
        String pid = jsonMenu.getString("pid");
        if (parentId.equals(pid)) {
            JSONArray c_node = treeRecursionDataList(treeList, menuId);
            jsonMenu.put("children", c_node);
            childMenu.add(jsonMenu);
        }
    }
    return childMenu;
}

两层循环的:

public static List<Map<String,Object>> treeForDataList(List<Map<String,Object>> treeNodes) {
    List<Map<String,Object>> trees = new ArrayList<Map<String,Object>>();
    for (Map<String,Object> treeNode : treeNodes) {
        if ("-1".equals(treeNode.get("pid"))) {
            trees.add(treeNode);
        }
        List<Map<String,Object>> childList = new ArrayList<Map<String,Object>>();
        for (Map<String,Object> it : treeNodes) {
            if (it.get("pid").toString().equals(treeNode.get("id").toString())) {
                childList.add(it);
                treeNode.put("children",childList);
            }
        }
    }
    return trees;
}

对于这两种方式: 个人建议如果只是简单的做数据格式,用两层循环就行,特别是你的数据或者层级比较多的情况下,用递归会使用栈内存,比较慢的,我的数据大概有3千条吧,层级稍多,用递归将近70000ms,但是两层循环1000ms左右。

我这里直接map做数据的,如果用实体也行,主要的几个字段无非是 id、pid、level、text、content、children、imgcode(用于展示图片),

最后生成的数据格式类似(比较懒,随便网上找了张图片):

四、后台数据做好了后,下面要做的就是前台渲染,那么如何做成前面所说的数据格式呢,我是用freemarker的宏递归

        <#macro tree data>
                <#list data as child>
                    <wx:sub-section>
                    <#if child?? && child.children?? && (child.children?size gt 0)>

                             <w:p wsp:rsidR="006A2A1B" wsp:rsidRDefault="006A2A1B" wsp:rsidP="006A2A1B">

                              。。。。。。。。。。。。。。

                              </w:p>
                        </#if>
                        <@tree data=child.children />
                    <#else>
                        <w:p wsp:rsidR="006A2A1B" wsp:rsidRDefault="006A2A1B" wsp:rsidP="006A2A1B">

                         </w:p>
                        </#if>
                    </#if>
                    </wx:sub-section>
                </#list>
            </#macro>
            <@tree data=reqList/>

              主要结构就是这样,一张图片说明下

五、这样话只要一个生成doc的方法就可行了,直接上代码

/**
 * 以下载的方式生成word,自定义路径
 * @param dataMap
 * @param out
 */
public void createDoc(Map<String, Object> dataMap,Writer out,String templateFile) {

    // 设置模本装置方法和路径,FreeMarker支持多种模板装载方法。可以重servlet,classpath,数据库装载,
    // ftl文件存放路径
    try {
        configure.setDirectoryForTemplateLoading(new File("d:/"));
        Template t = null;
        t = configure.getTemplate(templateFile);
        t.setEncoding("utf-8");
        t.process(dataMap, out);
    } catch (IOException e) {
        e.printStackTrace();
        logger.error("读取文件出错!");
    } catch (TemplateException e) {
        e.printStackTrace();
        logger.error("生成文件出错!");
    }finally{
        if (out != null){
            try {
                out.close();
            } catch (IOException e) {
                logger.error("流关闭出错!");
            }
        }

    }

}

dataMap就是传到模板上的数据,我的是dataMap.put("reqList","做好的数据集合");templateFile是你的模板名称:如xxx.xml或者改成XXX.ftl也可以,上面这个方法是流下载的方式,如果生成到本地磁盘的话自己改改就行了,

主体的思路是这样的,这样的话应该可以生成word的了,部分代码就不上了,网上找找都有的

说下我遇到的问题吧:

     图片生成系需要在具体的位置增加这段代码就行,

    <w:p wsp:rsidR="006A2A1B" wsp:rsidRDefault="009D00C1" wsp:rsidP="006A2A1B">
                            <w:pPr>
                                <w:rPr>
                                    <wx:font wx:val="Arial"/>
                                </w:rPr>
                            </w:pPr>
                            <w:r>
                                <w:rPr>
                                    <wx:font wx:val="Arial"/>
                                    <w:noProof/>
                                    <w:lang w:bidi="AR-SA"/>
                                </w:rPr>
                                <w:pict>
                                    <w:binData w:name="wordml://${child.fid}.jpg" xml:space="preserve">${child.base64code}</w:binData>
                                    <v:shape id="图片 4" o:spid="_x0000_i1026" type="#_x0000_t75" style="width:414.45pt;height:258.55pt;visibility:visible;mso-wrap-style:square">
                                        <v:imagedata src="wordml://${child.fid}.jpg" o:title=""/>
                                    </v:shape>
                                </w:pict>
                            </w:r>
                            </w:p>

这个${child.base64code}  就是图片的base64编码,其中一下几点注意:

     1和2出一定需要保持一致并且不能写死,之前我是写死的,生成文档是发现图片都是同一个图片,可以用文件的id替换,style中的width和height树形表示图片的高度和宽度,这块目前我没有做很好的处理,只是固定了一个大小,这样可能会出现部分过小或过大的图片产生变形目前我测试了几张图片都是按照1.33的比例压缩的,即获取到图片的宽高,除以1.33即可,没想到比较好的方案,但是已经足够满足我的需求。

另外附上获取图片base64码的方法:

  public  Map<String,Object> getImageStr(String imgFile) {
//        InputStream imgin = null;
//        BufferedImage img = null;
        ByteArrayOutputStream data = null;
        String imgWidth = "";
        String imgHeight = "";
        String base64code = "";
        try {
            //获取此路径的连接

            data = new ByteArrayOutputStream();
            URL url = new URL(imgFile);
            byte[] by = new byte[1024];
            // 创建链接       
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5 * 1000);
            InputStream is = conn.getInputStream();
            // 将内容读取内存中       
            int len = -1;
            while ((len = is.read(by)) != -1)
            {
                data.write(by, 0, len);
            }
            BASE64Encoder encoder = new BASE64Encoder();
            base64code = encoder.encode(data.toByteArray());
//            imgin = url.openStream();
//            img = ImageIO.read(imgin);
//            imgWidth = String.valueOf(img.getWidth());
//            imgHeight = String.valueOf(img.getHeight());
        } catch (FileNotFoundException e) {
            logger.error("未发现该文件!");
        } catch (IOException e) {
            logger.error("读取文件出错!");
        }finally {
            try {
                if (data != null){
                    data.close();
                }
            } catch (IOException e) {
                logger.error("流关闭出错!");
            }
        }
        Map<String,Object> result = new HashMap<>();
        result.put("base64code",base64code);
//        result.put("imgWidth",imgWidth);
//        result.put("imgHeight",imgHeight);
        return result;
    }

2018.12.11.使用xml模板方式生成的word有一个问题就是手机端查看时,全是xml格式的代码,那是因为模板本身就是XML格式文件,freemarker使用的方式是用类型字符串替换的方式,替换掉XML里面的字符然后生成按相同格式生成文件,然后后缀名定为.doc而已。 
由于XML文件的头部有<?mso-application progid="Word.Document"?>这样的字符串,所以电脑上的office word读到这个信息后知道按转换xml里标签转换成word的格式。 
但手机上的word软件则没有这个功能,所以就打开失败。

解决方法如下:

https://blog.csdn.net/FORLOVEHUAN/article/details/81452169

写的比较乱,但是整体思路就是这些,如果遇到问题可以留言讨论,或者各位有更好的解决方法的话,也请指正

猜你喜欢

转载自blog.csdn.net/jimmy609/article/details/84991526