首页 > 技术文章 > freemarker 导出word(表格,多列表,多图片)(原创)

w-yu-chen 2019-08-23 19:09 原文

一、jar包支持

1、freemarker

freemarker-2.3.28.jar

2、poi

 

poi-3.9.jar
poi-examples-3.9.jar
poi-excelant-3.9.jar
poi-ooxml-3.9.jar
poi-ooxml-schemas-3.9.jar
poi-scratchpad-3.9.jar

ps:如果项目里poi版本为poi-4.1.0及以上,建议使用poi-tl-1.5.0

  附带地址教程http://deepoove.com/poi-tl/

二、大概步骤

1、创建word—调整样式—调整XML代码—修改扩展名ftl,就可以使用了

我用的office版本是  office专业增强版 2016,不同版本可能有所不同。

三、创建office2007的重要步骤

wps没试过,不清楚。就是讨厌wps,没有原因。

1、表格取值

1、先用office2007新建一个word文档(一定要!!!),office是向下兼容的,2007及以下版本,无法打开freemarker导出后的2007以上版本的(意思是2007无法打开高版本)

2、新建好07.docx之后,用高版本或者当前版本打开。

  设定你需要的模板,先把内容都填充了,先调整好样式,在删掉内容后,写好参数 ,后台是以map key的形式往模板里插入的${xxx},尽量不要有个空格。

  需要插入图片的地方插入两张...最好是不同的图片,可以看出来区别(方便修改代码,因为要看懂word的XML语言,也后面多个图片循环),列表同样。见下图

 

3、表格完成之后,另保存为.xml文件,(是另存为,不是直接修改扩展名),选择word 2003 XML !!!。

 

 

 

4、然后在修改为.ftl后缀名,就可以用Notpad++打开看代码了,如果遇到  ${xxx}  这几个占位符不在一起的情况,都把他们调整放在一起,转xml的时候格式造成的

譬如这种:直接修改,红圈的样式可以直接删掉,不影响。再看看其他的,有很多占位符有问题。

 

2、图片循环

1、找到有一大串这种字符的地方(Base64字符串),word图片转换过的。直接删掉就可以,我们是要后台传值过来的。

2、然后修改这段代码,循环图片,自己缕缕,就能看出来,跟jsp列表循环差不多

如果用的2016版本,就需要循环三个地方。有指向性的问题,七标题里面会提到

 

方便使用,这里粘贴这部分代码。

 

 <!-- 代表一行几列的意思,由图片宽度决定 -->
                                <w:pict>
                                    <v:shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75" o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f">
                                        <v:stroke joinstyle="miter"/>
                                        <v:formulas>
                                            <v:f eqn="if lineDrawn pixelLineWidth 0"/>
                                            <v:f eqn="sum @0 1 0"/>
                                            <v:f eqn="sum 0 0 @1"/>
                                            <v:f eqn="prod @2 1 2"/>
                                            <v:f eqn="prod @3 21600 pixelWidth"/>
                                            <v:f eqn="prod @3 21600 pixelHeight"/>
                                            <v:f eqn="sum @0 0 1"/>
                                            <v:f eqn="prod @6 1 2"/>
                                            <v:f eqn="prod @7 21600 pixelWidth"/>
                                            <v:f eqn="sum @8 21600 0"/>
                                            <v:f eqn="prod @7 21600 pixelHeight"/>
                                            <v:f eqn="sum @10 21600 0"/>
                                        </v:formulas>
                                        <v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect"/>
                                        <o:lock v:ext="edit" aspectratio="t"/>
                                    </v:shapetype>
                                    <!-- office2007 图片循环-->
                                    <#list image as item >
                                        <w:binData w:name="wordml://${item.image_name}" xml:space="preserve">${item.image_base64}</w:binData>
                                    <v:shape id="_x0000_i1027" type="#_x0000_t75" style="width:255pt;height:255pt">
                                        <v:imagedata src="wordml://${item.image_name}" o:title="photo"/>
                                        <o:lock v:ext="edit" aspectratio="f"/>
                                    </v:shape>
                                    </#list>

                                </w:pict>

 

 

3、列表循环

 1、列表循环同理,找到大致的列表,写个list循环就可以,这里只截图我的结果了

好了,这部分完事了

 

 

四、jsp、js代码

  

<a class="layui-btn" onclick="exportWord(); return false;">导出word文档</a>

function exportWord() {
    var $form=$('<form action="'+ sys_ctx +'/EventWord/default.do">'+'<input type="hidden" name="method" value="exportWord"/></form>');
    $form.append('<input type="hidden" name="guid" value="' + guid+ '" />');
    $form.append('<input type="hidden" name="source_type_id" value="' + processInstanceId+ '" />');
    $form.appendTo($("body")).submit().remove();
}

五、java代码

1、EventWordController

 

package sdcncsi.ict.customized.event.EventWord;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import sdcncsi.ict.flow.WorkFlowService;
import sdcncsi.ict.util.RequestUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Transactional
@Controller
@RequestMapping("/EventWord/default.do")
public class EventWordController {
    @Autowired
    private WorkFlowService workFlowService;


    // 导出word
    @RequestMapping(params = "method=exportWord")
    public void exportWord(HttpServletRequest request, HttpServletResponse response) {
        try {
            EventWord eventWord = new EventWord(RequestUtil.getMap(request));
            eventWord.exportWord(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

 

2、EventWord

package sdcncsi.ict.customized.event.EventWord;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import sdcncsi.ict.customized.event.common.EventFlow;
import sdcncsi.ict.util.Base64Util;
import sdcncsi.ict.util.StringUtil;
import sdcncsi.ict.util.ZhsqBaseDao;
import sdcncsi.ict.util.cache.CacheUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class EventWord extends ZhsqBaseDao {

    public EventWord(Map mapIn) {
        super(mapIn);
    }



    private static String _PATH="/customized/event/EventDoc/2007template/";//模板路径
    private static String _PATH_NAME="event.ftl";//模板路径名称
    private static String _NAME="事件详细信息表.doc";//导出后的文件名称


    public void exportWord(HttpServletRequest request, HttpServletResponse response) throws IOException, TemplateException {

//        // 修改导出模板路径
        String exportTemplatePath = request.getSession().getServletContext().getRealPath(_PATH);
//
//        FileUtil fileUtil = new FileUtil();
//        // 上传路径
//        String uploadTempPath = CacheUtil.getParamValue("uploadTempPath");
//        // 插入数据后的文件路径
//        String dataFilePath = _TYPE + File.separator + DateUtil.getCurrentTime("yyyyMMdd") + File.separator;
//        fileUtil.Createdir(uploadTempPath + dataFilePath);
//        // 把模板拷贝到临时目录
//        fileUtil.copyFile("222.ftl", exportTemplatePath + File.separator, uploadTempPath + dataFilePath, _PATH_NAME);

//        String WordPath = uploadTempPath + dataFilePath + File.separator + downloadName;
//
        //第一步:创建一个Configuration对象,直接new一个对象。构造方法的参数就是freemarker对于的版本号。
        Configuration configuration = new Configuration();
        // 第二步:设置模板文件所在的路径。
        configuration.setDirectoryForTemplateLoading( new File(exportTemplatePath));
        //第三步:设置模板文件使用的字符集。一般就是utf-8.
        configuration.setDefaultEncoding("utf-8");
        // 第四步:加载一个模板,创建一个模板对象。
        Template template = configuration.getTemplate(_PATH_NAME);
        // 第五步:创建一个模板使用的数据集,可以是pojo也可以是map。一般是Map。
        Map dataModel = new HashMap();


        // 第六步 定义向数据集中添加数据

        //--------------从这里开始取所需要的数据 start
        EventFlow eventflow = new EventFlow(map);
        Map EventMap =eventflow.getEventDetail();//这是整个返回值
        eventflow.getAttachmentByEventGuid();//这是图片信息
        Map<String ,Object> detailMap = (Map) EventMap.get("detail");//从返回值里面取出详情数据
        List <Map<String ,Object>> photoList = (List<Map<String, Object>>) EventMap.get("fjurl");//图片信息是多个,所以用list
        map.put("processInstanceId",map.get("source_type_id"));
        List<Map<String, Object>> historyTasks = (List<Map<String, Object>>) eventflow.getHistoricTask().get("historyTasks");



        //开始循环取出多个图片 start
        List<Map<String, Object>> imageList=new ArrayList();
        String ftpIP = CacheUtil.getParamValue("ftpaddress");//缓存里取出ftp地址
        String port = CacheUtil.getParamValue("ftpserverport");//缓存里取出端口

        //下面有解释
        int j = 0;
        //需要循环word中的  Relationships标签,Id=  自己定义从rId10开始
        String relationship = "rId";
        int relationship_id = 9;//
        for (int i = 0; i <photoList.size() ; i++) {
            j++;
            relationship_id++;
            Map<String, Object> _map=new HashMap();
            Map map1 = photoList.get(i);
            String imgUrl = "http://"+ftpIP+":"+port+StringUtil.getStringValue(map1.get("remotepath"));
            //获取图片类型
            String type = StringUtil.getStringValue(map1.get("type"));
            //截取图片类型 jpeg
            type = type.substring(type.indexOf("/")+1);
            //获取图片名称,不加扩展名 word里面是image1 image2  ...依次递归
            String filename = "image"+j;
            String imageBase64 = Base64Util.ImageUrlBase64(imgUrl);

            _map.put("image_name",filename+"."+type);
            _map.put("image_base64",imageBase64);
            _map.put("image_type",StringUtil.getStringValue(map1.get("type")));
            _map.put("relationship_id",relationship+relationship_id);
            imageList.add(_map);
        }
        //开始循环取出多个图片 end


        //开始循环流程 start
        List<Map<String, Object>> list=new ArrayList();
        for (int i = 0; i < historyTasks.size(); i++) {
            Map<String, Object> _map=new HashMap();
            Map map1 = historyTasks.get(i);
            String cur_organizationname  = StringUtil.getStringValue(map1.get("cur_organizationname"));
            String cur_opername  =StringUtil.getStringValue(map1.get("cur_opername"));
            if(StringUtil.isNull(cur_organizationname)){
                cur_organizationname  = "网格员";
            }
            if(StringUtil.isNull(cur_opername)){
                cur_opername  = cur_organizationname;
            }
            //阶段
            String jied = "由【"+cur_organizationname+":"+cur_opername+"】"+StringUtil.getStringValue(map1.get("dictname"));
                    if(!"".equals(StringUtil.getStringValue(map1.get("to_organizationname")))){
                        jied+= "给【"+StringUtil.getStringValue(map1.get("to_organizationname"))+"】";
                    }

            //意见
            String yj = StringUtil.getStringValue(map1.get("content"));
            //操作时间
            String date = StringUtil.getStringValue(map1.get("createtime"));

            _map.put("jd",jied);
            _map.put("yj",yj);
            _map.put("date",date);

            list.add(_map);

        }
        //结束循环流程  end

        //--------------从这里开始取所需要的数据 end



        //开始放入数据  map就可以

        /**
         *  例如: .ftl里面取的方式
         * <#list image as item >
         *      <v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:250pt;height:250pt">
         *      <v:imagedata r:id="${item.relationship_id}" o:title="photo"/>
         *      </v:shape>
         * </#list>
         *
         *
         */
        dataModel.put("guid",detailMap.get("guid"));//事件编号
        dataModel.put("source_type",detailMap.get("source_type"));//问题来源
        dataModel.put("createdate",detailMap.get("createdate"));//发现时间
        dataModel.put("createopername",detailMap.get("createopername"));//上报人
        dataModel.put("phonenumber",detailMap.get("phonenumber"));//联系电话
        dataModel.put("type_1",detailMap.get("type_1"));//事件类型
        dataModel.put("address",detailMap.get("address"));//坐标
        dataModel.put("position",detailMap.get("position"));//发生地址
        dataModel.put("description",detailMap.get("description"));//案件描述
        dataModel.put("orgname",detailMap.get("orgname"));//所属网格
        dataModel.put("historyTasks",list);//流程
        dataModel.put("image",imageList);//多个图片


        // 设置下载文档名称
        String fileName = _NAME;
        fileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
        response.setHeader("Content-Disposition", "attachment;filename="+ fileName);
        Writer out = new BufferedWriter(new OutputStreamWriter(response.getOutputStream(),"utf-8"));

        // 第七步:调用模板对象的process方法输出文件。
        template.process(dataModel, out);
        // 第八步:关闭流。
        out.close();

    }

}

3、Base64Util

package sdcncsi.ict.util;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.imageio.stream.FileImageInputStream;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * @Description 图片字符串转换
 * @Author wangxa
 * @Date 2019-08-20 10:16
 */
public class Base64Util{
    /**
     * 字符串转图片
     * @param base64Str
     * @return
     */
    public static byte[] decode(String base64Str){
        byte[] b = null;
        BASE64Decoder decoder = new BASE64Decoder();
        try {
            b = decoder.decodeBuffer(replaceEnter(base64Str));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return b;
    }

    /**
     * 图片转字符串
     * @param image
     * @return
     */
    public static String encode(byte[] image){
        BASE64Encoder decoder = new BASE64Encoder();
        return replaceEnter(decoder.encode(image));
    }

    public static String encode(String uri){
        BASE64Encoder encoder = new BASE64Encoder();
        return replaceEnter(encoder.encode(uri.getBytes()));
    }

    /**
     *
     * @path    图片路径
     * @return
     */

    public static byte[] imageTobyte(String path){
        byte[] data = null;
        FileImageInputStream input = null;
        try {
            input = new FileImageInputStream(new File(path));
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int numBytesRead = 0;
            while((numBytesRead = input.read(buf)) != -1){
                output.write(buf, 0, numBytesRead);
            }
            data = output.toByteArray();
            output.close();
            input.close();

        } catch (Exception e) {
            e.printStackTrace();
        }

        return data;
    }



    public static String replaceEnter(String str){
        String reg ="[\n-\r]";
        Pattern p = Pattern.compile(reg);
        Matcher m = p.matcher(str);
        return m.replaceAll("");
    }

    /**
     * 远程读取image转换为Base64字符串
     * @param imgUrl =图片地址全路径
     * @return
     */
    public static String ImageUrlBase64(String imgUrl) {
        URL url = null;
        InputStream is = null;
        ByteArrayOutputStream outStream = null;
        HttpURLConnection httpUrl = null;
        try{
            url = new URL(imgUrl);
            httpUrl = (HttpURLConnection) url.openConnection();
            httpUrl.connect();
            httpUrl.getInputStream();
            is = httpUrl.getInputStream();

            outStream = new ByteArrayOutputStream();
            //创建一个Buffer字符串
            byte[] buffer = new byte[1024];
            //每次读取的字符串长度,如果为-1,代表全部读取完毕
            int len = 0;
            //使用一个输入流从buffer里把数据读取出来
            while( (len=is.read(buffer)) != -1 ){
                //用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
                outStream.write(buffer, 0, len);
            }
            // 对字节数组Base64编码
            return new BASE64Encoder().encode(outStream.toByteArray());
        }catch (Exception e) {
            e.printStackTrace();
        }
        finally{
            if(is != null)
            {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(outStream != null)
            {
                try {
                    outStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(httpUrl != null)
            {
                httpUrl.disconnect();
            }
        }
        return imgUrl;
    }

    /**
     * @Description: 获取图片对应的base64码(以文件的形式)
     * @Author: wangxa
     * @throws IOException
     * @Date: 18:25 2019/8/19
     */
    public static String getImageBase64String(File imgFile) throws IOException {
        InputStream inputStream = new FileInputStream(imgFile);
        byte[] data = new byte[inputStream.available()];
        int totalNumberBytes = inputStream.read(data);
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(data);
    }


}

好了,这就是所有的步骤了。希望需要的人少走弯路。

 

 

六、效果图展示

 

 

七、附带office2016的XML

(记录自己走过的坑o(╥﹏╥)o)  需要修改三个地方!!!图片才能展现,也就是为什么要用07版本的office创建了。

<!-- 大概是图片一行两列的意思 起 -->
                                <w:p w:rsidR="00914388" w:rsidRPr="00786262" w:rsidRDefault="00D04BAC">
                                    <w:pPr>
                                        <w:rPr>
                                            <w:rFonts w:ascii="仿宋" w:eastAsia="仿宋" w:hAnsi="仿宋"/>
                                            <w:sz w:val="28"/>
                                            <w:szCs w:val="28"/>
                                        </w:rPr>
                                    </w:pPr>
                                    <w:r>
                                        <w:rPr>
                                            <w:rFonts w:ascii="仿宋" w:eastAsia="仿宋" w:hAnsi="仿宋"/>
                                            <w:sz w:val="28"/>
                                            <w:szCs w:val="28"/>
                                        </w:rPr>
                                        <w:pict>
                                            <v:shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75" o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f">
                                                <v:stroke joinstyle="miter"/>
                                                <v:formulas>
                                                    <v:f eqn="if lineDrawn pixelLineWidth 0"/>
                                                    <v:f eqn="sum @0 1 0"/>
                                                    <v:f eqn="sum 0 0 @1"/>
                                                    <v:f eqn="prod @2 1 2"/>
                                                    <v:f eqn="prod @3 21600 pixelWidth"/>
                                                    <v:f eqn="prod @3 21600 pixelHeight"/>
                                                    <v:f eqn="sum @0 0 1"/>
                                                    <v:f eqn="prod @6 1 2"/>
                                                    <v:f eqn="prod @7 21600 pixelWidth"/>
                                                    <v:f eqn="sum @8 21600 0"/>
                                                    <v:f eqn="prod @7 21600 pixelHeight"/>
                                                    <v:f eqn="sum @10 21600 0"/>
                                                </v:formulas>
                                                <v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect"/>
                                                <o:lock v:ext="edit" aspectratio="t"/>
                                            </v:shapetype>
                                            <!-- 循环这个图片映射所在位置(自己理解的) <v:shape id="_x0000_i1025"  这个的id好像不用管。 -->
                                            <#list image as item >
                                                <v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:250pt;height:250pt">
                                                    <v:imagedata r:id="${item.relationship_id}" o:title="photo"/>
                                                </v:shape>
                                            </#list>

                                        </w:pict>
                                    </w:r>
                                </w:p>
                                <!-- 大概是图片一行两列的意思 止 -->
                            </w:tc>

---------------------------------------------------------


    <pkg:part pkg:name="/word/_rels/document.xml.rels" pkg:contentType="application/vnd.openxmlformats-package.relationships+xml" pkg:padding="256">
        <pkg:xmlData>
            <Relationships
                    xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
                <Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
                <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
                <Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
                <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
                <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
                <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" Target="endnotes.xml"/>
                <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml"/>

                <!-- 要循环这个,每张图片一个Id, 根据上面的Id,我们定义从 rId10开始作为循环图片-->
                <#list image as item>
                    <Relationship Id="${item.relationship_id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${item.image_name}"/>
                </#list>

            </Relationships>
        </pkg:xmlData>
    </pkg:part>

------------------------------------------------------------------------------------------------


    <!-- 图片开始 -->
    <#list image as item >
        <pkg:part pkg:name="/word/media/${item.image_name}" pkg:contentType="${item.image_type}" pkg:compression="store">
            <pkg:binaryData>${item.image_base64}</pkg:binaryData>
        </pkg:part>

    </#list>


    <!-- 图片结束 -->

 

推荐阅读