公司前一段时间做的一个项目中应用到了这个技术,需要后台获取数据后在前台直接下载word文档,利用freemarker模版生成的doc文档在电脑上可以正常打开,但是发送到手机上打开则全部变成“乱码”。但实际上并不是乱码,而是xml格式的代码,在手机等移动端显示不出正常的文档信息。之后辗转查询使用了很多方案,比如利用poi操作,利用jacob进行格式转换,但是效果都不尽如人意,而且处理过程非常复杂,浪费了大量时间。最终,通过不断摸索,找到了非常完美的解决方案。不需要先生成xml格式的doc文档再去转换,而是戒指在后台利用模版添加数据后,利用IO流进行替换,可以直接生成标准的docx文档。
首先需要理解的一点是,docx文档本身是一个压缩文件。在利用这个方案前要先准备好docx模版。在这个docx模版中,将要替换的数据用${}的方式替换掉,先占好位置。如图:
将docx改后缀名为.zip并解压出来。在word文件夹下有一个文件为、:document.xml
利用notpad等工具打开此文件,将格式修改好,比如在xml文件中${xxx}分别氛围${ , xxx 以及 } 三部分存贮在不同的标签下,要将他们合并到一起。
随后将保存好的document.xml和之前定义好的zip文件放入工程的配置文件夹下,方便引用。
核心代码如下
Configuration
configuration = new Configuration();
/** 设置编码 **/
/** 我的ftl文件是放在D盘的**/
String fileDirectory = "D:/cache/qqChace/T1/xlsx";
/** 加载文件 **/
configuration.setDirectoryForTemplateLoading(new File(fileDirectory));
/** 加载模板 **/
Template template = configuration.getTemplate("document.xml");
/** 准备数据 **/
Map<String,String> dataMap = new HashMap<>();
/** 在ftl文件中有${textDeal}这个标签**/
dataMap.put("id","哈士奇");
dataMap.put("number","20");
dataMap.put("language","java,php,python,c++.......");
dataMap.put("example","Hello World!");
/** 指定输出word文件的路径 **/
String outFilePath = "D:/cache/qqChace/T1/xlsx/data.xml";
File docFile = new File(outFilePath);
FileOutputStream fos = new FileOutputStream(docFile);
OutputStreamWriter oWriter = new OutputStreamWriter(fos);
Writer out = new BufferedWriter(new OutputStreamWriter(fos),10240);
template.process(dataMap,out);
if(out != null){
out.close();
}
// ZipUtils 是一个工具类,主要用来替换具体可以看github工程
ZipInputStream zipInputStream = ZipUtils.wrapZipInputStream(new FileInputStream(new File("D:/cache/qqChace/T1/xlsx/test.zip")));
ZipOutputStream zipOutputStream = ZipUtils.wrapZipOutputStream(new FileOutputStream(new File("D:/cache/qqChace/T1/xlsx/test.docx")));
String itemname = "word/document.xml";
ZipUtils.replaceItem(zipInputStream, zipOutputStream, itemname, new FileInputStream(new File("D:/cache/qqChace/T1/xlsx/data.xml")));
System.out.println("success");
上面主要解决的问题事文字的转换,下面要说的关于图片的转换问题。
将zip文件解压后会发现,document.xml中没有你原本保存在docx中的图片,图片是在word下的media文件夹下。document.xml中或许只是引用。但是仍可以通过这种方式,以流的形式替换图片。只要保证图片的顺序不乱就可以了。
下面是我在上面这段代码上进行的改动
String zipPath = this.getClass().getResource("/freemarkerDoc.zip").getPath();
try {
ZipInputStream zipInputStream = ZipUtils.wrapZipInputStream(new FileInputStream(new File(zipPath)));
ZipOutputStream zipOutputStream = ZipUtils.wrapZipOutputStream(new FileOutputStream(new File(basePath+patientInfo.getName()+"连续血氧检测报告("+reportInfo.getId()+").docx")));//这个是要生成文档的路径和名称
String itemname = "word/document.xml";
String itemname2 = "word/media/";
String picturePath = PropertiesUtil.getBinaryRoorPath("picturePath")+File.separator+patientInfo.getId()+File.separator+reportInfo.getId()+File.separator;
ZipUtils.replaceItem(zipInputStream, zipOutputStream, itemname, new FileInputStream(new File(url)), itemname2, picturePath);
System.out.println("success");
} catch (Exception e) {
System.out.println(e.toString());
}
我的docx文档中工有四张图片,所以在zipUtil中要传入你要替换上去的图片路径。代码如下
public static void replaceItem(ZipInputStream zipInputStream,
ZipOutputStream zipOutputStream,
String itemName,
InputStream itemInputStream
,
String itemName2,
String picturePath
){
//
if(null == zipInputStream){return;}
if(null == zipOutputStream){return;}
if(null == itemName){return;}
if(null == itemInputStream){return;}
InputStream itemIS = null;
//
ZipEntry entryIn;
try {
while((entryIn = zipInputStream.getNextEntry())!=null)
{
String entryName = entryIn.getName();
ZipEntry entryOut = new ZipEntry(entryName);
// 只使用 name
zipOutputStream.putNextEntry(entryOut);
// 缓冲区
byte [] buf = new byte[8*1024];
int len;
//image1.png等是media目录下的图片名称。
String ima1 = itemName2+"image1.png";
String ima2 = itemName2+"image2.png";
String ima3 = itemName2+"image3.png";
String ima4 = itemName2+"image4.png";
if(entryName.equals(itemName)){
// 使用替换流
while((len = (itemInputStream.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
}else if(entryName.equals(ima1)){
// 使用替换流,将需要替换上去的图片路径以流的形式获取
itemIS = new FileInputStream(new File(picturePath+"spo2PiePicture.png"));
while((len = (itemIS.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
}else if(entryName.equals(ima2)){
// 使用替换流
itemIS = new FileInputStream(new File(picturePath+"prPiePicture.png"));
while((len = (itemIS.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
}else if(entryName.equals(ima3)){
// 使用替换流
itemIS = new FileInputStream(new File(picturePath+"spo2Picture.png"));
while((len = (itemIS.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
}else if(entryName.equals(ima4)){
// 使用替换流
itemIS = new FileInputStream(new File(picturePath+"prPicture.png"));
while((len = (itemIS.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
}else {
// 输出普通Zip流
while((len = (zipInputStream.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
}
// 关闭此 entry
zipOutputStream.closeEntry();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//e.printStackTrace();
close(itemInputStream);
close(zipInputStream);
close(zipOutputStream);
close(itemIS);
}
}
转换完成后要讲所有的流全部关闭,就完成了。