目录
一、 前言
在实际开发中,经常会遇到根据给定模板生成Word文档的需求。大体操作是维护一份模板,再通过给这份模板中的变量赋予指定的值,最后调用转换接口,实现Word文档的生成。
本文主要讲述的是通过Freemarker生成Word文档。包括生成doc文档和docx文档。
二、什么是Freemarker?
FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
FreeMarker 是 免费的, 基于Apache许可证2.0版本发布。
附个链接:Freemarker中文官方参考手册
三、 生成doc文档
1、模板维护
Word 另存为 xml文件,将模板中需要生成值的地方用${变量名} 来替换。替换好后,将文件 重命名 为ftl(如freemarker.ftl)。
注意事项:直接在Word中替换变量时,后期转成ftl文件变量可能会被拆开。因此此处先将文件另存为xml格式,然后用word文档打开,然后编辑变量,这样可避免这种问题。当然以防万一,编辑完后可以再次校验一下生成的模板是否正确。
2、模板存放位置
1)可以存放本地任意位置,如 /users/xymar/docs
2)存放项目路径(推荐),如放在resources目录的templates文件夹下:
3、实现
public static void test(String path){
Map dataMap = new HashMap();
try {
dataMap.put("cardId", "666666");
dataMap.put("patientName", "张三");
dataMap.put("cardAttachType", "1");
// Map<String, String> dataMap = new HashMap<>(CardCode.codeMap);
Configuration configuration = new Configuration(new Version("2.3.0"));
configuration.setDefaultEncoding("utf-8");
//.ftl配置文件所在路径
//第一种使用相对一个java文件的相对路径
configuration.setClassForTemplateLoading(FreemarkerUtil.class, "/templates");
//第二种使用绝对路径
// configuration.setDirectoryForTemplateLoading(new File("/users/xymar/docs"));
//以utf-8的编码读取ftl模板文件
Template template = configuration.getTemplate("freemarker.ftl", "utf-8");
//输出文档路径及名称
File outFile = new File(path);
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile),
"utf-8"), 10240);
template.process(dataMap, out);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
可以根据自己情况替换map的值,也可结合实际将接口进行封装。
四、生成docx文档及转换成PDF
有时我们生成的Word文档会需要转换成PDF,这时生成docx格式更方便转换。此处采用docx4j方式生成docx的word。
1、模板维护
模板填入变量后(变量命名为${变量名}),将模板复制一份,后缀改为zip。
2、所需文件(2个)
拷贝出zip包里word路径下的document.xml文件,这个就是word的主内容文件。
维护好的模板(docx文件)和拷贝出的document.xml这两个文件是我们所需要用到的两个模板文件,拷贝到项目工程里面去。
3、依赖
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.4.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.docx4j/docx4j -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j</artifactId>
<version>6.1.2</version>
</dependency>
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-export-fo</artifactId>
<version>8.1.6</version>
</dependency>
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-core</artifactId>
<version>8.1.7</version>
</dependency>
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
<version>8.1.7</version>
</dependency>
4、实现
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import org.docx4j.Docx4J;
import org.docx4j.convert.out.FOSettings;
import org.docx4j.fonts.IdentityPlusMapper;
import org.docx4j.fonts.Mapper;
import org.docx4j.fonts.PhysicalFonts;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.springframework.core.io.ClassPathResource;
import java.io.*;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public class PdfUtil {
private static String separator = File.separator;//文件夹路径分格符
//=========================================生成申请表pdf===================================
/**
* freemark生成word----docx格式
* @param dataMap 数据源
* @param documentXmlName document.xml模板的文件名
* @param docxTempName docx模板的文件名
* @return 生成的文件路径
*/
public static String createApplyPdf(Map<String,Object> dataMap,String documentXmlName,String docxTempName) {
ZipOutputStream zipout = null;//word输出流
File tempPath = null;//docx格式的word文件路径
try {
//freemark根据模板生成内容xml
//================================获取 document.xml 输入流================================
ByteArrayInputStream documentInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, documentXmlName, separator + "template" + separator + "downLoad" + separator);
//================================获取 document.xml 输入流================================
//获取主模板docx
ClassPathResource resource = new ClassPathResource("template" + File.separator + "downLoad" + File.separator + docxTempName);
File docxFile = resource.getFile();
ZipFile zipFile = new ZipFile(docxFile);
Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
//输出word文件路径和名称
String fileName = "applyWord_" + System.currentTimeMillis() + ".docx";
String outPutWordPath = System.getProperty("java.io.tmpdir").replaceAll(separator + "$", "") + separator + fileName;
tempPath = new File(outPutWordPath);
//如果输出目标文件夹不存在,则创建
if (!tempPath.getParentFile().exists()) {
tempPath.mkdirs();
}
//docx文件输出流
zipout = new ZipOutputStream(new FileOutputStream(tempPath));
//循环遍历主模板docx文件,替换掉主内容区,也就是上面获取的document.xml的内容
//------------------覆盖文档------------------
int len = -1;
byte[] buffer = new byte[1024];
while (zipEntrys.hasMoreElements()) {
ZipEntry next = zipEntrys.nextElement();
InputStream is = zipFile.getInputStream(next);
if (next.toString().indexOf("media") < 0) {
zipout.putNextEntry(new ZipEntry(next.getName()));
if ("word/document.xml".equals(next.getName())) {
//写入填充数据后的主数据信息
if (documentInput != null) {
while ((len = documentInput.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
documentInput.close();
}
}else {//不是主数据区的都用主模板的
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}
}
//------------------覆盖文档------------------
zipout.close();//关闭
//----------------word转pdf--------------
return convertDocx2Pdf(outPutWordPath);
} catch (Exception e) {
e.printStackTrace();
try {
if(zipout!=null){
zipout.close();
}
}catch (Exception ex){
ex.printStackTrace();
}
}
return "";
}
/**
* word(docx)转pdf
* @param wordPath docx文件路径
* @return 生成的带水印的pdf路径
*/
public static String convertDocx2Pdf(String id, String wordPath) {
String regex=".*(Courier New|Arial|Times New Roman|Comic Sans|Georgia|Impact|Lucida Console|Lucida Sans Unicode|Palatino Linotype|Tahoma|Trebuchet|Verdana|Symbol|Webdings|Wingdings|Wingdings 2|MS Sans Serif|MS Serif).*";
Date startDate = new Date();
// String regex = null;
PhysicalFonts.setRegex(regex);
String fileKey = UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();
OutputStream os = null;
InputStream is = null;
try {
is = new FileInputStream(new File(wordPath));
WordprocessingMLPackage mlPackage = WordprocessingMLPackage.load(is);
Mapper fontMapper = new IdentityPlusMapper();
PhysicalFonts.addPhysicalFonts("SimSun", FreemarkerUtil.class.getResource("/data/simsun.ttc"));
// fontMapper.put("Helvetica", PhysicalFonts.get("SimSun"));
fontMapper.put("宋体", PhysicalFonts.get("SimSun"));
fontMapper.put("宋体 (中文正文)", PhysicalFonts.get("SimSun"));
PhysicalFonts.addPhysicalFonts("Wingdings 2", FreemarkerUtil.class.getResource("/data/Wingdings 2.ttf"));
fontMapper.put("Wingdings 2", PhysicalFonts.get("Wingdings 2"));
//解决宋体(正文)和宋体(标题)的乱码问题
PhysicalFonts.put("PMingLiU", PhysicalFonts.get("SimSun"));
PhysicalFonts.put("新細明體", PhysicalFonts.get("SimSun"));
mlPackage.setFontMapper(fontMapper);
//输出pdf文件路径和名称
String fileName = "pdfNoMark_" + System.currentTimeMillis() + ".pdf";
// String pdfNoMarkPath = System.getProperty("java.io.tmpdir").replaceAll(separator + "$", "") + separator + fileName;
String pdfNoMarkPath = "/tmp/" + fileName;
os = new java.io.FileOutputStream(pdfNoMarkPath);
//docx4j docx转pdf
FOSettings foSettings = Docx4J.createFOSettings();
foSettings.setWmlPackage(mlPackage);
Docx4J.toFO(foSettings, os, Docx4J.FLAG_EXPORT_PREFER_XSL);
is.close();//关闭输入流
os.close();//关闭输出流
//添加水印
return fileKey;
} catch (Exception e) {
e.printStackTrace();
try {
if(is != null){
is.close();
}
if(os != null){
os.close();
}
}catch (Exception ex){
ex.printStackTrace();
}
}finally {
File file = new File(wordPath);
if(file!=null&&file.isFile()&&file.exists()){
file.delete();
}
}
return "";
}
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.*;
import java.util.Map;
/**
* 获取freemarker模板字符串
*/
public class FreeMarkUtils {
/**
* 获取模板字符串输入流
* @param dataMap 参数
* @param templateName 模板名称
* @param tempPath 模板路径 classes下的路径 如果 classes/templates 传入 /templates即可
* @return
*/
public static ByteArrayInputStream getFreemarkerContentInputStream(Map dataMap, String templateName, String tempPath) {
ByteArrayInputStream in = null;
try {
//创建配置实例
Configuration configuration = new Configuration();
//设置编码
configuration.setDefaultEncoding("UTF-8");
//ftl模板文件统一放至 com.lun.template 包下面
// configuration.setDirectoryForTemplateLoading(new File("D:/idea_workspace/alarm/alarm/src/main/resources/template/"));
configuration.setClassForTemplateLoading(FreeMarkUtils.class, tempPath);
//获取模板
Template template = configuration.getTemplate(templateName);
StringWriter swriter = new StringWriter();
//生成文件
template.process(dataMap, swriter);
String result = swriter.toString();
in = new ByteArrayInputStream(swriter.toString().getBytes());
} catch (Exception e) {
e.printStackTrace();
}
return in;
}
}
五、以下是我过程中遇到的问题(欢迎补充):
1)转换成的pdf是xml文件
原因:这是因为之前用freemarker生成word的时候是生成doc格式的,是将模板文件保存为xml格式再生成word,所以生成出来的其实是XML文件。即使在生成的时候将文件后缀名改为docx,生成出来的文件也是打不开的。
解决方案:docx本质上是个压缩包,里面有包含word主要内容的document.xml文件、资源定义文件document.xml.rels、还有页眉页脚文件、图片资源等等,解决思路是替换内容区的document.xml文件。
2)生成的pdf中文乱码(全是#)
原因: 模板中的字体不被识别。
解决方案:维护模板时设置可被识别的字体,如果还是不能识别,可以在网上下载字体,并将字体文件放到项目路径下,同时接口调用时引入一下此文件即可。
3)生成的pdf特殊符号不识别(全是#)
原因: word有些特有的符号,如windings 2 等等
解决方案:如果也遇到不能识别的情况,同样的下载此符号的字体并引入即可。
4)Word中的特殊字符如何获取
参考方案:https://www.cnblogs.com/NieXiaoHui/p/7146898.html
六、参考链接
https://blog.youkuaiyun.com/qq_34908167/article/details/102784375
https://www.cnblogs.com/zhouyun-yx/p/11211354.html