Bootstrap

java对word文档预设参数填值并生成(包含图片)

目录

(1)定义word文档模板

(2)模板二次处理

处理模板图片,不涉及图片可以跳过

处理模板内容

(3)java对word模板填值

(4)Notepad++的XML Tools插件安装


工作上要搞一个合同签署功能,小程序上登录人进入功能会对合同进行电子签名,然后后端根据登录人信息和合同word文档模板生成一个合同word文档并保存,踩了不少坑。一开始是用了apache的poi,使用简单,读取word模板,然后遍历每一个段落和节点,判断节点是不是我定义的参数名,是就替换文本值,本以为会很简单就能搞定,结果每次遍历获取的节点经常不完整,${name}获取的时候可能会被分成三段,【${】【name】【}】,导致参数替换不上去,想着直接获取所有文本信息,然后直接string的replace方法替换,结果也不行,他不给直接对段落进行修改,直接麻了,而且pio包引入后,如果当前springboot版本太低,有些包就会出问题,又得特别处理,贼麻烦,后来就换成了freemarker,支持替换图片,不过步骤有些繁琐。


(1)定义word文档模板


既然是对word文档进行填值,那肯定得先定义一个word文档模板了,建一个docx后缀的word文档,并设置好格式,参数等,比如我建了一个word文件名为:wordTemplate.docx,内容是这样的,参数名是用${}包起来


(2)模板二次处理


格式全部调好后保存,并把文件后缀名改成zip,让它变成压缩包,记住你的word模板一定要是docx后缀的,doc后缀是老版的,搞不了。

打开压缩包是这个样子。

进入word文件夹里面是这样的,我们只需要关注【document.xml】【_rels】【media】,如果你不打算放图片,那只关注【document.xml】就行了。


处理模板图片,不涉及图片可以跳过


打开_rels文件夹是这样的。

【document.xml.rels】文件就是存放图片和文档之间的关系,我们把它解压出来并打开。

【打开xml时没有格式化过的,我用了Notepad++打开的,并装了XML Tools插件,然后把它格式化后才成了这个样子的】具体插件安装在文章最后面讲。


接着讲,image1名称是word文档生成的,为了后面填值方便区别,把 media/image1.png 改成 media/headImage.png,然后保存,回到压缩包里面,把原来的【document.xml.rels】替换掉,再打开【media】文件夹,会发现里面有个图片,名为image1.png,就是我们提前放入的头像,我们得把它改名成【headImage.png】,因为上一步,我们修改了【document.xml.rels】文件的映射,这里也得改。

这时候再把压缩包后缀改成docx,你会发现一样还能打开,不过这里我们改回去打开检查没问题后,再把后缀改回成zip。


这里再说一个点,如果模板图片太多,势必会造成模板文件很大,我们可以找一个透明或纯白色的宽高都是1px的图片, 反正很小的图片就行,名字改成【media】里面的图片名,放入【media】里面,相当于占位。也可以一开始创建word模板的时候,放图片直接放这个小体积的图片,把宽高调整到合适的就行。


处理模板内容


打开压缩包把【document.xml】模板内容文件解压出来打开并xml格式化,找到你设置的参数名。

这时候会发现,参数名乱了,${name}可能被分隔七零八落,我们需要重新调一下,确保参数名完整,然后再保存。

这里再讲讲图片跟【document.xml】的关联,我们通过【document.xml.rels】图片映射文件可以看到,每一个图片标签都会有一个id,这个头像这个图片的id是rId4。

我们复制它去【document.xml】里面找,就会发现这个图片的标签以及格式了。

了解一下就行了,回归正题,我们调整好【document.xml】文件并保存,这个文件暂时不需要放回压缩包替换原来文件,到这一步,我们就有两个文件了,这两个文件缺一不可。


找一张头像准备好,用来替换模板里的头像。头像就是下面这个狗头。至于说这个图片放哪,随意,反正到时候java能用IO流读取到就行了,准备图片的base64也行,后面我写了代码支持base64格式。


(3)java对word模板填值


处理完word模板后,来到java代码。 

先引入包。不推荐用最新的包,我引了最新的包,用main方法测试的时候没问题,结果启动spring服务的时候,就不行了,报找不到那个版本的参数,后来降级就可以了。

<dependency>
	<groupId>org.freemarker</groupId>
	<artifactId>freemarker</artifactId>
	<version>2.3.28</version>
</dependency>

代码大致思路是这样的:

  1. 读取模板【document.xml】
  2. 值填充
  3. 重新生成一个【document.xml】
  4. 把这个新的【document.xml】和头像图片写入定义好的压缩包模板,也就是【wordTemplate.zip】
  5. 然后把压缩包输出成word文档

写一个main方法用来测试。


public static void main (String[] args) {
	// 模板存放路径
	String templastPath = "D:/A";
	// zip模板名称
	String zipTemplastName = "wordTemplate.zip";
	// zip模板存放路径
	String zipTemplastPath = new StringBuffer(templastPath).append(File.separator).append(zipTemplastName).toString();
	// docx文档模板名称
	String docxTemplateName = "document.xml";
	// 输出路径
	String outPath = "D:/A/out";
	// docx模板填充后输出路径,这里的【document.xml】不能改
	String outputDocxTemplatePath = new StringBuffer(outPath).append(File.separator).append("document.xml").toString();
	// 最终生成的docx文档输出路径,这里的word文档输出文件名随意
	String outputDocxFilePath = new StringBuffer(outPath).append(File.separator).append("output.docx").toString();
	File outPathFile = new File(outPath);
	if (!outPathFile.exists() && !outPathFile.mkdirs()) {
		throw new RuntimeException("输出路径创建失败");
	}
	try (
		FileOutputStream out = new FileOutputStream(outputDocxTemplatePath);
		OutputStreamWriter outputStreamWriter = new OutputStreamWriter(out);
		BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
		// zip模板压缩包输入流
		FileInputStream zipTemplastInput = new FileInputStream(zipTemplastPath);
		// 最终docx文档输出流
		FileOutputStream finalDocxOutput = new FileOutputStream(outputDocxFilePath);
	){
		// 将要替换的值
		Map<String, Object> map = new HashMap<String, Object>() {{
			put("name", "韩西景");
			put("sex", "男");
			put("age", "51");
			put("homeAddress", "广东省广州市白云区景山路3612号");
		}};

		//创建配置实例 VERSION_2_3_28是pom文件引入时的版本号
		Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
		//设置编码
		configuration.setDefaultEncoding("UTF-8");
		// 设置模板路径
		configuration.setDirectoryForTemplateLoading(new File(templastPath));
		// 获取xml模板
		Template template = configuration.getTemplate(docxTemplateName);
		// 参数值填充
		template.process(map, bufferedWriter);
		// 读取模板zip压缩包
		ZipInputStream zipInputStream = ZipUtils.wrapZipInputStream(zipTemplastInput);
		// 最终生成的word文档输出流
		ZipOutputStream zipOutputStream = ZipUtils.wrapZipOutputStream(finalDocxOutput);
		// zip压缩包要替换的项
		Map<String, String> replaceItemMap = new HashMap<String, String>(){{
			// 替换图片,本地路径可以,网络路径不行
			// put("word/media/headImage.png", "D:/A/headImage.png");
			// 替换图片,base64写入
			put("word/media/headImage.png", new StringBuffer("data:image/png;base64,").append(imageToBase64("D:/A/headImage.png")).toString());
			// 替换内容
			put("word/document.xml", outputDocxTemplatePath);
		}};
		ZipUtils.replaceItem(zipInputStream, zipOutputStream, replaceItemMap);
	} catch (Exception e) {
		System.out.println("报错了,这里自己打log啥的,该处理的处理");
	} finally {
		// 删除填充后xml模板
		new File(outputDocxTemplatePath).delete();
	}
}

/**
 * 图片转base64
 * @param inputPath 图片路径
 * @return base64字符串
 */
private static String imageToBase64(String inputPath) {
	File file = new File(inputPath);
	ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
	try(
		FileInputStream fileInputStream = new FileInputStream(inputPath);
		BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
	) {
		int b;
		byte[] bytes = new byte[4096];
		while ((b = bufferedInputStream.read(bytes)) > -1) {
			byteBuffer.put(bytes, 0, b);
		}
		return Base64.encodeBase64String(byteBuffer.array());
	} catch (Exception e) {
		System.out.println("报错了,这里自己打log啥的,该处理的处理");
	}
	return null;
}

这是压缩包内容替换util类

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.Map;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

public class ZipUtils {
    private static final Logger log = LoggerFactory.getLogger(ZipUtils.class);

    /**
     * 替换某个 item,
     * @param zipInputStream zip文件的zip输入流
     * @param zipOutputStream 输出的zip输出流
     * @param replaceItemMap 要替换的项 key为项名称,value为新文件的路径
     */
    public static void replaceItem(ZipInputStream zipInputStream,
                                   ZipOutputStream zipOutputStream,
                                   Map<String, String> replaceItemMap
    ){
        if(Objects.isNull(zipInputStream) || Objects.isNull(zipOutputStream) || Objects.isNull(replaceItemMap)){
            return;
        }
        ZipEntry entryIn;
        try {
            // 缓冲区
            byte [] buf = new byte[4096];
            int len;
            while((entryIn = zipInputStream.getNextEntry())!=null) {
                String entryName =  entryIn.getName();
                ZipEntry entryOut = new ZipEntry(entryName);
                // 只使用 name
                zipOutputStream.putNextEntry(entryOut);
                // 是否已替换
                boolean replaceFlag = false;
                for (Map.Entry<String, String> entry : replaceItemMap.entrySet()) {
                    if(entryName.equals(entry.getKey()) && StringUtils.isNotBlank(entry.getValue())){
                        String value = entry.getValue();
                        if (StringUtils.startsWith(value, "data:image/")) {
                            // 如果是base64格式的图片
                            value = value.substring(value.indexOf(";base64,") + 8);
                            byte[] bytes = Base64.decodeBase64(value);
                            zipOutputStream.write(bytes, 0, bytes.length);
                        } else {
                            InputStream itemInputStream = new FileInputStream(entry.getValue());
                            BufferedInputStream bufferedInputStream = new BufferedInputStream(itemInputStream);
                            // 使用替换流
                            while((len = (bufferedInputStream.read(buf))) > 0) {
                                zipOutputStream.write(buf, 0, len);
                            }
                            close(bufferedInputStream);
                            close(itemInputStream);
                        }
                        replaceFlag = true;
                        break;
                    }
                }
                if (!replaceFlag) {
                    // 输出普通Zip流
                    while((len = (zipInputStream.read(buf))) > 0) {
                        zipOutputStream.write(buf, 0, len);
                    }
                }
                // 关闭此 entry
                zipOutputStream.closeEntry();
            }
        } catch (IOException e) {
            log.error("压缩包内容替换出错", e);
        } finally {
            close(zipInputStream);
            close(zipOutputStream);
        }
    }

    /**
     * 包装输入流
     */
    public static ZipInputStream wrapZipInputStream(InputStream inputStream){
        ZipInputStream zipInputStream = new ZipInputStream(inputStream);
        return zipInputStream;
    }

    /**
     * 包装输出流
     */
    public static ZipOutputStream wrapZipOutputStream(OutputStream outputStream){
        ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
        return zipOutputStream;
    }
    private static void close(InputStream inputStream){
        if (null != inputStream){
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error("输入流关闭错误", e);
            }
        }
    }

    private static void close(OutputStream outputStream){
        if (null != outputStream){
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {
                log.error("输出流关闭错误", e);
            }
        }
    }
}

执行main方法之后,可以看到生成word文档了。


部署到服务器的话,就把模板文件放到服务器上面,然后配置模板路径,具体使用引入模板路径就行了。

至于对word文档里面的表格生成这种,暂时没有研究。 


(4)Notepad++的XML Tools插件安装

仅用于格式化模板xml文件,方便调整xml文件,可装可不装,问题不大。 

插件安装:顶部菜单栏【插件】->【插件管理】打开后


好了,到这结束了,真够累人,这该死的996,永无止境的打工。


码字不易,于你有利,勿忘点赞 

;