最近的工作中有一个需求,需要处理word文档,有一些内容需要根据不同用户进行替换修改,使用的是word文档,替换后的内容还需要转换为pdf进行签章确认,并进行防篡改处理。
所以记录一下处理步骤,首先可以从百度文库上下载一份用于测试使用的询证函
比如这篇文章,我已经下载下来了,然后打开文档,对需要进行替换的部分使用变量占位符处理
然后将word另存为xml格式
另存的时候,最好把文件名改为英文名,防止freemark读取模板文件路径时,中文路径乱码问题
然后再将xml后缀改为ftl格式,因为freemarker模板文件的格式是ftl格式,并将模板文件放在本地目录下:
可以看到改了后缀为ftl,打开文件仍然是xml格式
接下来创建实体类,实体类属性名要与刚才在word文档中设置的占位符变量名相同,用于替换占位符为具体的数据,至此,准备工作基本上已经完成
接下来开始处理模板文件:
所需要的jar包依赖
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>15.8.0</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3。3.9</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.10</version>
</dependency>
创建实体类:
import lombok.Data;
/**
* @author xiaomifeng1010
* @version 1.0
* @date: 2021/11/27 11:56
* @Description
*/
@Data
public class AssetsReconciliation {
private String enterpriseName;
private String agencyName;
private String address;
private String amount;
/**
* 邮编
*/
private String postcode;
private String phone;
private String fax;
private String linkman;
private String year;
private String month;
private String day;
private String date;
/**
* 资产
*/
private String assets;
/**
* 负债
*/
private String liabilities;
/**
* 备注
*/
private String content;
/**
* 签章
*/
private String sign;
/**
* 经办人
*/
private String operator;
}
创建处理工具类:
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.setting.dialect.Props;
import cn.hutool.system.OsInfo;
import com.aspose.cells.License;
import com.aspose.words.Document;
import com.aspose.words.FontSettings;
import com.aspose.words.SaveFormat;
import com.xiaomifeng1010.tmc.newenergy.word.bo.AssetsReconciliation;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* @author xiaomifeng1010
* @version 1.0
* @date: 2021/11/27 10:20
* @Description
*/
@Slf4j
public class ConvertWordToPDFUtil {
private static Configuration cfg;
/**
* word模板文件所在目录路径
*/
private static Props props=new Props("pdf.properties");
private static String templateFileDirPath=props.getProperty("template.path");
private static String docFilePath=props.getProperty("word.path");
private static boolean initSuccess = true;
static{
// 初始化freemarker配置
try {
cfg=new Configuration(Configuration.VERSION_2_3_22);
// 加载模板所在目录(从pdf.properties文件中可以看出,模板文件是在本地存放的)
// 当然也可以把ftl模板文件放在resources目录下,这样的话,就需要获取classpath下的目录
cfg.setDirectoryForTemplateLoading(new File(templateFileDirPath));
cfg.setDefaultEncoding(CharsetUtil.UTF_8);
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
} catch (IOException e) {
log.error("freemarker初始化配置出错",e);
initSuccess=false;
}
}
/**
* 获取license 去除水印
*
* @return
*/
public static boolean getLicense() {
boolean result = false;
try {
InputStream is = ConvertWordToPDFUtil.class.getClassLoader().getResourceAsStream("\\license.xml");
License aposeLic = new License();
aposeLic.setLicense(is);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
*使用freemarker处理word模板
* @return
*/
public static String handleWordTemplate(AssetsReconciliation assetsReconciliation){
String docpath=docFilePath+System.currentTimeMillis()+".docx";
try (Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(docpath), StandardCharsets.UTF_8))) {
String fileName="template_test.ftl";
// 获取模板
Template template = cfg.getTemplate(fileName, CharsetUtil.UTF_8);
// 往模板中填充内容,并保存为docx文件
template.process(assetsReconciliation,out);
} catch (Exception e) {
log.error("模板填充内容失败!", e);
return StringUtils.EMPTY;
}
return docpath;
}
/**
* word 转 pdf
*
* @param inputPath word文件path
* @param outPath pdf文件path
* @return
*/
public static Boolean word2pdf(String inputPath, String outPath) {
OsInfo osInfo = new OsInfo();
// linux平台下需要安装字体样式,从window下拷贝过去安装即可
if (osInfo.isLinux()) {
FontSettings.setFontsFolder(File.separator + "usr" + File.separator + "share" + File.separator + "fonts", true);
} else if (osInfo.isMac()) {
FontSettings.setFontsFolder("/Library/Fonts/Microsoft", true);
}
try (FileOutputStream os = new FileOutputStream(outPath)) {
if (getLicense()) {
long start = System.currentTimeMillis();
Document doc = new Document(inputPath);
doc.save(os, SaveFormat.PDF);
long end = System.currentTimeMillis();
log.info("转换成功, 花费 " + (end - start) / 1000.0 + " seconds!");
return true;
}
return false;
} catch (Exception e) {
log.error("转换报错!", e);
return false;
}
}
}
pdf.properties放在项目的resources目录下:
以及license.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<License>
<Data>
<Products>
<Product>Aspose.Total for Java</Product>
<Product>Aspose.Words for Java</Product>
</Products>
<EditionType>Enterprise</EditionType>
<SubscriptionExpiry>20991231</SubscriptionExpiry>
<LicenseExpiry>20991231</LicenseExpiry>
<SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>
</Data>
<Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature>
</License>
然后写一个controller类请求方法测试一下
import cn.hutool.core.date.DateUtil;
import com.xiaomifeng1010.tmc.newenergy.word.bo.AssetsReconciliation;
import com.xiaomifeng1010.tmc.newenergy.word.util.ConvertWordToPDFUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author xiaomifeng1010
* @version 1.0
* @date: 2021/11/27 12:53
* @Description
*/
@Api(value = "处理word文档",tags = {"处理word文档"})
@ApiSort(26)
@RestController
@RequestMapping("/word")
public class WordTestController {
@ApiOperation("测试填充word模板并转换为pdf")
@GetMapping("/convertTest")
public String testConvertExcelToPDF(){
AssetsReconciliation assetsReconciliation = new AssetsReconciliation();
assetsReconciliation.setAddress("广东省广州市白云区人和镇999路233号");
assetsReconciliation.setAmount("¥2,456,987.05");
assetsReconciliation.setAssets("¥1,345,675.45");
assetsReconciliation.setAgencyName("非凡科技有限公司");
assetsReconciliation.setContent("无异议");
assetsReconciliation.setDate(DateUtil.now());
assetsReconciliation.setEnterpriseName("天涯科技有限公司");
assetsReconciliation.setFax("010-3458-450");
assetsReconciliation.setLiabilities("¥45,769.56");
assetsReconciliation.setYear("2021");
assetsReconciliation.setMonth("11");
assetsReconciliation.setDay("27");
assetsReconciliation.setLinkman("小明");
assetsReconciliation.setOperator("ablert");
assetsReconciliation.setPhone("13576890543");
assetsReconciliation.setPostcode("443900");
assetsReconciliation.setSign("假装有印章");
String docPath = ConvertWordToPDFUtil.handleWordTemplate(assetsReconciliation);
if (StringUtils.isNotEmpty(docPath)) {
String pdfFileName="C:\\Users\\MSI\\Desktop\\template\\"+ RandomStringUtils.randomAlphanumeric(6) + ".pdf";
ConvertWordToPDFUtil.word2pdf(docPath,pdfFileName);
return "填充word模板成功,并转为pdf成功";
}
return "处理失败!";
}
}
然后再knife4j接口文档界面发起请求
填充内容后的word文档:
注意:使用freemarker处理之后的word文档用microsoft word是打不开的,不知道为什么,但是可以用金山WPS 文字软件打开,效果如上
然后再看一下转换为pdf后的效果:
样式和word文档样式是一样的,这就是为什么不建议使用apache poi去处理word文档,再用itext生成pdf的原因。poi可以读取到word文档的内容,但是读取出来后,会读成为字符串了,样式都丢失了,再用itext去保存为pdf的时候,因为样式丢失,整体布局是错乱的。
注意事项:我在用freemark处理word转换之后的ftl模板时遇到了一些麻烦问题,现在也总结一下
doc格式的文档,在一些需要填充数据的地方,写上了EL表达式${变量名},但是在另存为xml格式的时候,一些EL表达式变量,会被拆分开,这时候,freemarker在解析模板的时候就会报错:
Encountered "<", but was expecting one of:
<STRING_LITERAL>
<RAW_STRING>
"false"
"true"
<INTEGER>
<DECIMAL>
"."
"+"
"-"
"!"
"["
"("
"{"
<ID>
此时需要打开保存的xml文件
可以看到原本设置的${enterpriseName}被拆分开成了好几部分
而正常的应该是这样的
应该是整体在一起的,所以呢需要进行手动修改xml文件,是正哥EL表达式是整体在一起的
这还没完,注意下边还有一个 <w:proofErr w:type="gramStart"/>标签,包括刚才删除的那一部分,也有<w:proofErr w:type="spellStart"/>注意都有一个proofErr的属性,说明这个标签是提示错误的,这部分里边的内容有错误
所以,列信息那几个字也得处理一下,把列信息三个字移到下边,然后删除这个标签
下边的agencyName出错与enterpriseName出错一致,处理一样
这样的话,语法问题就解决了,freemark就可以正常解析EL表达式了。还有一个问题就是在创建实体类的时候,最开始的时候,实体类创建的时候少了一个属性amount,但是模板文件中是有这个EL表达式的
但是创建实体类时候,没加上这个属性,填充模板内容的时候也报错了
Caused by: freemarker.core.InvalidReferenceException: The following has evaluated to null or missing: ==> amount in
所以在测试时候,设置AssetsReconciliation时候,就没有设置amount的值,在填充模板时候,${amount}就为null,就报上边这个异常,所以后来在实体类上加上了这个属性,并赋值,就正常了
或者修改模板文件,添加判空的判断,给个默认值
在ftl文件中修改 ${(amount)!' '} 或者${amount!" "} 如果为空,就以默认值(“!”后的字符)显示;
还有就是如果同时使用apose-words处理word,有用apose-cells处理excel的话,需要同时分开用两个工具类或配置文件去分别获取对应的license,不能共用一个工具类去获取license,不然总会有一个工具处理后带水印(处理word转pdf之后,或者excel转pdf后带水印)