poi-tl

lishihuan大约 11 分钟

poi-tl

java根据word模板导出word文档

是一个基于Apache POI的Word模板引擎,也是一个免费开源的Java类库

NOTE: 1.12.x 要求 POI 版本在 5.2.2+.

poi-tl和poi-tl历史版本

1.12.0 Documentation,Apache POI5.2.2+,JDK1.8+

其中 1.7.x 版本下 没有 处理图片的 Pictures类

1. poi-tl特性

Word模板引擎功能描述
文本将标签渲染为文本
图片将标签渲染为图片
表格将标签渲染为表格
列表将标签渲染为列表
图表条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)、散点图等图表渲染
If Condition判断根据条件隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Foreach Loop循环根据集合循环某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Loop表格行循环复制渲染表格的某一行
Loop表格列循环复制渲染表格的某一列
Loop有序列表支持有序列表的循环,同时支持多级列表
Highlight代码高亮word中代码块高亮展示,支持26种语言和上百种着色样式
Markdown将Markdown渲染为word文档
Word批注完整的批注功能,创建批注、修改批注等
Word附件Word中插入附件
SDT内容控件内容控件内标签支持
Textbox文本框文本框内标签支持
图片替换将原有图片替换成另一张图片
书签、锚点、超链接支持设置书签,文档内锚点和超链接功能
Expression Language完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL…
样式模板即样式,同时代码也可以设置样式
模板嵌套模板包含子模板,子模板再包含子模板
合并Word合并Merge,也可以在指定位置进行合并
用户自定义函数(插件)插件化设计,在文档任何位置执行函数

2. 使用

2.1.Maven添加依赖

需要poi

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.2</version>
</dependency>
<dependency>
  <groupId>com.deepoove</groupId>
  <artifactId>poi-tl</artifactId>
  <version>1.12.0</version>
</dependency>

2.2.入门示例

需要docx 格式的word

新建Word文档template.docx

添加文本 文档中添加 {{title}}

public static void main(String[] args) throws IOException {
    XWPFTemplate.compile("D:\\template.docx").render(new HashMap<String, Object>(){{
    	put("title", "测试测试");
    }}).writeToFile("D:\\out_template.docx");
}


输出文件

Map<String, Object> map = new HashMap<String,Object>();
map.put("score", "28");
map.put("title", "测试任务"); 
File file = new File("D:/文件输入.docx");
XWPFTemplate template = XWPFTemplate.compile(file).render(map);

// 输出文件
FileOutputStream out = new FileOutputStream(new File("D:\\文件输出.docx"));
template.write(out);
out.flush();
out.close();
template.close();
PoitlIOUtils.closeQuietlyMulti(template, bos, out);


网络流输出

Map<String, Object> map = new HashMap<String,Object>();
map.put("score", "28");
map.put("title", "测试任务"); 
File file = new File("D:/文件输入.docx");
XWPFTemplate template = XWPFTemplate.compile(file).render(map);


//   网络流输出
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition","attachment;filename=\""+"out_template.docx"+"\"");

OutputStream out = response.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(out);
template.write(bos);
bos.flush();
out.flush();
// 这步不能忘记,否则,文件流无法正常关闭,导致模板文件无法修改删除
PoitlIOUtils.closeQuietlyMulti(template, bos, out);

针对 需要进度条的场景

try {
    //存放要填充的数据
    Map<String, Object> datas = Maps.newHashMap();         
    map.put("score", "28");
    map.put("title", "测试任务"); 
    File fileByUrl = BasUtil.getFileByUrl(filePath);
    XWPFTemplate compile = XWPFTemplate.compile(fileByUrl.getAbsolutePath(), configure);
    compile.render(datas);

    // 为了获取文件流的大小 ,主要是设置   请求头中的 Content-Length
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    compile.write(bos);
    byte[] fileBytes = bos.toByteArray();

    //输出为文件,指定输出文件名
    //            compile.writeToFile(destFilePath+"out_test01.docx");
    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition", "attachment; filename=" + Encodes.urlEncode(fileName));
    response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileBytes.length)); // 这样前端可以进行进度计算
    // 写入文件内容到输出流
    OutputStream out = response.getOutputStream();
    out.write(fileBytes);
    out.flush();
    PoitlIOUtils.closeQuietlyMulti(compile, bos, out);
} catch (Exception e) {
    e.printStackTrace();
}


针对网络文件

File fileByUrl = BasUtil.getFileByUrl(filePath);
XWPFTemplate compile = XWPFTemplate.compile(fileByUrl.getAbsolutePath(), configure);
compile.render(datas);

通过网络url返回File对象

 public static File getFileByUrl(String fileUrl){
        try {
            // 创建 URL 对象
            URL url = new URL(fileUrl);
            // 打开网络连接
            InputStream inputStream = url.openStream();

            // 将文件内容写入临时文件
            File tempFile = File.createTempFile("tempfile", ".docx");
            FileOutputStream outputStream = new FileOutputStream(tempFile);

            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            // 关闭流
            inputStream.close();
            outputStream.close();
            return tempFile;
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new CustomException("下载失败,请重试!");
    }

3. 标签

  • 在POI-TL中没有复杂的控制结构和变量赋值,只有标签;
  • 标签前后由两个大括号组成
  • 标签的内容为:符号+标签名称(符号代表对应的标签类型)

符号与标签类型关系如下:

符号标签类型
(无符号,默认)文本
@图片
#表格
*列表

3.1 文本

  • String :文本
  • TextRenderData :有样式的文本
  • HyperlinkTextRenderData :超链接和锚点文本
  • Object :调用 toString() 方法转化为文本

文本换行使用 \n 字符。

推荐使用工厂 Texts 构建文本模型。

put("name", "Sayi");
put("author", Texts.of("Sayi").color("000000").create());
put("link", Texts.of("website").link("http://deepoove.com").create());
put("anchor", Texts.of("anchortxt").anchor("appendix1").create());

TextRenderData的结构体

{
  "text": "Sayi",
  "style": {
    "strike": false, // 删除线
    "bold": true, 
    "italic": false,  //斜体
    "color": "00FF00", 
    "underLine": false, // 下划线
    "fontFamily": "微软雅黑", 
    "fontSize": 12, 
    "highlightColor": "green", // 背景高亮色
    "vertAlign": "superscript", //上标或者下标
    "characterSpacing" : 20 //间距
  }

3.2 图片open in new window

图片标签以@开始:{{@var}}

数据模型:

  • String :图片url或者本地路径,默认使用图片自身尺寸
  • ByteArrayPictureRenderData
  • FilePictureRenderData
  • UrlPictureRenderData

推荐使用工厂 Pictures 构建图片模型。

// 指定图片路径
put("image", "logo.png");
// svg图片
put("svg", "https://img.shields.io/badge/jdk-1.6%2B-orange.svg");

// 图片文件
put("image1", Pictures.ofLocal("logo.png").size(120, 120).create());

// 图片流
put("streamImg", Pictures.ofStream(new FileInputStream("logo.jpeg"), PictureType.JPEG)
  .size(100, 120).create());

// 网络图片(注意网络耗时对系统可能的性能影响)
put("urlImg", Pictures.ofUrl("http://deepoove.com/images/icecream.png")
  .size(100, 100).create());

// java图片,我们可以利用Java生成图表插入到word文档中
put("buffered", Pictures.ofBufferedImage(bufferImage, PictureType.PNG)
  .size(100, 100).create());

FilePictureRenderData的结构体

{
  "pictureType" : "PNG", //图片类型
  "path": "logo.png", //图片路径
  "pictureStyle": { // 尺寸 ,单位是像素
    "width": 100, 
    "height": 100 
  },
  "altMeta": "图片不存在" 
}

3.3 表格open in new window

表格标签以#开始:{{#var}}

数据模型:

  • TableRenderData

推荐使用工厂 TablesRowsCells 构建表格模型。

1. Example 1. 基础表格示例

// 一个2行2列的表格
put("table0", Tables.of(new String[][] {
                new String[] { "00", "01" },
                new String[] { "10", "11" }
            }).border(BorderStyle.DEFAULT).create());
0001
1011

2. Example 2. 表格样式示例

// 第0行居中且背景为蓝色的表格
RowRenderData row0 = Rows.of("姓名", "学历").textColor("FFFFFF")
      .bgColor("4472C4").center().create();
RowRenderData row1 = Rows.create("李四", "博士");
put("table1", Tables.create(row0, row1));
image-20240307171637944
image-20240307171637944

3. Example 3. 表格合并示例

// 合并第1行所有单元格的表格
RowRenderData row0 = Rows.of("列0", "列1", "列2").center().bgColor("4472C4").create();
RowRenderData row1 = Rows.create("没有数据", null, null);
MergeCellRule rule = MergeCellRule.builder().map(Grid.of(1, 0), Grid.of(1, 2)).build();
put("table3", Tables.of(row0, row1).mergeRule(rule).create());
image-20240307171800810
image-20240307171800810

TableRenderData表格模型在单元格内可以展示文本和图片,同时也可以指定表格样式、行样式和单元格样式,而且在N行N列渲染完成后可以应用单元格合并规则 MergeCellRule ,从而实现更复杂的表格。

{
  "rows": [  //行数据
    {
      "cells": [   // 单元格数据
        {
          "paragraphs": [ // 单元格内段落
            {
              "contents": [
                {
                  [TextRenderData] // 单元格内文本
                },
                {
                  [PictureRenderData] //单元格内图片
                }
              ],
              "paragraphStyle": null // 单元格内段落文本的样式:对齐
            }
          ],
          "cellStyle": {  // 单元格样式:垂直对齐方式,背景色
            "backgroundColor": "00000",
            "vertAlign": "CENTER"
          }
        }
      ],
      "rowStyle": { // 行样式:行高(单位cm)
        "height": 2.0f
      }
    }
  ],
  "tableStyle": { //表格样式:表格对齐、边框样式
    "width": 14.63f, // 表格宽度(单位cm),表格的最大宽度 = 页面宽度 - 页边距宽度 * 2,页面宽度为A4(20.99 * 29.6,页边距为3.18 * 2.54)的文档最大表格宽度14.63cm。
    "colWidths": null
  },
  "mergeRule": { // 单元格合并规则,比如第0行第0列至第1行第2列单元格合并
    "mapping": {
      "0-0": "1-2"
    }
  }
}

4. 案例

// 插入表格
// 用行循环插件
LoopRowTableRenderPolicy renderPolicy = new LoopRowTableRenderPolicy();
ConfigureBuilder builder = Configure.builder();
Configure configure = builder.bind("antiBurstingTitle", renderPolicy).build();
datas.put("antiBurstingTitle", BasData.getAntiBurstingList());
// 模拟数据
public static List<AntiBursting2VO> getAntiBurstingList() {
    List<AntiBursting2VO> list = Lists.newArrayList();
    list.add(new AntiBursting2VO(1,"合肥",46,28,116,591));
    list.add(new AntiBursting2VO(2, "蚌埠", 2, 54, 79, null));
    list.add(new AntiBursting2VO(3, "淮北", 47, 64, null, null));
    list.add(new AntiBursting2VO(4, "宣城", 46, 42, null, null));
    list.add(new AntiBursting2VO(5, "马鞍山", 8, 1, 41, 64));
    list.add(new AntiBursting2VO(6, "六安", 1, null, 37, 55));
    list.add(new AntiBursting2VO(7, "滁州", 5, 4, 35, 46));
    list.add(new AntiBursting2VO(8, "芜湖", 1, 29, 47, null));
    list.add(new AntiBursting2VO(9, "亳州", null, 36, null, null));
    list.add(new AntiBursting2VO(10, "阜阳", 3, 1, 23, 109));
    list.add(new AntiBursting2VO(11, "黄山", 1, 2, 19, 20));
    list.add(new AntiBursting2VO(12, "安庆", 2, 17, 54, null));
    list.add(new AntiBursting2VO(13, "淮南", 14, 77, null, null));
    list.add(new AntiBursting2VO(14, "宿州", 2, 1, null, 63));
    list.add(new AntiBursting2VO(15, "铜陵", 1, null, 22, null));
    list.add(new AntiBursting2VO(16, "池州", 2, 3, 19, null));
    return list;
}

5. 表格行循环open in new window

https://blog.csdn.net/ximaiyao1984/article/details/126502073open in new window

https://blog.csdn.net/ximaiyao1984/article/details/126502073open in new window

  • 循环行
观测指标 {{rows}}日期
[title]
  • 循环列
{{column}}观测指标[month]
结论

3.4 图表

https://blog.csdn.net/yelangkingwuzuhu/article/details/131786614open in new window

需要先插入图表,占位,并且图表类型要对应上,比如 柱状图+折线图,不能选择柱状图

同时修改右击图表选择 --> 查看可选文字 --> 填写 {{变量字段}}

image-20240308160407406
image-20240308160407406
package com.yelang.test;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.ChartMultiSeriesRenderData;
import com.deepoove.poi.data.Charts;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.Pictures;
import com.deepoove.poi.data.SeriesRenderData;
 
public class TestMain2 {
	
	public static void main(String[] args) throws IOException {
		Map<String, Object> map = new HashMap<String,Object>();
        // 1. 普通文本
        map.put("score", "28");
        map.put("title", "测试任务");
        map.put("type", "上半年综合测评");
        map.put("status", "已完成");
        map.put("time", "2023-07-18");
        
        // 2. 图片
        map.put("locpicture", new PictureRenderData(400, 300, "D:/image.jpg"));
        map.put("urlImg", Pictures.ofUrl("https://p1.itc.cn/images01/20230418/5d13ab4a86c04a8dac668bf4129e1f0c.png", PictureType.PNG).size(400, 300).create());
        
        // 3. 柱状图
        ChartMultiSeriesRenderData sbqk = Charts
                .ofMultiSeries("十六市区县情况", new String[] { "济南","青岛","烟台","威海"})
                .addSeries("上报情况", new Double[] { 15.0,20.6,42.6,90.1})
                .addSeries("查出情况", new Double[] { 12.0,15.3,28.6,80.1})
                .create();
 
        map.put("sbqk", sbqk);
 
        // 4. 柱状图
        ChartMultiSeriesRenderData sjzlpm = Charts
                .ofMultiSeries("医院综合排名", new String[] { "山东大学齐鲁医院","山东省泰山医院","山东省第二人民医院","山东省第三医院"})
                .addSeries("数据质量排名", new Double[] { 70.5,40.6,22.7,85.4})
                .addSeries("价格质量排名", new Double[] { 80.5,75.6,72.7,85.4})
                .create();
 
        map.put("sjzlpm", sjzlpm);
 
        
        // 5. 柱状图、折线图共存
        List<SeriesRenderData> seriesRenderData = new ArrayList<SeriesRenderData>(3);
        SeriesRenderData series1 = new SeriesRenderData("GDP", new Double[] {70.5,40.6,22.7,85.4,700.0,40.8});
        series1.setComboType(SeriesRenderData.ComboType.BAR);
        seriesRenderData.add(series1);
        
        SeriesRenderData series2 = new SeriesRenderData("人口", new Double[] {80.5,50.6,62.7,45.4,200.0,140.8});
        series2.setComboType(SeriesRenderData.ComboType.BAR);
        seriesRenderData.add(series2);
        
        SeriesRenderData series3 = new SeriesRenderData("指数", new Double[] {0.6,0.6,0.7,0.4,0.7,0.8});
        series3.setComboType(SeriesRenderData.ComboType.LINE);
        seriesRenderData.add(series3);
        
        ChartMultiSeriesRenderData hntb = Charts
                .ofMultiSeries("某省社会排名", new String[] { "城市1","城市2","城市3","城市4","城市5","城市6"})
                .create();
        hntb.setSeriesDatas(seriesRenderData);
        
        map.put("hntb", hntb);
 
        File file = new File("D:/文件输入.docx");
        XWPFTemplate template = XWPFTemplate.compile(file).render(map);
 
        FileOutputStream out = new FileOutputStream(new File("D:\\文件输出.docx"));
        template.write(out);
        out.flush();
        out.close();
        template.close();
        System.out.println("完成");
	}
}

案例

参考:https://www.jb51.net/program/299547zc7.htmopen in new window

img
img
    /**
     * 根据网络生成word
     **/
    public static void generateByNetWork() {
        String ip = "192.168.x.x";
        // 我这里是用文件服务器返回的网络文件流,你也可以直接放置一个文件的url
        String pictureUrl = "http://" + ip + ":8080/api/file/download?id=b9e0ac0336a84fc0b4ebb4cdf2c805e0.png";
        HashMap<String, Object> finalMap = new HashMap<>();
        finalMap.put("姓名", "张三");
        finalMap.put("身份证号", "511232XXXXXXX");
        finalMap.put("手机号", "1328292xxxx");
        finalMap.put("员工签字", "张三");
        finalMap.put("日期", "2023-05-16");
        // 从网络流读取图片,置入word模板,等待编译
        if (Validator.isNotEmpty(pictureUrl)) {
            PictureRenderData picture = Pictures.ofUrl(pictureUrl).size(40, 30).create();
            finalMap.put("员工签字图片", picture);
        }
        ArrayList<Object> workList = CollUtil.newArrayList();
        finalMap.put("工作经历列表", workList);
        for (int i = 0; i < 3; i++) {
            // 模拟从mysql查询列表数据
            HashMap<String, Object> workItem = new HashMap<>();
            workItem.put("序号", i + 1);
            workItem.put("公司名", "四川有限公司" + i);
            workItem.put("工作时长", i + 10 + "小时");
            workItem.put("salary", "800" + i);
            workItem.put("联系", "项目经理" + i);
            workItem.put("岗位", "java工程师" + i);
            workList.add(workItem);
        }
        // 注意:此处用的是 <区块对> key是字符串,value则放置一个集合,类似于模板引擎的foreach标签
        ArrayList<Object> stateList = CollUtil.newArrayList();
        finalMap.put("申明项列表", stateList);
        // 模拟从mysql查询数据,改造为word模板所需的数据结构
        List<String> stateListFromMySQL = Arrays.asList("本人所递交的所有办理人才引进材料及填写的情况均属实;"
                , "我已认真阅读以上内容并确认;"
                , "若在申请期间信息变更不做变更。若违反,本人愿意承担由此产生的后果。");
        for (int i = 0; i < stateListFromMySQL.size(); i++) {
            HashMap<String, Object> stateItem = new HashMap<>();
            // 此处在 每行文字后,都添加\n 的换行符。注意:此换行仅仅只是换行,并不等同于enter,不会触发类似于 <在word中enter换行,自动触发编排序号累加> 的动作
            stateItem.put("item", stateListFromMySQL.get(i) + "\n");
            stateList.add(stateItem);
        }
        // 从网络url 下载word模板到指定文件夹
        File wordTemplate = HttpUtil.downloadFileFromUrl("http://" + ip + ":8080/file/download" + "?id=talentsTemplate.docx", "D://upload" + File.separator);
        // 此处使用了poi-tl的<表格行循环插件>,此处一定要进行参数bind,方便word模板参数替换
        LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
        Configure build = Configure.builder().bind(policy, "工作经历列表").build();
        // 进行编译
        XWPFTemplate render = XWPFTemplate.compile(wordTemplate.getAbsolutePath(), build).render(finalMap);
        File word = new File("D://upload" + File.separator + IdUtil.getSnowflake().nextId() + ".docx");
        try {
            // 关键步骤,
            render.writeToFile(word.getAbsolutePath());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 下面的方法可以根据业务调整,我这里是将参数替换后的word上传到项目对应的文件服务器,拿到的url进行存入数据库
        String json = HttpUtil.createPost("http://" + ip + ":8080/file/upload")
                .header("Content-Type", ContentType.MULTIPART.getValue())
                .form("file", word)
                .execute()
                .body();
        // hutool工具类,删除 word。因为在这个流程中,word只是一个中间产物,因为我上边已经把该word 上传到我的文件服务器,并且文件服务器 会返回给我它对应的url
        FileUtil.del(word);
        List<String> list = JSONUtil.parseArray(json).toList(String.class);
    }