Java Freemarker 生成word文档; docx4j Word转PDF

一、 前言

在实际开发中,经常会遇到根据给定模板生成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.csdn.net/qq_34908167/article/details/102784375
https://www.cnblogs.com/zhouyun-yx/p/11211354.html

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值