使用freemarker引擎动态生成word文件

  • 0 前言
  • 1 工具与环境配置
  • 1.1 sublime
  • 1.2 依赖包
  • 1.3 word模版ftl文件生成步骤
  • 2 Word Xml基本语法介绍
  • 3 动态数据填充
  • 单元测试
  • ftl模板
  • 4 动态表格生成
  • 单元测试
  • ftl模板
  • 5 动态目录与动态标题结构
  • 单元测试
  • ftl模板
  • 6 插入图片
  • 单元测试
  • ftl模板


0 前言

本文使用freemarker模板引擎动态建立word文件,可以实现数据填充,文档结构和目录的变化,表格生成等功能,使用WPS2019,jdk版本1.8.0_141,maven版本3.1.1,freemarker版本2.3.23,idea版本Community Edition 2018.1.1。

1 工具与环境配置

1.1 sublime

安装sublime,选择Preferences–>Package Control–>Install Package–>安装HTML/CSS/JS Prettify。

1.2 依赖包

以maven工程为例,新建项目,在pom.xml文件中引入freemarker、junit、lombok依赖

<dependency>
	<groupId>freemarker</groupId>
	<artifactId>freemarker</artifactId>
	<version>2.3.23</version>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.8</version>
</dependency>

1.3 word模版ftl文件生成步骤

freemarker使用专门的模板文件ftl(FreeMarker Template Language)格式,为了快速建立word模板文件,可以采用反编译的方式。

  1. 新建一个doc格式的word文档;
  2. 另存为xml格式文件,注意并非word xml文件;
  3. 修改后缀名为ftl;
  4. 使用sublime打开和编辑,在空白处右键,HTML/CSS/JS Prettify–>Prettify Code可以格式化代码。

2 Word Xml基本语法介绍

ftl文件采用word xml格式的语法。以这种格式存储的文件可以通过其他软件的编译创建word文档,不需要用专门的文稿软件打开。一个简单ftl文件如下所示

<?xml version="1.0"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml">
   <w:body>
       <w:p>
           <w:r>
               <w:t>tyler</w:t>
           </w:r>
       </w:p>
   </w:body>
</w:wordDocument>

文档内容包含在<w:body>标签之间。<w:p><w:r><w:t>是三个基本标签。
<w:p>标签表示一个段落,段落属性用<w:pPr>来指定,一个段落中可以包含很多“执行段”<w:r>。在执行段中,包含文本、制表符、回车符等特殊符号。<w:rPr>可以指定该执行段的属性。<w:t>之间包含真实的文本。
参考资料officeopenxml

3 动态数据填充

很多时候,人们会制定一些模板,如会议通知等,仅在会议时间、地点处留出空白,这样下次使用的时候只需要填写时间、地点就可以快速生成一个会议通知。因此我们首先实现文档内数据内容的动态填充。新建maven工程,工程目录如下

freemark 行列动态对应的table freemarker动态生成word表格_microsoft

建立word工具类WordUtil,按照路径读取已有的ftl模板

import freemarker.cache.FileTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.*;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class WordUtil {
    private Configuration configure;

    public WordUtil() {
        configure = new Configuration(Configuration.VERSION_2_3_23);
        configure.setDefaultEncoding("utf-8");
    }

    public boolean createWord(Map<String, Object> dataMap, String inputFilePath, String outFilePath) {
        boolean flag = false;

        try {
            TemplateLoader templateLoader;
            templateLoader=new FileTemplateLoader(new File("./"));
            configure.setTemplateLoader(templateLoader);
            Template template=configure.getTemplate(inputFilePath,"UTF-8");
            Writer out = new BufferedWriter(
            	new OutputStreamWriter(
            		new FileOutputStream(
            			new File(outFilePath)
            		), "utf-8"
            	), 10240
            );
            template.process(dataMap, out);
            out.close();
            flag = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return flag;
    }
}

建立Person模型

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class Person {
    String name;
    String age;
    String gender;

    public Person(String name, String age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
}

单元测试

建立单元测试,调用WordUtil的createWord方法。其入参为存储待填充的数据的map、ftl的存储路径、生成文档的路径。文件名以时间戳命名,避免每次生成文件覆盖。

import com.tyler.freemarker.model.Person;
import com.tyler.freemarker.WordUtil;
import org.junit.Test;
import java.text.SimpleDateFormat;
import java.util.*;

public class WordGenerateTest {

    @Test
    public void variableTest() {

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        Date date = new Date(System.currentTimeMillis());
        String outputFileName = formatter.format(date);

        String inputFilePath = "./src/main/resources/template.ftl";
        String outFilePath = "./src/main/resources/"+outputFileName+".doc";

        WordUtil wu = new WordUtil();
        Map<String, Object> map = new HashMap<>();
        Person person = new Person("Tyler", "18", "male");

        map.put("person", person);
        wu.createWord(map, inputFilePath, outFilePath);

    }
}

ftl模板

ftl模板中留出了Person的属性的位置,可以通过读取map的数据填充进去。在freemarker中,${variable}可以获取变量variable的值,person是传进来Person类实例的一个key值,通过key值访问到这个实例。和Java一样,类的属性可以通过.操作符获取。这样就建立了Java类与ftl中数据的映射,实现了对同一个模板的复用,给定不同的数据可以生成很多相同格式的word文档。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml" 
	xmlns:v="urn:schemas-microsoft-com:vml" 
	xmlns:w10="urn:schemas-microsoft-com:office:word" 
	xmlns:sl="http://schemas.microsoft.com/schemaLibrary/2003/core" 
	xmlns:aml="http://schemas.microsoft.com/aml/2001/core" 
	xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint" 
	xmlns:o="urn:schemas-microsoft-com:office:office" 
	xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" 
	w:macrosPresent="no" w:embeddedObjPresent="no" 
	w:ocxPresent="no" xml:space="preserve">
    <w:body>
	    <w:p>
	        <w:r>
	            <w:t>姓名:${person.name}</w:t>
	        </w:r>
	    </w:p>
	    <w:p>
	        <w:r>
	            <w:t>年龄:${person.age}</w:t>
	        </w:r>
	    </w:p>
	    <w:p>
	        <w:r>
	            <w:t>性别:${person.gender}</w:t>
	        </w:r>
	    </w:p>
    </w:body>
</w:wordDocument>

生成结果展示

freemark 行列动态对应的table freemarker动态生成word表格_单元测试_02

4 动态表格生成

表格是数据展示的重要方式。在可以填充数据的基础上,可以将这些数据以表格的形式展示。比如用一个表格统计公司员工的基本信息,姓名、年龄、性别。只需要在map中存储一个List<Person>,再设计一个与之对应的表格并留出空白就好。
但是另一个公司也想用这套模板,但是两个公司的员工数量不同,因此需要修改表格的行数。接下来实现表格的动态生成,行数与List的尺寸对应。这就需要用到循环语句。
freemarker可以设定计数循环,如

<#list 1..5 as i>
	<w:t>${i}</w:t>
</#list>

也可以直接循环List的内容

<#list personList as pl>
	<w:t>${pl.name}</w:t>
</#list>

此处选择第二种,直接循环personList的元素。建立单元测试,生成一个List<Person>作为数据传输给ftl模板。

单元测试

import com.tyler.freemarker.model.Person;
import com.tyler.freemarker.WordUtil;
import org.junit.Test;
import java.text.SimpleDateFormat;
import java.util.*;

public class WordGenerateTest {

    @Test
    public void tableTest() {

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        Date date = new Date(System.currentTimeMillis());
        String outputFileName = formatter.format(date);

        String inputFilePath = "./src/main/resources/template.ftl";
        String outFilePath = "./src/main/resources/"+outputFileName+".doc";

        WordUtil wu = new WordUtil();
        Map<String, Object> map = new HashMap<>();

        List<Person> personList = new ArrayList<>();
        personList.add(new Person("Tyler", "18", "male"));
        personList.add(new Person("Tom", "20", "male"));
        personList.add(new Person("Marry", "19", "female"));

        map.put("personList", personList);
        wu.createWord(map, inputFilePath, outFilePath);
    }
}

ftl模板

freemarker中,表格内容在<w:tbl> </w:tbl>中间,此处的ftl模板包含了很多表格的属性设置,在应用过程中,可以按照想要的表格样式先建立一个带有空表格的word文档,再反编译成ftl模板,避免手动设置属性。这里只实现了动态的行数,列数同理。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml" 
	xmlns:v="urn:schemas-microsoft-com:vml" 
	xmlns:w10="urn:schemas-microsoft-com:office:word" 
	xmlns:sl="http://schemas.microsoft.com/schemaLibrary/2003/core" 
	xmlns:aml="http://schemas.microsoft.com/aml/2001/core" 
	xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint" 
	xmlns:o="urn:schemas-microsoft-com:office:office" 
	xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" 
	w:macrosPresent="no" w:embeddedObjPresent="no" 
	w:ocxPresent="no" xml:space="preserve">
    <w:body>
    	
        <w:tbl>
            <w:tblPr>
                <w:tblStyle w:val="a63" />
                <w:tblW w:w="9243" w:type="dxa" />
                <w:tblInd w:w="0" w:type="dxa" />
                <w:tblBorders>
                    <w:top w:val="single" 
                    w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto" />
                    <w:left w:val="single" 
                    w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto" />
                    <w:bottom w:val="single" 
                    w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto" />
                    <w:right w:val="single" 
                    w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto" />
                    <w:insideH w:val="single" 
                    w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto" />
                    <w:insideV w:val="single" 
                    w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto" />
                </w:tblBorders>
                <w:tblLayout w:type="Fixed" />
                <w:tblCellMar>
                    <w:top w:w="0" w:type="dxa" />
                    <w:left w:w="108" w:type="dxa" />
                    <w:bottom w:w="0" w:type="dxa" />
                    <w:right w:w="108" w:type="dxa" />
                </w:tblCellMar>
            </w:tblPr>
            <w:tblGrid>
                <w:gridCol w:w="3499" />
                <w:gridCol w:w="5744" />
            </w:tblGrid>
            <!-->以下开始循环内容,list的每个元素为一行,总共三行<-->
            <#list personList as pl>
	            <w:tr>
	                <w:tblPrEx>
	                    <w:tblBorders>
	                        <w:top w:val="single" w:sz="4" 
	                        wx:bdrwidth="10" w:space="0" w:color="auto" />
	                        <w:left w:val="single" w:sz="4" 
	                        wx:bdrwidth="10" w:space="0" w:color="auto" />
	                        <w:bottom w:val="single" w:sz="4" 
	                        wx:bdrwidth="10" w:space="0" w:color="auto" />
	                        <w:right w:val="single" w:sz="4" 
	                        wx:bdrwidth="10" w:space="0" w:color="auto" />
	                        <w:insideH w:val="single" w:sz="4" 
	                        wx:bdrwidth="10" w:space="0" w:color="auto" />
	                        <w:insideV w:val="single" w:sz="4" 
	                        wx:bdrwidth="10" w:space="0" w:color="auto" />
	                    </w:tblBorders>
	                    <w:tblCellMar>
	                        <w:top w:w="0" w:type="dxa" />
	                        <w:left w:w="108" w:type="dxa" />
	                        <w:bottom w:w="0" w:type="dxa" />
	                        <w:right w:w="108" w:type="dxa" />
	                    </w:tblCellMar>
	                </w:tblPrEx>
	                <w:trPr>
	                    <w:trHeight w:val="361" w:h-rule="atLeast" />
	                </w:trPr>
	                <w:tc>
	                    <w:tcPr>
	                        <w:tcW w:w="3499" w:type="dxa" />
	                        <w:shd w:val="clear" w:color="auto" 
	                        w:fill="auto" />
	                        <w:noWrap w:val="0" />
	                        <w:vAlign w:val="center" />
	                    </w:tcPr>
	                    <w:p>
	                        <w:pPr>
	                            <w:spacing w:before-lines="50" 
	                            w:after-lines="50" w:line="240" w:line-rule="auto" />
	                            <w:jc w:val="left" />
	                            <w:rPr>
	                                <w:rFonts w:ascii="宋体" w:h-ansi="宋体" 
	                                w:fareast="宋体" w:hint="default" />
	                                <w:color w:val="auto" />
	                                <w:sz-cs w:val="21" />
	                            </w:rPr>
	                        </w:pPr>
	                        <w:r>
	                            <w:rPr>
	                                <w:rFonts w:ascii="宋体" 
	                                w:h-ansi="宋体" w:fareast="宋体" 
	                                w:cs="宋体" w:hint="fareast" />
	                                <w:color w:val="auto" />
	                                <w:kern w:val="0" />
	                                <w:sz-cs w:val="21" />
	                            </w:rPr>
	                            <w:t>${pl.name}</w:t>
	                        </w:r>
	                    </w:p>
	                </w:tc>
	                <w:tc>
	                    <w:tcPr>
	                        <w:tcW w:w="5744" w:type="dxa" />
	                        <w:shd w:val="clear" w:color="auto" 
	                        w:fill="auto" />
	                        <w:noWrap w:val="0" />
	                        <w:vAlign w:val="center" />
	                    </w:tcPr>
	                    <w:p>
	                        <w:pPr>
	                            <w:spacing w:before-lines="50" 
	                            w:after-lines="50" w:line="240" 
	                            w:line-rule="auto" />
	                            <w:jc w:val="left" />
	                            <w:rPr>
	                                <w:rFonts w:ascii="宋体" 
	                                w:h-ansi="宋体" w:fareast="宋体" 
	                                w:hint="default" />
	                                <w:sz-cs w:val="21" />
	                                <w:lang w:val="EN-US" w:fareast="ZH-CN" />
	                            </w:rPr>
	                        </w:pPr>
	                        <w:r>
	                            <w:rPr>
	                                <w:rFonts w:ascii="宋体" 
	                                w:h-ansi="宋体" w:fareast="宋体" 
	                                w:hint="fareast" />
	                                <w:sz-cs w:val="21" />
	                                <w:lang w:val="EN-US" w:fareast="ZH-CN" />
	                            </w:rPr>
	                            <w:t>${pl.age}</w:t>
	                        </w:r>
	                    </w:p>
	                </w:tc>
	                <w:tc>
	                    <w:tcPr>
	                        <w:tcW w:w="5744" w:type="dxa" />
	                        <w:shd w:val="clear" w:color="auto" 
	                        w:fill="auto" />
	                        <w:noWrap w:val="0" />
	                        <w:vAlign w:val="center" />
	                    </w:tcPr>
	                    <w:p>
	                        <w:pPr>
	                            <w:spacing w:before-lines="50" 
	                            w:after-lines="50" w:line="240" 
	                            w:line-rule="auto" />
	                            <w:jc w:val="left" />
	                            <w:rPr>
	                                <w:rFonts w:ascii="宋体" 
	                                w:h-ansi="宋体" w:fareast="宋体" 
	                                w:hint="default" />
	                                <w:sz-cs w:val="21" />
	                                <w:lang w:val="EN-US" w:fareast="ZH-CN" />
	                            </w:rPr>
	                        </w:pPr>
	                        <w:r>
	                            <w:rPr>
	                                <w:rFonts w:ascii="宋体" w:h-ansi="宋体" 
	                                w:fareast="宋体" w:hint="fareast" />
	                                <w:sz-cs w:val="21" />
	                                <w:lang w:val="EN-US" w:fareast="ZH-CN" />
	                            </w:rPr>
	                            <w:t>${pl.gender}</w:t>
	                        </w:r>
	                    </w:p>
	                </w:tc>
	            </w:tr>
            </#list>
        </w:tbl>
    </w:body>
</w:wordDocument>

生成结果展示

freemark 行列动态对应的table freemarker动态生成word表格_microsoft_03

5 动态目录与动态标题结构

使用循环结构,我们还可以建立动态的文档框架。一个文档的框架由一级标题、二级标题等确定。如果各级标题的数量和内容也由List控制,那么文档就可以实现动态结构。同理也可以实现动态的目录。
首先建立一个Company类做为一级标题,每个Company包含一个长度不等的List<Person>

import lombok.Getter;
import lombok.Setter;
import java.util.List;

@Getter
@Setter
public class Company {
    String name;
    List<Person> personList;

    public Company(String name, List<Person> personList) {
        this.name = name;
        this.personList = personList;
    }
}

单元测试

建立单元测试

import com.tyler.freemarker.FigureUtil;
import com.tyler.freemarker.WordUtil;
import com.tyler.freemarker.model.Company;
import com.tyler.freemarker.model.Person;
import org.junit.Test;

import java.text.SimpleDateFormat;
import java.util.*;

public class WordGenerateTest {

    @Test
    public void titleTest() {
        Random r = new Random();

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        Date date = new Date(System.currentTimeMillis());
        String outputFileName = formatter.format(date);

        String inputFilePath = "./src/main/resources/template.ftl";
        String outFilePath = "./src/main/resources/"+outputFileName+".doc";

        WordUtil wu = new WordUtil();
        Map<String, Object> map = new HashMap<>();

        List<Company> companyList = new ArrayList<>();

        for (int i = 0; i < r.nextInt(5)+2; i++) {
            List<Person> personList = new ArrayList<>();
            for (int j = 0; j <r.nextInt(4)+2 ; j++) {
                Person person = new Person("name"+r.nextInt(15), ""+r.nextInt(50), "male");
                personList.add(person);
            }
            Company company = new Company("company"+r.nextInt(13), personList);
            companyList.add(company);
        }
        map.put("companyList", companyList);
        wu.createWord(map, inputFilePath, outFilePath);

    }

}

ftl模板

ftl模版文件,此处使用了嵌套的双层循环,外层循环一级标题,内层循环二级标题。目录与标题之间需要建立超链接,点击目录可以跳转正文标题。目录处<w:instrText> HYPERLINK \l ${cl.name} </w:instrText>是建立超链接,<w:instrText> PAGEREF ${cl.name} </w:instrText>是将超链接与页码关联。这两个地方的索引 ${cl.name}必须和标题的<aml:annotation aml:id="0" w:type="Word.Bookmark.Start" w:name="${cl.name}" />一致,才能建立超链接。因此为了避免索引冲突,这里选取名字作为索引值,也可以自行设定其他不会冲突的字符串,保证唯一性就可以。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"
                xmlns:v="urn:schemas-microsoft-com:vml"
                xmlns:w10="urn:schemas-microsoft-com:office:word"
                xmlns:sl="http://schemas.microsoft.com/schemaLibrary/2003/core"
                xmlns:aml="http://schemas.microsoft.com/aml/2001/core"
                xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint"
                xmlns:o="urn:schemas-microsoft-com:office:office"
                xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
                w:macrosPresent="no" w:embeddedObjPresent="no" w:ocxPresent="no"
                xml:space="preserve">
    <w:body>
        <wx:sect>
            <w:p>
                <w:pPr>
                    <w:spacing w:before="0" w:before-lines="0" w:after="0" w:after-lines="0" w:line="240" w:line-rule="auto" />
                    <w:ind w:left="0" w:left-chars="0" w:right="0" w:right-chars="0" w:first-line="0" w:first-line-chars="0" />
                    <w:jc w:val="center" />
                </w:pPr>
                <w:r>
                    <w:rPr>
                        <w:rFonts w:ascii="宋体" w:h-ansi="宋体" w:fareast="宋体" w:hint="default" />
                        <w:sz w:val="21" />
                    </w:rPr>
                    <w:t>目录</w:t>
                </w:r>
            </w:p>
            <#list companyList as cl>
                <w:p>
                    <w:pPr>
                        <w:pStyle w:val="a4" />
                        <w:tabs>
                            <w:tab w:val="right" w:leader="dot" w:pos="8306" />
                        </w:tabs>
                    </w:pPr>
                    <w:r>
                        <w:fldChar w:fldCharType="begin" />
                    </w:r>
                    <w:r>
                        <w:instrText>TOC \o "1-3" \h \u </w:instrText>
                    </w:r>
                    <w:r>
                        <w:fldChar w:fldCharType="separate" />
                    </w:r>
                    <w:r>
                        <w:fldChar w:fldCharType="begin" />
                    </w:r>
                    <w:r>
                        <w:instrText> HYPERLINK \l ${cl.name} </w:instrText>
                    </w:r>
                    <w:r>
                        <w:fldChar w:fldCharType="separate" />
                    </w:r>
                    <w:r>
                        <w:rPr>
                            <w:rFonts w:hint="fareast" />
                            <w:lang w:fareast="ZH-CN" />
                        </w:rPr>
                        <w:t>${cl.name}</w:t>
                    </w:r>
                    <w:r>
                        <w:tab />
                    </w:r>
                    <w:r>
                        <w:fldChar w:fldCharType="begin" />
                    </w:r>
                    <w:r>
                        <w:instrText> PAGEREF ${cl.name} </w:instrText>
                    </w:r>
                    <w:r>
                        <w:fldChar w:fldCharType="separate" />
                    </w:r>
                    <w:r>
                        <w:t>1</w:t>
                    </w:r>
                    <w:r>
                        <w:fldChar w:fldCharType="end" />
                    </w:r>
                    <w:r>
                        <w:fldChar w:fldCharType="end" />
                    </w:r>
                </w:p>
                <#list cl.getPersonList() as pl>
                    <w:p>
                        <w:pPr>
                            <w:pStyle w:val="a5" />
                            <w:tabs>
                                <w:tab w:val="right" w:leader="dot" w:pos="8306" />
                            </w:tabs>
                        </w:pPr>
                        <w:r>
                            <w:fldChar w:fldCharType="begin" />
                        </w:r>
                        <w:r>
                            <w:instrText> HYPERLINK \l ${pl.name} </w:instrText>
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="separate" />
                        </w:r>
                        <w:r>
                            <w:rPr>
                                <w:rFonts w:hint="fareast" />
                                <w:lang w:val="EN-US" w:fareast="ZH-CN" />
                            </w:rPr>
                            <w:t>${pl.name}</w:t>
                        </w:r>
                        <w:r>
                            <w:tab />
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="begin" />
                        </w:r>
                        <w:r>
                            <w:instrText> PAGEREF ${pl.name} </w:instrText>
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="separate" />
                        </w:r>
                        <w:r>
                            <w:t>1</w:t>
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="end" />
                        </w:r>
                        <w:r>
                            <w:fldChar w:fldCharType="end" />
                        </w:r>
                    </w:p>
                </#list>
            </#list>


            <w:p>
                <w:r>
                    <w:fldChar w:fldCharType="end" />
                </w:r>
            </w:p>
            <#list companyList as cl>
                <w:p>
                    <w:pPr>
                        <w:pStyle w:val="2" />
                        <w:rPr>
                            <w:rFonts w:hint="fareast" />
                            <w:lang w:val="EN-US" w:fareast="ZH-CN" />
                        </w:rPr>
                    </w:pPr>
                    <aml:annotation aml:id="0" w:type="Word.Bookmark.Start" w:name="${cl.name}" />
                    <w:r>
                        <w:rPr>
                            <w:rFonts w:hint="fareast" />
                            <w:lang w:fareast="ZH-CN" />
                            <w:sz w:val="50" />
                        </w:rPr>
                        <w:t>${cl.name}</w:t>
                    </w:r>
                    <aml:annotation aml:id="0" w:type="Word.Bookmark.End" />
                </w:p>
                <#list cl.getPersonList() as pl>
                    <w:p>
                        <w:pPr>
                            <w:pStyle w:val="3" />
                            <w:rPr>
                                <w:rFonts w:hint="fareast" />
                                <w:lang w:val="EN-US" w:fareast="ZH-CN" />
                            </w:rPr>
                        </w:pPr>
                        <aml:annotation aml:id="1" w:type="Word.Bookmark.Start" w:name="${pl.name}" />
                        <w:r>
                            <w:rPr>
                                <w:rFonts w:hint="fareast" />
                                <w:lang w:val="EN-US" w:fareast="ZH-CN" />
                                <w:sz w:val="30" />
                            </w:rPr>
                            <w:t>${pl.name}</w:t>
                        </w:r>
                        <aml:annotation aml:id="1" w:type="Word.Bookmark.End" />
                    </w:p>
                </#list>
            </#list>


            <w:sectPr>
                <w:pgSz w:w="11906" w:h="16838" />
                <w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="851" w:footer="992" w:gutter="0" />
                <w:cols w:space="720" />
                <w:docGrid w:type="lines" w:line-pitch="312" />
            </w:sectPr>
        </wx:sect>
    </w:body>
</w:wordDocument>

生成结果展示

freemark 行列动态对应的table freemarker动态生成word表格_单元测试_04

注:上述生成的文件中,暂时无法做到更新页码,只能手动打开word文件,右键–>更新域–>更新整个目录。
目录更新的实现思路可以参考:代码操作Word时,目录自动更新的两种方法

6 插入图片

插入word的图片要转码成base64码,才能写入ftl文件中,因此先建立转码处理的接口。生成的base64码以字符串形式存储。

import java.util.Base64;
import java.util.Base64.Encoder;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FigureUtil {
    public static String getImageStr(String imgFilePath) {

        InputStream in = null;
        byte[] data = null;
        try {
            in = new FileInputStream(imgFilePath);
            int avai = in.available();
            data = new byte[avai];
            in.read(data);
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(in !=null){
                try{
                    in.close();
                } catch(IOException e) {
                    e.printStackTrace();
                }
            }
        }
        Encoder encoder = Base64.getEncoder();;
        return encoder.encodeToString(data);
    }
}

单元测试

建立单元测试

import com.tyler.freemarker.FigureUtil;
import com.tyler.freemarker.WordUtil;
import org.junit.Test;

import java.text.SimpleDateFormat;
import java.util.*;


public class WordGenerateTest {

    @Test
    public void figureTest() {
        String imgFilePath = "./src/main/resources/1.jpg";
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        Date date = new Date(System.currentTimeMillis());
        String outputFileName = formatter.format(date);

        String inputFilePath = "./src/main/resources/template.ftl";
        String outFilePath = "./src/main/resources/"+outputFileName+".doc";

        WordUtil wu = new WordUtil();
        Map<String, Object> map = new HashMap<>();

        String img = FigureUtil.getImageStr(imgFilePath);
        map.put("img", img);

        wu.createWord(map, inputFilePath, outFilePath);
    }


}

ftl模板

ftl模版文件,base64码非常长,这里使用img来代替base64码字符串

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"
	xmlns:v="urn:schemas-microsoft-com:vml"
	xmlns:w10="urn:schemas-microsoft-com:office:word"
	xmlns:sl="http://schemas.microsoft.com/schemaLibrary/2003/core"
	xmlns:aml="http://schemas.microsoft.com/aml/2001/core"
	xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint"
	xmlns:o="urn:schemas-microsoft-com:office:office"
	xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
	w:macrosPresent="no" w:embeddedObjPresent="no"
	w:ocxPresent="no" xml:space="preserve">
    <w:body>
        <wx:sect>
            <w:p>
                <w:pPr>
                    <w:rPr>
                        <w:rFonts w:hint="default" />
                        <w:lang w:val="EN-US" w:fareast="ZH-CN" />
                    </w:rPr>
                </w:pPr>
                <w:r>
                    <w:rPr>
                        <w:rFonts w:hint="default" />
                        <w:lang w:val="EN-US" w:fareast="ZH-CN" />
                    </w:rPr>
                    <w:pict>
                        <w:binData w:name="wordml://1.png">${img}</w:binData>
                        <v:shape id="_x0000_s1026" o:spt="75" alt="1" type="#_x0000_t75"
                        	style="height:140.9pt;width:211.35pt;" filled="f"
                        	o:preferrelative="t" stroked="f" coordsize="21600,21600">
                            <v:path />
                            <v:fill on="f" focussize="0,0" />
                            <v:stroke on="f" />
                            <v:imagedata src="wordml://1.png" o:title="1" />
                            <o:lock v:ext="edit" aspectratio="t" />
                            <w10:wrap type="none" />
                            <w10:anchorlock />
                        </v:shape>
                    </w:pict>
                </w:r>
            </w:p>
            <w:sectPr>
                <w:pgSz w:w="11906" w:h="16838" />
                <w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="851" w:footer="992" w:gutter="0" />
                <w:cols w:space="720" />
                <w:docGrid w:type="lines" w:line-pitch="312" />
            </w:sectPr>
        </wx:sect>
    </w:body>
</w:wordDocument>

生成结果展示

freemark 行列动态对应的table freemarker动态生成word表格_xml_05


建议预先生成模版的时候随便插入一个图片占位,并设置好格式和位置。