为某单位开发的一款项目申报审批系统,用户需求在申报阶段填写的信息资料能够导出PDF。且项目申报的报告正文为用户上传,所以需要合并导出。
在项目初期阶段使用的是PDF的预设模板导出,因为以前使用过,比较熟悉。所以优先选择此方法,但项目测试阶段发现问题,因为某些项目的某些资料是动态的,不能确定有多少,PDF预设模板方式不够灵活,而且某些表格内容长度也是不确定的,导出效果很差。
总体解决思路为导出word,因为有许多开源方法支持,且导出内容更灵活。满足用户数据内容长度不确定的要求。再将word转换PDF与用户上传的报告正文合并导出。
第一想法是想到easypoi导出word的方式。easypoi是对poi的二次封装,使得poi的多数功能得以简单实现,让许多没有接触过poi的开发者也能实现对Excel,word的导出。通过简单的注解和表达式语法实现导出功能。
引入依赖
GItHub地址
教程地址
<!-- easyPOi-->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.2.0</version>
</dependency>
指令 | 作用 |
---|---|
{{test ? obj:obj2}} | 三元运算 |
n: | 表示 这个cell是数值类型 {{n:}} |
le: | 代表长度{{le:()}} 在if/else 运用{{le:() > 8 ? obj1 : obj2}} |
fd: | 格式化时间 {{fd:(obj;yyyy-MM-dd)}} |
fn: | 格式化数字 {{fn:(obj;###.00)}} |
fe: | 遍历数据,创建row |
!fe: | 遍历数据不创建row |
$fe: | 下移插入,把当前行,下面的行全部下移.size()行,然后插入 |
#fe: | 横向遍历 |
v_fe: | 横向遍历值 |
!if: | 删除当前列 {{!if:(test)}} |
‘’ | 单引号表示常量值 ‘’ 比如’1’ 那么输出的就是 1 |
&NULL& | 空格 |
]] | 换行符 多行遍历导出 |
sum: | 统计数据 |
模板设置好后,将文件放在项目能访问点的地方,可以是resources文件夹下。如下
我系统内是将文件上传oss文件服务,通过系统配置获取文件访问URL的方式,避免修改一次导出模板就得打包一次项目。
dataMap.put("birth", PdfUtils.getDateMonth(vo.getSysUser().getBirthday()));//申请者出生年月
dataMap.put("applicationPhone", vo.getSysUser().getPhone() == null ? "" : vo.getSysUser().getPhone());//申请者手机号码
dataMap.put("applicationEmail", vo.getSysUser().getEmail() == null ? "" : vo.getSysUser().getEmail());//申请者电子邮件
dataMap.put("companyName", vo.getDepart().getDepartName() == null ? "" : vo.getDepart().getDepartName());//申请单位--名称
dataMap.put("companyPeople", vo.getDepart().getPeople() == null ? "" : vo.getDepart().getPeople());//申请单位-联系人
dataMap.put("companyPhone", vo.getDepart().getMobile() == null ? "" : vo.getDepart().getMobile());//申请单位--手机
dataMap.put("companyEmail", vo.getDepart().getEmail() == null ? "" : vo.getDepart().getEmail());//申请单位--电子邮件
if (vo.getUnits().size() > 0) {
dataMap.put("coorList",vo.getUnits());
}
这里的word生成是用了一个临时文件夹进行保存。使用easypoi工具类下的方法exportWord07()实现word的数据填充,方法前一个参数为模板URL,后一个为map的数据内容。因为我这里使用的oss文件存储,直接使用URL会获取不到文件。所以使用了工具类通过URL获取File。代码贴下面。
//临时文件夹路径
String filename = (String) dataMap.get("fileName");
//word导出模板
String url = wordUrl;
File templateFile = UrlFilesToZip.Url2File(url);
//获取模板文档
//File templateFile = new File(url);
//2.映射为模板
XWPFDocument xwpfDocument = null;
xwpfDocument = WordExportUtil.exportWord07(templateFile.getPath(), dataMap);
//删除
File file = new File(filename);
for (File listFile : file.listFiles()) {
listFile.delete();
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
FileOutputStream outputStream = new FileOutputStream(filename + "/test.docx");
xwpfDocument.write(outputStream);
通过URL获取网络文件
public static File Url2File(String url) throws Exception {
//对本地文件命名,可以从链接截取,可以自己手写,看需求
String fileName = "fileName";
// String fileName = url.substring(url.lastIndexOf("."));
File file = null;
URL urlfile;
InputStream inStream = null;
OutputStream os = null;
file = File.createTempFile("net_url", fileName);
//下载
urlfile = new URL(url);
inStream = urlfile.openStream();
os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = inStream.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
return file;
}
这里还没进行word转PDF的代码编写,就发现此方法的不足之处。在上面提到过合作单位信息栏。如果表格的第一列是固定的不进行遍历,而在第二列开始遍历插入。easypoi的原始方法是不满足需求的。查询后有方式可以通过修改源码实现,但没尝试,感觉麻烦,且easypoi对于富文本内容导出的处理不够完美,只能通过自己找方法实现处理。尝试过后仍然有部分样式无法保留。所以弃用此方式。
通过查找,发现poi-tl的开源类库,也是基于poi开发,且对word导出的支持更好,对于导出的方式与easypoi相同,减少了关于数据准备阶段的代码修改。使用方法在参考文档中也有很多例子。
开发参考文档
注意版本对应,不然会出问题
<!-- POI -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
<!-- poi-tl -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.10.0</version>
</dependency>
<!--poi-tl 富文本插件-->
<dependency>
<groupId>io.github.draco1023</groupId>
<artifactId>poi-tl-ext</artifactId>
<version>0.4.2</version>
</dependency>
此处的数据格式与之前的数据格式一致,无需修改
public byte[] exportWordByPOi_tl(Map dataMap, String wordPath)throws Exception{
long start = System.currentTimeMillis();
// wordPath = "D:/Test/poi-tl/青年科学基金项目申请书导出模板.docx";
// wordPath = "https://guizhou-keyan-oss.oss-cn-hangzhou.aliyuncs.com/temp/博士基金项目申请书导出模板_1676885802664.docx";
//绑定行循环
LoopRowTableRenderPolicy policy=new LoopRowTableRenderPolicy();
//富文本插件
HtmlRenderPolicy htmlRenderPolicy = new HtmlRenderPolicy();
Configure configure = Configure.builder().bind("coorList", policy).bind("peopleList", policy).bind("peopleSingList", policy)
.bind("fileList", policy).bind("allotList",policy).bind("equipmentList",policy).bind("budget_state",htmlRenderPolicy).build();
//通过url获取网络文件输入流
InputStream inputStream = POICacheManager.getFile(wordPath);
XWPFTemplate render = XWPFTemplate.compile(inputStream,configure).render(dataMap);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
render.write(byteArrayOutputStream);
log.info("导出word消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
render.close();
byteArrayOutputStream.close();
return byteArrayOutputStream.toByteArray();
}
行循环与富文本内容处理都使用了插件,所以代码中添加了二者的写法。poi-tl类库中方法XWPFTemplate.compile不支持模板的网络url使用,所以使用了easypoi中获取网络文件输入流方法,也可以自己写一个工具实现。此处便于下一步的word转PDF,所以返回了byte数组。如果不需要下一步的处理,也可以像后文直接进行输出流返回给前端。
此处先使用了poi的word转PDF,虽然此方法使用率很高,但是效果实在不怎么样。后来看到一个横向对比的文章
Java开发中Word转PDF文件5种方案横向评测
对比中aspose与spire的转换效果最好,本着互联网精神,最后选择aspose的方式。
<!--aspose 破解 word转pdf-->
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/aspose-words-16.8.0-jdk16.jar</systemPath>
</dependency>
这里的jar包是下载后放置在系统resources/lib文件夹,因为做一些其他操作,所以仓库是没有这个jar包。
链接:https://pan.baidu.com/s/1k4qEQBHf-t8rco6PSWwpiQ
提取码:1446
使用aspose转换需要进行验证
public byte[] asposeWord2Pdf(InputStream inputStream) {
if (!getLicense()) { // 验证License 若不验证则转化出的pdf文档会有水印产生
return null;
}
if (inputStream.equals(null)) {
log.info("word为null");
return null;
}
try {
long old = System.currentTimeMillis();
ByteArrayOutputStream os = new ByteArrayOutputStream();
com.aspose.words.Document doc = new com.aspose.words.Document(inputStream); //Address是将要被转化的word文档
doc.save(os, SaveFormat.PDF);//全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF, EPUB, XPS, SWF 相互转换
os.close();
log.info("word2pdf消耗" + (System.currentTimeMillis() - old) + "毫秒");
return os.toByteArray();
} catch (Exception e) {
log.info("doc转pdf文件失败,", e);
return null;
}
}
license验证
public boolean getLicense() {
boolean result = false;
try {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("wordPath/license.xml");
License aposeLic = new License();
aposeLic.setLicense(resourceAsStream);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
license内容,放置于resources/wordPath
<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>
这里的合并我理解的是一页一页的拼接在上一页后面,不知道还有没有其他方式,至于PDF查找那一块现在我有点困,不想写了。还有俩小时下班,有人需要再补吧。合并后通过Response给前端。
/**
* 合并pdf 关键词查找合并的位置
* @Author CoCo
* @Date 2023/2/16
* @params
* @return
*/
public void mergePdf(HttpServletResponse response, byte[] pdfByte, String textUrl,String keyWord) {
response.reset();
response.setContentType("application/pdf");
//response.setContentType("content-type:octet-stream;charset=UTF-8");
try {
response.setHeader("Content-Disposition", "attachment;filename=" + new String(("filename").getBytes(), "iso-8859-1"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
PdfUtils pdfUtils=new PdfUtils();
com.itextpdf.text.Document document = null;
PdfCopy copy = null;
OutputStream os = null;
boolean empty = textUrl != null;
try {
os = response.getOutputStream();
// File file = new File("D:/Test/aspose.pdf"); //新建一个pdf文档
// FileOutputStream oss = new FileOutputStream(file);
document = new com.itextpdf.text.Document(new PdfReader(pdfByte).getPageSize(1));
copy = new PdfCopy(document, os);
document.open();
PdfReader pdfReader = new PdfReader(pdfByte);
PdfReader textReader = null;
for (int i = 1; i <= pdfReader.getNumberOfPages(); i++) {
PdfImportedPage pdfPage = copy.getImportedPage(pdfReader, i);
//根据关键字查询页数及其他信息
List list = pdfUtils.matchPage(pdfReader, i, keyWord);
//如果PDF关键字查找有返回数据且正文url不为空,拼接正文pdf
if (!list.isEmpty()&&empty){
textReader = new PdfReader(UrlFilesToZip.getFileFromURL(textUrl));
for (int k = 1; k <= textReader.getNumberOfPages(); k++) {
PdfImportedPage textPage = copy.getImportedPage(textReader,k);
copy.addPage(textPage);
}
}
copy.addPage(pdfPage);
}
} catch (Exception e) {
e.printStackTrace();
log.info("合并失败");
}
finally {
if (copy != null) {
try {
copy.close();
} catch (Exception ex) {
/* ignore */
}
}
if (document != null) {
try {
document.close();
} catch (Exception ex) {
/* ignore */
}
}
if (os != null) {
try {
os.close();
} catch (Exception ex) {
/* ignore */
}
}
}
}