freemarker 导出word(表格,多列表,多图片)(原创)
一、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> <!-- 图片结束 -->