java使用poi-tl导出word及转换PDF后的合并导出pdf

1、背景

为某单位开发的一款项目申报审批系统,用户需求在申报阶段填写的信息资料能够导出PDF。且项目申报的报告正文为用户上传,所以需要合并导出。

2、问题

在项目初期阶段使用的是PDF的预设模板导出,因为以前使用过,比较熟悉。所以优先选择此方法,但项目测试阶段发现问题,因为某些项目的某些资料是动态的,不能确定有多少,PDF预设模板方式不够灵活,而且某些表格内容长度也是不确定的,导出效果很差。

3、解决

总体解决思路为导出word,因为有许多开源方法支持,且导出内容更灵活。满足用户数据内容长度不确定的要求。再将word转换PDF与用户上传的报告正文合并导出。

一、easypoi导出word

第一想法是想到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: 统计数据

创建模板
java使用poi-tl导出word及转换PDF后的合并导出pdf_第1张图片注意模板中合租单位信息栏处,此处有坑,后续会讲到

模板上传与代码编写

模板设置好后,将文件放在项目能访问点的地方,可以是resources文件夹下。如下
java使用poi-tl导出word及转换PDF后的合并导出pdf_第2张图片

我系统内是将文件上传oss文件服务,通过系统配置获取文件访问URL的方式,避免修改一次导出模板就得打包一次项目。

代码编写
1、数据准备
  		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());
        }
2、代码实现

这里的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导出word

通过查找,发现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>

创建模板

与easypoi类似,只是在列表遍历处不一样
java使用poi-tl导出word及转换PDF后的合并导出pdf_第3张图片

代码编写

1、数据准备

此处的数据格式与之前的数据格式一致,无需修改

2、代码编写
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数组。如果不需要下一步的处理,也可以像后文直接进行输出流返回给前端。

三、word转PDF的合并导出

此处先使用了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>

java使用poi-tl导出word及转换PDF后的合并导出pdf_第4张图片

这里的jar包是下载后放置在系统resources/lib文件夹,因为做一些其他操作,所以仓库是没有这个jar包。

链接:https://pan.baidu.com/s/1k4qEQBHf-t8rco6PSWwpiQ
提取码:1446

代码编写

1、word转PDF

使用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>
2、查找位置合并PDF

这里的合并我理解的是一页一页的拼接在上一页后面,不知道还有没有其他方式,至于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 */
                }
            }
        }

    }

你可能感兴趣的:(java,word,pdf)