动态生成word文件并转成pdf格式(支持linux+windows)
前言
最近由于项目要求需要导出pdf文件,模板是word版本
项目有两期需求第二期是关于动态合并word中的单元格,这里合并在一起写给各位
我也是踩了各种坑之后才完整写出这篇文章
一、导出word文件
环境准备
word替参需要 freemarker-2.3.30.jar
word转pdf需要 docx4j-6.1.2.jar
建议使用maven防止缺失相关依赖包
示例
首先找一个word模板
我这边主要是处理表格示例如下
单个参数
如果是单个参数不在循环中直接替换成${参数名字}即可
动态行参数
在需要填参的地方写入自己定义的名字方便在xml文件中查找,另存为word xml文档,此处有一个坑,如果用上方的xml 文件,在有些2003文件格式的会出现不能转换问题
打开XML文件(此处我使用的notepad++然后使用XML tools进行格式化)
找到你需要填写参数的那一行
以上面图片为例第一个填写参数为“日期”,找到对应的字,然后向上查找到
<w:tr>标签这个代表的是一行
此处我需要使用list进行循环出多行所以在<w:tr>标签前面加入
<#list refuelList as refuel>
并找到对应的tr结束标签加入对应的</#list>
在中间的相关参数替换成如下这种参数
参数解析${}表示占位符是必须的 refuel.refuelDateStr中refuel来自<#list refuelList as refuel>中as后的值,refuelDateStr代表list中的具体参数名字
动态行合并参数
首先找到需要合并的列位置
此处根据上方动态行参数图片找到对应日期所在位置然后向上找到<w:tc>位置在如图位置添加判断,此处用的比较简单的方式判断
合并需要的属性值:<w:vMerge w:val=‘restart’/>和<w:vMerge/>
图片参数:
首先在word模板中添加一张图片,注:如需插入多张图片切记不要上传同一张图片,不然上传完后只能在xml中找到一段base64字符串,word相同图片会进行引用
base64字符串很容易就找到此处不截图了,找到base64字符串后替换成 ${image1},整段base64字符串替换的。
注:此处为了解决如果无图片也会进行图片大小占位问题需要找到<wp:extent cx="…" cy="…"/> 标签
此处的cx代表图片宽度,cy代表图片高度他们都是 *9525的倍数
使用图片作为参数是可能会缺失依赖包xmlgraphics-commons-2.1.jar,可以去maven仓自行下载
此处是我处理这个图片的部分代码
BASE64Encoder base64 = new BASE64Encoder();
InputStream in = null;
try {
try {
// 输入流
File imageFile = new File(filePathName);
BufferedImage sourceImg = ImageIO.read(new FileInputStream(imageFile));
//必需元素: <wp:extent cx="5274310" cy="3620117"/> 是图片的宽高= 像素值*9525;
//这边先固定宽度,长度根据上传的图片进行处理,防止图片太大导致word格式不好看问题
Integer width = 250;
sourceImg.getWidth();
Integer height = (Integer)(sourceImg.getHeight()*250)/sourceImg.getWidth();
dataMap.put("imageSize1","<wp:extent cx=\""+width*9525+"\" cy=\""+height*9525+"\"/>");
in = new FileInputStream(imageFile);
byte[] bytes = new byte[in.available()];
if(bytes.length>0){
in.read(bytes);
dataMap.put("image1",base64.encode(bytes));
}
} finally {
// 完毕,关闭所有链接
if (EmptyUtil.isNotEmpty(in)) {
in.close();
}
}
动态单元格合并代码示例
import common.utils.DocUtils;
import common.utils.PdfUtils;
import module.vms.refuel.model.VmsRefuelModel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class mergeCellsUtils {
public static void main(String[] args) throws Exception{
mergeCells();
}
public static void mergeCells() throws Exception{
Map<String, Object> dataMap = new HashMap<String, Object>();
List<VmsRefuelModel> lists = new ArrayList<VmsRefuelModel>();
VmsRefuelModel refuelModel1 = new VmsRefuelModel();
refuelModel1.setRefuelDateStr("2020-09-07");
refuelModel1.setRefuelTime("09:15:10");
refuelModel1.setRefuelPrice(5.7);
refuelModel1.setRefuelLiter(20.0);
refuelModel1.setRefuelAmount(114.0);
refuelModel1.setGasStation("中石化加油站");
lists.add(refuelModel1);
VmsRefuelModel refuelModel2 = new VmsRefuelModel();
refuelModel2.setRefuelDateStr("null");
refuelModel2.setRefuelTime("null");
refuelModel2.setRefuelPrice(5.7);
refuelModel2.setRefuelLiter(21.0);
refuelModel2.setRefuelAmount(115.0);
refuelModel2.setGasStation("null");
lists.add(refuelModel2);
VmsRefuelModel refuelModel3 = new VmsRefuelModel();
refuelModel3.setRefuelDateStr("null");
refuelModel3.setRefuelTime("null");
refuelModel3.setRefuelPrice(5.7);
refuelModel3.setRefuelLiter(22.0);
refuelModel3.setRefuelAmount(116.0);
refuelModel3.setGasStation("null");
lists.add(refuelModel3);
VmsRefuelModel refuelModel4 = new VmsRefuelModel();
refuelModel4.setRefuelDateStr("2020-09-08");
refuelModel4.setRefuelTime("09:15:30");
refuelModel4.setRefuelPrice(5.7);
refuelModel4.setRefuelLiter(23.0);
refuelModel4.setRefuelAmount(117.0);
refuelModel4.setGasStation("中石化加油站1");
lists.add(refuelModel4);
VmsRefuelModel refuelModel5 = new VmsRefuelModel();
refuelModel5.setRefuelDateStr("2020-09-09");
refuelModel5.setRefuelTime("09:15:30");
refuelModel5.setRefuelPrice(5.7);
refuelModel5.setRefuelLiter(24.0);
refuelModel5.setRefuelAmount(118.0);
refuelModel5.setGasStation("中石化加油站2");
lists.add(refuelModel5);
VmsRefuelModel refuelModel6 = new VmsRefuelModel();
refuelModel6.setRefuelDateStr("null");
refuelModel6.setRefuelTime("null");
refuelModel6.setRefuelPrice(5.7);
refuelModel6.setRefuelLiter(25.0);
refuelModel6.setRefuelAmount(119.0);
refuelModel6.setGasStation("null");
lists.add(refuelModel6);
dataMap.put("refuelList",lists);
//获取模板
String fileBasePath = "D:/tempFile/vmsfile/template/vehicle/";
String tempFileName = "tempFile.ftl";
String outXmlPath = DocUtils.docParamHandle(dataMap,fileBasePath,tempFileName);
//将doc文档转化为word类型文档
String docPath = DocUtils.xmlCoverDoc(outXmlPath);
//将word类型文件转化为pdf文档
String pdfPath = PdfUtils.docCoverPdf(docPath);
System.out.println(pdfPath);
}
}
工具类代码
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.docx4j.Docx4J;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.Map;
/**
* @author John
* 2020年9月1日15:49:37
*/
public class DocUtils {
private final static Logger log = LoggerFactory.getLogger(DocUtils.class);
/**
* 将xml格式的文件使用freemarker将参数替换
* @param dataMap 文档参数替换
* @param tempPath 模板路径
* @param tempFileName 模板名称
* @return 生成文件路径
* @throws Exception 异常
*/
public static String docParamHandle(Map<String, Object> dataMap,String tempPath,String tempFileName) throws Exception{
Configuration configuration = new Configuration();
configuration.setDefaultEncoding("utf-8");
//有两种方式获取你的模板,模板在项目中时用第一个,模板在本地时用第二个。
//注意:两种方式的路径都只需要写到模板的上一级目录
log.info("获取模板地址为:"+tempPath);
configuration.setDirectoryForTemplateLoading(new File(tempPath));
Writer out = null;
OutputStream outputStream = null;
String outTempFileName = "tempFile.doc";
//使用freemark处理文件参数并转化为xml格式doc文档
try {
Template t = configuration.getTemplate(tempFileName, "utf-8"); //文件名,获取模板
File outFile = new File(tempPath+outTempFileName);
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile), "utf-8")); //输出路径
t.process(dataMap, out);
} finally {
// 完毕,关闭所有链接
if(out !=null)
out.close();
if (EmptyUtil.isNotEmpty(outputStream)) {
outputStream.close();
}
}
return tempPath+outTempFileName;
}
/**
* 将xml格式的doc文件转为docx文件
* @param xmlPath 文件路径
* @return 返回生成后的文件路径
*/
public static String xmlCoverDoc(String xmlPath){
if(xmlPath.endsWith(".doc")){
//DOC文档才转换
String docxPath = xmlPath.replaceAll(".doc",".docx");
try(FileInputStream inputStream = new FileInputStream(xmlPath);){
WordprocessingMLPackage wmlPackage = Docx4J.load(inputStream);
//转换为DOCX
try(FileOutputStream docx = new FileOutputStream(docxPath);){
Docx4J.save(wmlPackage, docx, Docx4J.FLAG_SAVE_ZIP_FILE);
xmlPath = docxPath;
}
} catch (Exception e) {
e.printStackTrace();
log.error(xmlPath+":不需要转换:"+e.getLocalizedMessage());
}
}
log.info("WORD 路径:"+xmlPath);
return xmlPath;
}
public static void main(String[] args) {
xmlCoverDoc("D:/tempFile/vmsfile/export1.doc");
}
}
import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.Dispatch;
import com.lqting.framework.utils.SystemConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
/**
* @author John
* 2020年9月1日15:49:37
*/
public class PdfUtils {
private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class);
//window版本操作方式
public static void docCoverPdfWindows(String wordFile,String pdfFile){//wordFile word 的路径 //pdfFile pdf 的路径
ActiveXComponent app = null;
logger.info("开始转换...");
// 开始时间
// long start = System.currentTimeMillis();
try {
// 打开word
app = new ActiveXComponent("Word.Application");
// 获得word中所有打开的文档
Dispatch documents = app.getProperty("Documents").toDispatch();
logger.info("打开文件: " + wordFile);
// 打开文档
Dispatch document = Dispatch.call(documents, "Open", wordFile, false, true).toDispatch();
// 如果文件存在的话,不会覆盖,会直接报错,所以我们需要判断文件是否存在
File target = new File(pdfFile);
if (target.exists()) {
target.delete();
}
logger.info("另存为: " + pdfFile);
Dispatch.call(document, "SaveAs", pdfFile, 17);
// 关闭文档
Dispatch.call(document, "Close", false);
}catch(Exception e) {
logger.error("转换失败:{}",e);
throw e;
}finally {
// 关闭office
app.invoke("Quit", 0);
}
}
public static String docCoverPdf(String docxPath) throws Exception {
File file = new File(docxPath);
String parentPath = file.getParent();
String fileName = file.getName();
String outFilePath = parentPath+fileName.substring(0,fileName.lastIndexOf(".doc"))+".pdf";
try {
String environment = SystemConfig.getProperty("system.run.environment");//获取当前所处系统环境
String command = "";
if (environment.contains("windows")) {
docCoverPdfWindows(docxPath,outFilePath);
return outFilePath;
} else {
//此处的libreoffice7.0是在linux下生成的
command = "libreoffice7.0 --headless --invisible --convert-to pdf "+docxPath+" --outdir "+parentPath;
}
String result = CommandExecute.executeCommand(command);
// logger.info("result==" + result);
logger.info("生成pdf的result==" + result);
if (result.equals("") || result.contains("writer_pdf_Export")) {
return outFilePath;
}
} catch (Exception e) {
throw e;
}
return "";
}
}
此处有两个方法,第二个方法主要是为了解决doc文件在转成pdf文件时容易出现问题在pdf文件中会直接输出xml内容到pdf文件中
导出pdf所需的环境
windows环境
链接:https://pan.baidu.com/s/13RdIxmI0fXXT4HzcONaFXg.
提取码: 7ni7
windows环境直接用以上代码然后需要把配套的dll文件放在jdk的jre下bin中
liunx环境
liunx环境需要安装软件
LibreOffice_7.0.0_Linux_x86-64_rpm.tar.gz
1、配置linux下转换word文档环境
①:安装libreoffice
安装包可用 https://pan.baidu.com/s/1ADgKNWMDX-JUFKo9qOgemg.
提取码: 328b
(https://blog.csdn.net/LookingTomorrow/article/details/93632378).
步骤如下:
解压: tar -xvf LibreOffice_7.0.0_Linux_x86-64_rpm.tar.gz
安装:
cd LibreOffice_7.0.0_Linux_x86-64_rpm.tar.gz/RPMS
yum localinstall *.rpm
查看:
which libreoffice7.0 看到路径为 /usr/bin/libreoffice7.0
转换测试命令
libreoffice7.0 --headless --invisible --convert-to pdf doc文件目录 --outdir pdf放置的文件目录
安装可参考[https://blog.csdn.net/LookingTomorrow/article/details/93632378]
②导入字体
第一步:将windows下喜欢的字体文件copy到一个文件夹中,例如将XP里WINDOWS/FONTS中的字体文件在linux中命名为xpfonts
第二步:将copy到的字体文件夹copy到系统字体文件夹中并且修改权限
cp {存放xpfonts的路径}/xpfonts /usr/share/fonts/
第三步:建立字体缓存
cd /usr/share/fonts/xpfonts
mkfontscale
mkfontdir
fc-cache -fv