动态生成pdf
需求
最近有个需求,就是办公系统,往往有生成doc,pdf的需求。以往,我接触到的系统,生成doc,有这么几种方式,一种是直接利用ipoi来生成doc,这种最大的问题是,当模板的样式比较复杂的话,生成极为困难,我也没进过有人这么生成,目前这家公司倒是有这么一个思路,利用itext直接读取模板的documnet,然后因为模板里插有书签,那么我们可以在读到书签后,直接替换为数据,那么就可以达到生成doc的目的,但是需求中如果有表格,循环数据,这种需求的话,应该是要循环插入样式和数据,这种我觉得是比较复杂的,耗时耗力。然后我前一家的公司,主要做的是银行业务,这种doc报表还是比较多的,都是基本利用将doc另存为xml,然后利用freemarker来生成带有数据的doc,或者是直接转为html,然后渲染html来给前端预览,这种也是比较讨巧的。样式也很支持。
但是在遇到转为pdf的时候,问题暴露出来了,利用xml生成doc或者docx的方法,其本质还是xml,然后利用openoffice来生成pdf的话,就可能直接报错或者转换的是xml的pdf,这样肯定是不行的,所以需要找到一个方法能动态生成pdf。
第一个思路,xml转doc,在转pdf
因为doc转pdf,直接调用本地的openoffice,还是好转换pdf的,但是现在目前生成的是doc的皮,本质是xml,所以我们就需要将xml转换为doc,我本来以为是会比较多的方法,但是结果百度了一圈,发现,大部分都是不支持,或者支持的是在线转换,这样肯定是不行的,靠谱点的也就是利用调用本地的office,这又涉及到,不支持linux,不支持商用。毙掉
第二个思路,利用html转pdf
我们xml转pdf失败,那直接用html转pdf好吗?我又百度了一圈,发现比较好的思路也就是
https://www.cnblogs.com/yunfeiyang-88/p/10984740.html
但是主要问题也是两个,一个是图片资源问题,这种转换出来的html,他的图片资源需要自己管理,想想也知道,转换的html,他引用的是外部的图片,所以项目用的话,需要管理图片资源,然后样式转换,我记得说复杂样式是支持不好的,我手动转,doc转html,然后转回doc也是丢了些样式。小宇同学说他转换的工具样式支持的比较好,没尝试,待检验。
https://www.jianshu.com/p/4d65857ffe5e/
https://blog.csdn.net/qq_38423105/article/details/81389445
第三个思路,利用docx转pdf
我们动态生成的doc本质还是xml,那我们能不能动态生成docx呢?在我的理解下,doc本质是流,而docx是本质已经是xml了(其基于Office Open XML标准的压缩文件格式取代了其以前专有的默认文件格式,在传统的文件名扩展名后面添加了字母“x”),我百度了下,动态生成docx是可以做到的。
动态生成docx
具体思路就是:
1、将docx文档模板改为ZIP格式(修改.docx后缀名为.zip),然后把zip解压到当前目录
2.将解压后word目录下document.xml文档放入项目中,用于后边内容填充。
这里有个坑,document.xml的读取,因为我们部署是打包成jar来部署,那么就涉及到一个问题,项目开发能读到的资源,在打包成jar包后读取不到,报file not found 的错误,这个原因是在本地开发和war部署的时候,利用类加载器获取到当前类,然后加载,这个本质还是拼接路径。
String basePath = FreeMarkerWordUtil.class.getResource("/").getPath() + "templates/";
这样的话,导致打包成jar,jar里可不是让你随便读取的,所以会出错,java提供了以流的方式读取资源,所以freemarker的配置记得配置的模板路径为流。
3.修改document.xml,利用freemarker标签填充document.xml
这里又有坑,如果你格式化document.xml,然后写freemarker的标签,你会发现,生成的docx会格式乱了,样式乱了。
我猜想的是,docx读取的时候,其实里面的样式有读取换行和空格,所以导致docx全乱了。
然后我测试了下,xml的格式化网站的格式化后压缩,是不会丢样式的。
但是有个问题,我们的freemarker的<#if> <#list>这些标签,对xml来说是不合法的标签,如果xml格式化后压缩,还是丢失样式。
但是如果不格式化,然后填充数据,太折磨人了。
所以我们的解决方案是,先不管循环或者判断的填充数据,先格式化后把一个个填充的先填充好的,然后格式化,在手动把需要循环或者判断的标签加上去,比如之前用特殊字符占位,然后替换。
4.在输入docx文档的时候把填充过内容的的 document.xml用流的方式写入zip
package com.xy.microservice.orderserver.utils;
import com.xy.microservice.common.utils.idUtil.IdGen;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import java.io.*;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
//注意有个坑
//freemarker读取文件的时候,默认是以文件形式的读取模板
//所以,打包成war或者本地运行都是可以的
//但是,打包成jar包,jar包里的文件不是文件,jar中的文件不能以File的形式读取,只能以流的形式读取。如果用File读取就会报文件不存在的异常。看下TemplateLoader的实现类,可以换成SpringTemplateLoader,这个模版加载器是用流的形式读取模版
//所以我改变了模板加载方式FreeMarkerWordUtil
@Slf4j
public class FreeMarkerWordUtil {
/**
* @Desc:生成文件通过map,通过本地项目的resource的templates文件夹下模板文件渲染到对应文件夹下
* @Author:wzy
* @Date:
* @param dataMap
* word中需要展示的动态数据,用map集合来保存
* @param templateName
* word模板名称,例如:test.ftl
* @param fileName
* 生成的文件名称,例如:test.doc
*/
public static String createWord(Map dataMap, String templateName,String destPath, String fileName) {
String filePath = "";
try {
String basePath = FreeMarkerWordUtil.class.getResource("/").getPath() + "templates/";
//System.out.println(destPath);
// 创建配置实例
Configuration configuration = new Configuration();
// 设置编码
configuration.setDefaultEncoding("UTF-8");
// ftl模板文件统一放至 resouce.template 包下面
/*configuration.setClassForTemplateLoading(FreeMarkerWordUtil.class,
"/config/model/");*/
//读取jar包下的模板
configuration.setClassForTemplateLoading(FreeMarkerWordUtil.class, "/templates/");
configuration.setTemplateLoader(new ClassTemplateLoader(FreeMarkerWordUtil.class, "/templates/"));
//war包方式读取文件
// FileTemplateLoader ftl1 = new FileTemplateLoader(new File(basePath));
// configuration.setTemplateLoader(ftl1);
// 获取模板
Template template = configuration.getTemplate(templateName, "UTF-8");
filePath = destPath + File.separator + fileName;
// 输出文件
File outFile = new File(filePath);
// 如果输出目标文件夹不存在,则创建
if (!outFile.getParentFile().exists()) {
outFile.getParentFile().mkdirs();
}
// 将模板和数据模型合并生成文件
Writer out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(outFile), "UTF-8"));
// 生成文件
template.process(dataMap, out);
try {
Runtime.getRuntime().exec("chmod 775 " + destPath + File.separator + fileName);
}catch (Exception e){
}
// 关闭流
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
return filePath;
}
/**
* @Desc:生成word文件通过对象,通过本地项目的resource的templates文件夹下模板文件渲染到对应文件夹下
* @Author:comlc
* @Date:
* @param data
* word中需要展示的动态数据,用对象来保存
* @param templateName
* word模板名称,例如:test.ftl
* @param fileName
* 生成的文件名称,例如:test.doc
*/
public static String createWordByObject(Object data, String templateName,String destPath, String fileName) {
String filePath = "";
try {
String basePath = FreeMarkerWordUtil.class.getResource("/").getPath() + "templates/";
//System.out.println(destPath);
// 创建配置实例
Configuration configuration = new Configuration();
// 设置编码
configuration.setDefaultEncoding("utf-8");
// ftl模板文件统一放至 com.lun.template 包下面
/*configuration.setClassForTemplateLoading(FreeMarkerWordUtil.class,
"/config/model/");*/
configuration.setClassForTemplateLoading(FreeMarkerWordUtil.class, "/templates/");
configuration.setTemplateLoader(new ClassTemplateLoader(FreeMarkerWordUtil.class, "/templates/"));
//war包方式读取文件
// FileTemplateLoader ftl1 = new FileTemplateLoader(new File(basePath));
// configuration.setTemplateLoader(ftl1);
// 获取模板
Template template = configuration.getTemplate(templateName, "utf-8");
// 输出文件
File outFile = new File(destPath + File.separator + fileName);
// 如果输出目标文件夹不存在,则创建
if (!outFile.getParentFile().exists()) {
outFile.getParentFile().mkdirs();
}
// 将模板和数据模型合并生成文件
Writer out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(outFile), "UTF-8"));
template.process(data, out);
filePath = destPath + File.separator + fileName;
try {
Runtime.getRuntime().exec("chmod 775 " + destPath + File.separator + fileName);
}catch (Exception e){
}
// 关闭流
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
return filePath;
}
/**
* 获取模板文件的string
* @param o 数据对象
* @param templateName 模板名称
* @return
*/
public static String createStringByFile(Object o, String templateName){
String returnString="";
try {
String basePath = FreeMarkerWordUtil.class.getResource("/").getPath() + "generatorConfig/";
Configuration configuration = new Configuration(Configuration.VERSION_2_3_23);
configuration.setDefaultEncoding("UTF-8");
FileTemplateLoader ftl1 = new FileTemplateLoader(new File(basePath));
configuration.setTemplateLoader(ftl1);
// 获取模板
Template template = configuration.getTemplate(templateName);
StringWriter writer = new StringWriter();
template.process(o,writer);
returnString=writer.toString();
System.out.println(writer.toString());
} catch (Exception e) {
e.printStackTrace();
}
return returnString;
}
public static String createString(Map dataMap, String templateString) {
String returnString="";
Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
StringTemplateLoader stringLoader = new StringTemplateLoader();
stringLoader.putTemplate("myTemplate",templateString);
cfg.setTemplateLoader(stringLoader);
try {
Template template = cfg.getTemplate("myTemplate","utf-8");
StringWriter writer = new StringWriter();
try {
template.process(dataMap, writer);
returnString=writer.toString();
System.out.println(writer.toString());
} catch (TemplateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return returnString;
}
private static String getJarResouce2TempFile(String filepath) throws IOException {
if(filepath==null || "".equals(filepath)) return null;
//返回读取指定资源的输入流
// ClassPathResource classPathResource = new ClassPathResource(filepath);
InputStream is=FreeMarkerWordUtil.class.getResourceAsStream(filepath);
String fileSuffix = filepath.substring(filepath.lastIndexOf("."));
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String tempfilePath="/tmp/"+IdGen.uuid()+fileSuffix;
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(tempfilePath)));
String ss;
while((ss=br.readLine()) != null){
System.out.println(ss);
pw.println(ss);
}
pw.close();
// BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(tempfilePath));
// int len;
// while((len=br.read())!=-1) {
// bos.write(len);
// }
br.close();
// bos.close();
log.info("读取资源文件{}成功",filepath);
return tempfilePath;
}
public static String getFileString(String file) throws IOException {
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis, "utf-8");
BufferedReader br = new BufferedReader(isr);
StringBuilder stringBuilder = new StringBuilder();
String temp="";
while (temp != null) {
temp = br.readLine();
if (temp != null ) {
stringBuilder.append(temp);
}
}
return stringBuilder.toString();
}
public static boolean repalceZip(String tempFileName,String docxName,String targetPath) throws Exception {
File file = new File(tempFileName);
ZipFile zipFile = new ZipFile(docxName);
Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
ZipOutputStream zipout = new ZipOutputStream(new FileOutputStream(targetPath));
int len = -1;
byte[] buffer = new byte[8888888];
while (zipEntrys.hasMoreElements()) {
ZipEntry next = zipEntrys.nextElement();
InputStream is = zipFile.getInputStream(next);
// 把输入流的文件传到输出流中 如果是word/document.xml由我们输入
zipout.putNextEntry(new ZipEntry(next.toString()));
if ("word/document.xml".equals(next.toString())) {
InputStream in = new FileInputStream(file);
while ((len = in.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
in.close();
} else {
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}
zipout.close();
return true;
}
/**
* @Description 根据参数生成docx合同文档
* @throws Exception
*/
public static boolean outputWordByObject(String templateName,String docxName,String targetPath, Object object) throws Exception {
String tempFileName= IdGen.uuid()+".xml";
createWordByObject(object,templateName,"/tmp",tempFileName);
File file = new File("/tmp/"+tempFileName);
repalceZip(file.getPath(),docxName,targetPath);
if(file.exists()){
file.delete();
}
return true;
}
/**
* @Description 根据参数生成docx合同文档
* @throws Exception
*/
public static boolean outputWord(String templateName,String docxName,String targetPath, Map<String, Object> param) throws Exception {
String tempFileName= IdGen.uuid()+".xml";
createWord(param,templateName,"/tmp",tempFileName);
File file = new File("/tmp/"+tempFileName);
repalceZip(file.getPath(),docxName,targetPath);
if(file.exists()){
file.delete();
}
return true;
}
public static void main(String[] args) throws Exception {
// Map<String,Object> hashMap = new HashMap<>();
// hashMap.put("name","浙江鑫烨");
createString(hashMap,"${name!}");
createWord(hashMap,"test.xml","/home","document.xml");
// ZipUtils 是一个工具类,主要用来替换
ZipInputStream zipInputStream = ZipUtils.wrapZipInputStream(new FileInputStream(new File("F:\\home\\3221.zip")));
ZipOutputStream zipOutputStream = ZipUtils.wrapZipOutputStream(new FileOutputStream(new File("F:\\home\\3221.docx")));
String itemname = "word/document.xml";
ZipUtils.replaceItem(zipInputStream, zipOutputStream, itemname, new FileInputStream(new File("/home/document.xml")));
WordUtil.word7Pdf("F:\\home\\3221.docx","F:\\home\\3221.pdf");
// outputWord("9757 OA-test.xml","2.docx","/home/3.docx",hashMap);
//
//
// System.out.println("success");
String file="/tmp/3.docx";
int i = file.lastIndexOf(".");
System.out.println(file.substring(i+1,file.length()));
}
}
本质就是利用zipentry读取docx,然后替换掉document.xml内容,然后在输出为docx文件,这样就达到了动态生成的目的。
但是有问题,docx与document.xml绑定严重,想想也知道,他们是配套的。
然后怎么读取docx,如果是放项目的资源文件夹里,又有那个问题,读取jar包只能是流,
我想过读取流然后输出文件来再一步生成资源,但是乱码了,生成也失败了。
想过读取流来替换zipentry。没试验。
最后就要么本地建立个专门的文件夹,放模板。
要么传aws,我最后选择项目里的模板管理,将docx传到aws,用到再下载到本地。
5.输出docx文档