Java操作Word文档

在日常开发中,经常遇到需要自动化处理Word文档的需求,比如批量生成报告、填写模板内容等。Java作为一种广泛应用的编程语言,提供了多种方式来操作Word文档。本文将详细介绍如何使用Java处理Word文档,并通过实战示例带你入门。

引言

Word文档本质上是一个遵循Open XML标准的ZIP压缩包,包含了一系列XML文件和其他资源(如图片)。因此,操作Word文档的关键在于解析和修改这些XML文件。Java开发者可以选择多种库来实现这一目标,包括但不限于Apache POI、docx4j、iText以及Spire.Doc for Java等。下面,我们将逐一探讨这些工具,并给出具体示例。

1、技术选型

工具优点缺点简介
Apache POI开源免费、社区活跃、功能完善。对于复杂的Word样式处理支持有限。Apache POI是Apache软件基金会的一个项目,提供了一套用于读写Microsoft Office格式档案的Java API,包括Word、Excel等。对于Word文档,主要使用的是POI的HWPF(处理.doc文件)和XWPF(处理.docx文件)模块。
docx4j功能强大,支持复杂Word操作,如样式、表格、图片插入等。学习曲线相对较陡峭,文档相对不够丰富。docx4j是一个开源库,专为操作.docx(Open XML)格式的Word文档设计,提供了丰富的API来处理XML内容。
iText如果你的项目已经使用了iText处理PDF,那么使用它来生成简单的Word文档会比较方便。Word处理功能不如Apache POI或docx4j全面。虽然iText主要用于PDF处理,但它也支持生成Word(.docx)文档,尽管功能相比专门的Word处理库较为有限。
Spire.Doc for Java功能强大,支持度高,文档和客户服务较完善。需要付费使用,免费版有功能限制。Spire.Doc for Java是一个商业库,专注于Word文档的处理,提供了丰富的功能,包括创建、读取、编辑、转换Word文档等。

结论

选择合适的库取决于你的具体需求和项目条件。如果你需要处理大量复杂的Word文档且预算允许,Spire.Doc可能是最佳选择。而对于开源解决方案,Apache POI适合初学者和基本需求,而docx4j则更适合处理高级场景。iText虽能生成Word,但更擅长PDF处理。无论哪种选择,掌握基本的API使用和理解Word的内部结构都是关键。希望本文能帮助你在Java项目中有效操作Word文档。

本篇文章主要以Apache POI来实现具体业务。

2、基础文本填充

2.1 引入依赖

2.1.1. poi
  • 基础库poi是最基础的Apache POI库,包含了处理老版本Office文件格式(如.xls.doc)的类和方法。它不直接支持.xlsx.docx等基于XML的文件格式。这个库主要用于处理二进制文件格式,并且是其他更特定库的基础。
2.1.2. poi-ooxml
  • XML支持poi-ooxml是针对基于XML的Office Open XML格式(.xlsx.docx.pptx等)的扩展库。它依赖于poi库,并添加了处理Open XML文件所需的所有额外类和方法。当你需要读写新格式的Office文件时,这个库是必不可少的。它包含了解析和生成Open XML文档所需的API。
2.1.3. poi-ooxml-schemas
  • XML模式与验证poi-ooxml-schemas包含了Office Open XML格式的完整XML模式定义。这些模式定义对于验证生成的Open XML文档是否符合官方规范非常重要,确保了文档的兼容性和正确性。这个依赖项不是直接用于编写代码操作POI的API,而是作为后台支持,帮助POI库正确解析和验证XML结构。

总结

  • 如果你只处理老版本的Office文件(.xls, .doc),可能只需要poi库。
  • 处理.xlsx, .docx等XML格式的文件时,你需要同时引入poipoi-ooxml,因为后者依赖前者,并且提供了处理这些新格式的功能。
  • poi-ooxml-schemas虽然不是每次都需要,但对于确保生成的文档结构正确,特别是在复杂的文档处理场景下,是非常推荐加入的依赖,因为它提供了详细的XML模式验证能力。

在Maven或Gradle项目中,通常你会同时声明这三个依赖(如果处理XML格式文件的话),以确保所有必要的组件都已就绪。

<properties>        
        <poi-ooxml.version>4.1.2</poi-ooxml.version>
        <poi.version>3.17</poi.version>    
</properties>
<dependencies>
<!-- poi -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>${poi.version}</version>
        </dependency>
        <!-- poi-ooxml -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>${poi.version}</version>
        </dependency>

        <!-- 读写Microsoft Office poi-ooxml-schemas -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>${poi-ooxml.version}</version>
        </dependency>
</dependencies>

2.2 业务思路

  • 报告模板设计

    使用Word文档作为模板,预先设计好报告的布局,包括封面页、基本信息页、测量数据表格、历史数据图表等部分。在需要填充数据的地方,设定占位符或者使用特定标记(例如{{username}}{{weight}}等)。

  • 使用Apache POI生成Word文档

利用Apache POI库(特别是poipoi-ooxml)来读取模板Word文件,并根据整理好的数据集替换模板中的占位符:

  • 加载模板文档。

  • 遍历文档,查找并替换所有的占位符。

  • 利用poi-ooxml和图表生成库(如JFreeChart结合Apache POI导出图表)生成历史测量数据的柱状图,并嵌入Word文档中。

  • 文件存储与接口设计

    • 生成的Word文档可以临时保存在服务器的文件系统或云存储中。
    • 设计一个RESTful API,接收生成报告的请求,处理逻辑后,返回文件的下载链接或Base64编码的文件内容给前端。
    • 考虑到安全性,可以设置链接的有效期,过期自动删除临时文件。

在这里插入图片描述

将模板文件放置resourcs/templates文件夹下,

在这里插入图片描述

2.3 业务层 OfficeService

package com.example.demo.service.impl;

import com.example.demo.dto.HealthReportQuery;
import com.example.demo.service.OfficeService;
import com.example.demo.uitls.Office2PdfService;
import com.example.demo.uitls.OfficeUtils;
import com.example.demo.uitls.SpringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import java.io.FileInputStream;

import org.apache.poi.xwpf.usermodel.XWPFDocument;

/**
 * OfficeServiceImpl :
 *
 * @author zyw
 * @create 2024-06-24  15:41
 */
@Service
public class OfficeServiceImpl implements OfficeService {

    /**
     * 个人健康报告模板
     */
    public static final String PERSONAL_HEALTH_REPORT_TEMPLATE = "classpath:templates/HealthReport.docx";

    @Resource
    private ResourceLoader resourceLoader;
    @Resource
    private Office2PdfService office2PdfService;

    @Override
    public XWPFDocument getHealthReport(HealthReportQuery query) {
        try {
            FileInputStream fileInputStream = SpringUtils.convertInputStreamToFileInputStream(resourceLoader.getResource(PERSONAL_HEALTH_REPORT_TEMPLATE).getInputStream());
            XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream);
            // 替换文本数据构建
            OfficeUtils.paragraphTextFilling(xwpfDocument,OfficeUtils.objectToMap(query));
            return xwpfDocument;
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public void getHealthReportWord(XWPFDocument document, HealthReportQuery query, HttpServletResponse response) {
        OfficeUtils.processingWordResponses("健康问卷-" + query.getName(), OfficeUtils.writeDocumentToInputStream(document), response);
    }

}

2.4 通用工具类 OfficeUtils

package com.example.demo.uitls;

import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.lang.reflect.Field;
import java.io.*;
import java.util.*;

/**
 * OfficeUtils : Office工具类
 *
 * @author zyw
 * @create 2024-06-24  16:35
 */

public class OfficeUtils {

    /**
     * 对象转Map
     * @param obj
     * @return
     */
    public static Map<String, String> objectToMap(Object obj) {
        Map<String, String> map = new HashMap<>();
        Class<?> clazz = obj.getClass();

        // 获取类中所有声明的字段(包括私有、受保护、默认、公共)
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true); // 设置字段可访问(如果是私有的)

            try {
                Object value = field.get(obj);
                String key = "${" + field.getName() + "}"; // 构造key,以${name}形式
                map.put(key, String.valueOf(value));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        return map;
    }

    /**
     * 段落文本填充
     *
     * @param document 文档
     * @param insertTextMap 填充内容
     */
    public static void paragraphTextFilling(XWPFDocument document, Map<String, String> insertTextMap) {
        Set<String> set = insertTextMap.keySet();

        Iterator<XWPFParagraph> itPara = document.getParagraphsIterator();
        while (itPara.hasNext()) {
            // 获取文档中当前的段落文字信息
            XWPFParagraph paragraph = itPara.next();
            List<XWPFRun> run = paragraph.getRuns();
            // 遍历段落文字对象
            for (int i = 0; i < run.size(); i++) {
                // 获取段落对象
                if (run.get(i) == null) {    //段落为空跳过
                    continue;
                }
                String sectionItem = null;
                try {
                    // 检查段落中是否包含文本框
                    sectionItem = run.get(i).getText(run.get(i).getTextPosition());    //段落内容
                } catch (Exception e) {
                }
                if (sectionItem == null) {
                    continue;
                }
                // 遍历自定义表单关键字,替换Word文档中的内容
                Iterator<String> iterator = set.iterator();
                while (iterator.hasNext()) {
                    // 当前关键字
                    String key = iterator.next();
                    // 替换内容
                    sectionItem = sectionItem.replace(key, String.valueOf(insertTextMap.get(key)));
                }
                run.get(i).setText(sectionItem, 0);
            }
        }
    }

    /**
     * 处理Word响应
     *
     * @param downloadName 下载文件名
     * @param inputStream 文件输入流
     * @param response 响应
     */
    public static void processingWordResponses(String downloadName,
                                               InputStream inputStream,
                                               HttpServletResponse response) {
        try {
            // 设置响应的Content-Type
            response.setContentType("application/octet-stream");
            response.setCharacterEncoding("utf-8");
            // 设置Content-Disposition头部,指示浏览器下载文件,文件名为document.docx
            downloadName = new String(downloadName.getBytes("UTF-8"), "ISO-8859-1");
            response.setHeader("Content-Disposition", "attachment;filename=" + downloadName + ".docx");

            // 获取响应的输出流
            OutputStream outputStream = response.getOutputStream();
            byte[] buffer = new byte[4096];
            int bytesRead = -1;
            // 将InputStream中的内容写入到OutputStream中
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            // 关闭流
            inputStream.close();
            outputStream.close();
        }catch (Exception e){

        }
    }

    /**
     * word转InputStream
     *
     * @param document
     * @return
     */
    public static InputStream writeDocumentToInputStream(XWPFDocument document) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            document.write(byteArrayOutputStream);
            byteArrayOutputStream.close();
            return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

2.5 控制层 OfficeController

package com.example.demo.controller;

import com.example.demo.dto.HealthReportQuery;
import com.example.demo.service.OfficeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;

/**
 * OfficeController : Office办公文件控制器
 *
 * @author zyw
 * @create 2024-06-24  15:40
 */

@Tag(name = "Office办公文件控制器")
@RestController
@RequestMapping("/office")
public class OfficeController {

    @Resource
    private OfficeService officeService;

    @GetMapping("/getHealthReportWord")
    @Operation(summary = "获取健康报告Word", description = "获取健康报告")
    @Parameters({
            @Parameter(name = "name", description = "姓名", required = true, in = ParameterIn.QUERY),
            @Parameter(name = "gender", description = "性别", required = true, in = ParameterIn.QUERY),
            @Parameter(name = "age", description = "年龄", required = true, in = ParameterIn.QUERY)
    })
    public void getHealthReportWord(HealthReportQuery query, HttpServletResponse response) {
        officeService.getHealthReportWord(officeService.getHealthReport(query), query, response);
    }

}

在这里插入图片描述

可以看到我们通过接口传输的三个参数均已渲染到了指定${}位置

在这里插入图片描述

3、表格

需求:我们需要根据输入的身高、体重、运动能力在文档中动态展示所处的健康状态

3.1 准备模板

在这里插入图片描述

  • 在基本信息中我们需要将姓名、性别、头像等基本信息填入模板中已存在的表格的指定单元格里
  • 在"3、您目前的体育运动水平"和"4、您的体重指数"两个标题下我们需要动态生成"体力活动水平标尺"和"体重指数标尺",同时展示出所处的健康状态

3.2 业务层 OfficeService

业务流程:
  • 模板中已存在的表格,可以通过遍历文档所有表格获取:List tables = document.getTables();

  • 模板中未存在的表格,我们通过找到模板中所需插入动态表格的上一个段落,再其下创建新的段落以及表格实现;

名词解释:
  1. XWPFDocument: 这个类代表一个Word文档。它是操作Word文件的入口点,允许你创建新的文档,读取现有的文档,添加或删除段落、表格、图片等元素。
  2. XWPFTable: 表示Word文档中的表格。你可以使用这个类来创建新的表格,获取或设置表格的属性(比如宽度、边框样式),以及操作表格中的行和单元格。
  3. XWPFParagraph: 代表文档中的一个段落。段落可以包含文本、图片、表格等多种元素。你可以使用这个类来创建新的段落,设置对齐方式、缩进、间距等格式,以及添加或删除段落中的文本或其它内容。
  4. XWPFTableCell: 单元格类,表示表格中的一个单元格。你可以通过这个类来设置单元格的内容(包括文本和嵌入的对象)、样式(如背景色、边框)以及合并或拆分单元格等。
  5. XWPFRun: 运行对象,是段落中最基本的文本处理单位。一个段落可以由一个或多个run组成,每个run可以有不同的字体样式、颜色、大小等。当你需要在同一个段落中应用不同的格式时,就会用到多个run。例如,改变文本颜色、加粗或斜体等操作都是通过对特定的run进行设置来实现的。

综上所述,这些类共同构成了操作Word文档的框架,让你能够在Java程序中灵活地创建和修改复杂的Word文档结构。

package com.example.demo.service.impl;

import com.example.demo.dto.HealthReportQuery;
import com.example.demo.service.OfficeService;
import com.example.demo.uitls.Office2PdfService;
import com.example.demo.uitls.OfficeUtils;
import com.example.demo.uitls.SpringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import java.io.*;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.List;
import java.util.Objects;

/**
 * OfficeServiceImpl :
 *
 * @author zyw
 * @create 2024-06-24  15:41
 */
@Service
@Slf4j
public class OfficeServiceImpl implements OfficeService {

    /**
     * 个人健康报告模板
     */
    public static final String PERSONAL_HEALTH_REPORT_TEMPLATE = "classpath:templates/HealthReport.docx";

    private static final String HEADER_1_3 = "3、您目前的体育运动水平";
    private static final String HEADER_1_4 = "4、您的体重指数";

    @Resource
    private ResourceLoader resourceLoader;
    @Resource
    private Office2PdfService office2PdfService;

    @Override
    public XWPFDocument getHealthReport(HealthReportQuery query) {
        try {
            FileInputStream fileInputStream = SpringUtils.convertInputStreamToFileInputStream(resourceLoader.getResource(PERSONAL_HEALTH_REPORT_TEMPLATE).getInputStream());
            XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream);
            // 替换文本数据构建
            OfficeUtils.paragraphTextFilling(xwpfDocument, OfficeUtils.objectToMap(query));
            // 在基本信息表格中填充数据
            fillInTable(xwpfDocument, query);

            // 插入体育运动水平表格
            int index3 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_3);
            handleTableOne(xwpfDocument, index3, query.getSportsLevel());
            // 插入体重指数表格
            int index4 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_4);
            handleTableTwo(xwpfDocument, index4, query.getHeight(), query.getWeight());
            
            return xwpfDocument;
        } catch (Exception e) {
            return null;
        }
    }
    
    /**
     * 填充基本信息表格
     *
     * @param document
     * @param query
     */
    public void fillInTable(XWPFDocument document, HealthReportQuery query) throws IOException, InvalidFormatException {
        // 获取表格对象集合
        List<XWPFTable> tables = document.getTables();
        // 获取模板中第一个表格
        XWPFTable xwpfTable = tables.get(0);
        xwpfTable.getRow(0).getCell(1).setText(query.getName());
        xwpfTable.getRow(0).getCell(3).setText(query.getGender());
        xwpfTable.getRow(1).getCell(1).setText(query.getAge() + "岁");
        xwpfTable.getRow(1).getCell(3).setText(query.getNativePlace());
        xwpfTable.getRow(2).getCell(1).setText(query.getHeight() + "cm");
        xwpfTable.getRow(2).getCell(3).setText(query.getWeight() + "kg");
        xwpfTable.getRow(3).getCell(1).setText(String.valueOf(query.getPhone()));
        xwpfTable.getRow(4).getCell(1).setText(query.getAddress());
        // 在第一行第五列插入图片
        XWPFTableCell cell04 = xwpfTable.getRow(0).getCell(4);
        XWPFParagraph xwpfParagraph = cell04.getParagraphs().get(0);
        // 通过URL获取图片数据
        InputStream inputStream = new URL("http://127.0.0.1:1030/zyw/static/2024/06/25/lbxx_20240625141543A001.png").openStream();
        XWPFRun run = xwpfParagraph.createRun();
        run.addPicture(inputStream,
                Document.PICTURE_TYPE_PNG, "头像",
                Units.toEMU(150), Units.toEMU(150));
        // 设置垂直居中
        cell04.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
        for (XWPFParagraph para : cell04.getParagraphs()) {
            //居中
            para.setAlignment(ParagraphAlignment.CENTER);
        }
        inputStream.close();


    }

    /**
     * 表格1 (体育运动水平)
     *
     * @param document    文档
     * @param index       索引
     * @param sportsLevel 个人运动水平
     */
    public void handleTableOne(XWPFDocument document, Integer index, String sportsLevel) {
        // 获取所有段落
        List<XWPFParagraph> paragraphs = document.getParagraphs();
        // 在目标段落后添加一个新的段落
        XWPFParagraph paragraph = document.insertNewParagraph(paragraphs.get(index + 1).getCTP().newCursor().newCursor());
        // 设置段落的样式和属性,实现换行
        paragraph.setWordWrap(true); // 设置自动换行
        // 创建表格
        XmlCursor cursor = paragraph.getCTP().newCursor();
        // 在指定游标位置插入表格
        XWPFTable table = document.insertNewTbl(cursor);
        // 去除表格边框设置表格宽度
        OfficeUtils.setTableWidthToRemoveBorder(table, 7920);

        // 设置表格内容
        XWPFTableRow row0 = OfficeUtils.createRow(table, 0);
        OfficeUtils.setRowHeight(row0, 2);
        XWPFTableCell cell01 = OfficeUtils.createCell(row0, 0);
        XWPFRun run1 = cell01.getParagraphs().get(0).createRun();
        // 设置字体为宋体
        run1.setFontFamily("宋体");
        // 设置字号为四号(12磅)
        run1.setFontSize(12);
        run1.setText("您目前的体力活动:");
        OfficeUtils.setTheLandscapeHeader(cell01, 0.25);
        for (int i = 1; i <= 3; i++) {
            OfficeUtils.setsTheCellWidth(OfficeUtils.createCell(row0, i), 0.13);
        }
        if (StringUtils.isNotBlank(sportsLevel)) {
            XWPFTableCell cell;
            switch (sportsLevel) {
                case "体力活动不足":
                    cell = row0.getCell(1);
                    cell.setText("不足");
                    //大红色
                    cell.setColor("FF0000");
                    break;
                case "体力活动中等":
                    cell = row0.getCell(2);
                    cell.setText("中等");
                    //天蓝色
                    cell.setColor("4E95D9");
                    break;
                case "体力活动充分":
                    cell = row0.getCell(3);
                    cell.setText("充分");
                    //绿色
                    cell.setColor("00FF00");
                    break;
                default:
            }
        } else {
            XWPFTableCell cell = row0.getCell(1);
            cell.setText("暂无分析数据");
        }
        // 在目标段落后添加第二个新的段落
        XWPFParagraph paragraph2 = document.insertNewParagraph(paragraphs.get(index + 2).getCTP().newCursor().newCursor());
        // 设置段落的样式和属性,实现换行
        paragraph2.setWordWrap(true); // 设置自动换行
        // 创建表格
        XmlCursor cursor2 = paragraph2.getCTP().newCursor();
        // 在指定游标位置插入表格
        XWPFTable table2 = document.insertNewTbl(cursor2);
        // 去除表格边框设置表格宽度
        OfficeUtils.setTableWidthToRemoveBorder(table2, 7920);

        XWPFTableRow row1 = OfficeUtils.createRow(table2, 0);
        OfficeUtils.setRowHeight(row1, 2);
        XWPFTableCell cell11 = OfficeUtils.createCell(row1, 0);
        XWPFRun run2 = cell11.getParagraphs().get(0).createRun();
        // 设置字体为宋体
        run2.setFontFamily("宋体");
        // 设置字号为四号(12磅)
        run2.setFontSize(12);
        run2.setText("体力活动水平标尺:");
        OfficeUtils.setTheLandscapeHeader(cell11, 0.25);
        XWPFTableCell cell12 = OfficeUtils.createCell(row1, 1);
        cell12.setText("不足");
        OfficeUtils.setsTheCellWidth(cell12, 0.13);
        //大红色
        cell12.setColor("FF0000");
        XWPFTableCell cell13 = OfficeUtils.createCell(row1, 2);
        cell13.setText("中等");
        OfficeUtils.setsTheCellWidth(cell13, 0.13);

        //天蓝色
        cell13.setColor("4E95D9");

        XWPFTableCell cell14 = OfficeUtils.createCell(row1, 3);
        cell14.setText("充分");
        OfficeUtils.setsTheCellWidth(cell14, 0.13);
        //绿色
        cell14.setColor("00B050");

    }

    /**
     * 表格2
     *
     * @param document 文档
     * @param index    索引
     * @param height   身高
     * @param weight   体重
     */
    public void handleTableTwo(XWPFDocument document, Integer index, Double height, Double weight) {

        // 获取所有段落
        List<XWPFParagraph> paragraphs = document.getParagraphs();
        // 在目标段落后添加一个新的段落
        XWPFParagraph paragraph1 = document.insertNewParagraph(paragraphs.get(index + 1).getCTP().newCursor().newCursor());
        // 设置段落的样式和属性,实现换行
        paragraph1.setWordWrap(true); // 设置自动换行
        // 创建表格
        XmlCursor cursor1 = paragraph1.getCTP().newCursor();
        // 在指定游标位置插入表格
        XWPFTable table1 = document.insertNewTbl(cursor1);
        // 去除表格边框设置表格宽度
        OfficeUtils.setTableWidthToRemoveBorder(table1, 7920);

        // 设置表格内容
        XWPFTableRow row00 = OfficeUtils.createRow(table1, 0);
        OfficeUtils.setRowHeight(row00, 2);
        XWPFTableCell cell001 = OfficeUtils.createCell(row00, 0);
        XWPFRun run01 = cell001.getParagraphs().get(0).createRun();
        // 设置字体为宋体
        run01.setFontFamily("宋体");
        // 设置字号为四号(12磅)
        run01.setFontSize(12);
        run01.setText("您的体重:");
        OfficeUtils.setTheLandscapeHeader(cell001, 0.15);

        for (int i = 1; i <= 4; i++) {
            OfficeUtils.setsTheCellWidth(OfficeUtils.createCell(row00, i), 0.13);
        }
        XWPFTableCell cell011 = row00.getCell(1);
        if (Objects.nonNull(weight)) {
            cell011.setText(weight + "kg");
        } else {
            cell011.setText("暂未获得");
        }
        // 在目标段落后添加第二个新的段落
        XWPFParagraph paragraph = document.insertNewParagraph(paragraphs.get(index + 2).getCTP().newCursor().newCursor());
        // 在目标段落后添加新的段落
        // 设置段落的样式和属性,实现换行
        paragraph.setWordWrap(true); // 设置自动换行
        // 创建表格
        XmlCursor cursor = paragraph.getCTP().newCursor();
        // 在指定游标位置插入表格
        XWPFTable table = document.insertNewTbl(cursor);
        // 去除表格边框设置表格宽度
        OfficeUtils.setTableWidthToRemoveBorder(table, 7920);

        // 设置表格内容
        XWPFTableRow row0 = OfficeUtils.createRow(table, 0);
        OfficeUtils.setRowHeight(row0, 2);
        XWPFTableCell cell01 = OfficeUtils.createCell(row0, 0);
        XWPFRun run1 = cell01.getParagraphs().get(0).createRun();
        // 设置字体为宋体
        run1.setFontFamily("宋体");
        // 设置字号为四号(12磅)
        run1.setFontSize(12);
        run1.setText("您的体重指数:");
        OfficeUtils.setTheLandscapeHeader(cell01, 0.15);

        for (int i = 1; i <= 4; i++) {
            OfficeUtils.setsTheCellWidth(OfficeUtils.createCell(row0, i), 0.13);
        }
        Double bmi = calculateBmi(height, weight);
        if (Objects.nonNull(bmi)) {
            XWPFTableCell cell = null;
            if (bmi <= 18.5) {
                cell = row0.getCell(1);
                //天蓝色
                cell.setColor("4E95D9");
            } else if (bmi <= 24) {
                cell = row0.getCell(2);
                //绿色
                cell.setColor("00B050");
            } else if (bmi <= 28) {
                cell = row0.getCell(3);
                //黄色
                cell.setColor("FFC000");
            } else {
                cell = row0.getCell(4);
                //大红色
                cell.setColor("FF0000");
            }
            cell.setText(String.valueOf(bmi));
        }

        // 在目标段落后添加第三个新的段落
        XWPFParagraph paragraph2 = document.insertNewParagraph(paragraphs.get(index + 3).getCTP().newCursor().newCursor());

        // 设置段落的样式和属性,实现换行
        paragraph2.setWordWrap(true); // 设置自动换行
        // 创建表格
        XmlCursor cursor2 = paragraph2.getCTP().newCursor();
        // 在指定游标位置插入表格
        XWPFTable table2 = document.insertNewTbl(cursor2);
        // 去除表格边框设置表格宽度
        OfficeUtils.setTableWidthToRemoveBorder(table2, 7920);

        XWPFTableRow row1 = OfficeUtils.createRow(table2, 0);
        OfficeUtils.setRowHeight(row1, 2);
        XWPFTableCell cell11 = OfficeUtils.createCell(row1, 0);
        XWPFRun run2 = cell11.getParagraphs().get(0).createRun();
        // 设置字体为宋体
        run2.setFontFamily("宋体");
        // 设置字号为四号(12磅)
        run2.setFontSize(12);
        run2.setText("体重指数标尺:");
        OfficeUtils.setTheLandscapeHeader(cell11, 0.15);
        XWPFTableCell cell12 = OfficeUtils.createCell(row1, 1);
        cell12.setText("0~18.5");
        OfficeUtils.setsTheCellWidth(cell12, 0.13);
        //天蓝色
        cell12.setColor("4E95D9");
        XWPFTableCell cell13 = OfficeUtils.createCell(row1, 2);
        cell13.setText("18.6~24");
        OfficeUtils.setsTheCellWidth(cell13, 0.13);
        //绿色
        cell13.setColor("00B050");

        XWPFTableCell cell14 = OfficeUtils.createCell(row1, 3);
        cell14.setText("24.1~28");
        OfficeUtils.setsTheCellWidth(cell14, 0.13);
        //黄色
        cell14.setColor("FFC000");

        XWPFTableCell cell15 = OfficeUtils.createCell(row1, 4);
        cell15.setText("<28");
        OfficeUtils.setsTheCellWidth(cell15, 0.13);

        //大红色
        cell15.setColor("FF0000");
    }

    /**
     * 计算BMI
     *
     * @param height 身高
     * @param weight 体重
     * @return
     */
    public Double calculateBmi(Double height, Double weight) {
        height = height / 100;
        if (height <= 0 || weight <= 0) {
            log.error("身高和体重必须是正数!");
            return null;
        }
        DecimalFormat df = new DecimalFormat("#.##");
        return Double.parseDouble(df.format(weight / (height * height)));
    }

    @Override
    public void getHealthReportWord(XWPFDocument document, HealthReportQuery query, HttpServletResponse response) {
        OfficeUtils.processingWordResponses("健康报告-" + query.getName(), OfficeUtils.writeDocumentToInputStream(document), response);
    }
}

图片这里我使用的是在本地文件服务上上传的一张图片,关于文件服务的搭建可以阅读下面这篇博客:
Java实现对象存储的4种方式(本地对象存储、MINIO、阿里云OSS、FastDFS)

3.2 Word工具类OfficeUtils

抽出公共部分代码,编写静态方法到工具类中,解耦合。

这里仅列出这部分功能涉及的静态方法,上诉功能已列出的方法这里不展示。

package com.example.demo.uitls;

import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;

import java.lang.reflect.Field;
import java.io.*;
import java.math.BigInteger;
import java.util.*;

/**
 * OfficeUtils : Office工具类
 *
 * @author zyw
 * @create 2024-06-24  16:35
 */

public class OfficeUtils {

    /**
     * 设置表格宽度去除边框
     * @param table 表格
     * @param width 宽度值
     */
    public static void setTableWidthToRemoveBorder(XWPFTable table,Integer width) {
        // 去除表格边框
        CTTblPr tblPr2 = table.getCTTbl().getTblPr();
        CTTblBorders borders2 = tblPr2.addNewTblBorders();
        borders2.addNewBottom().setVal(STBorder.NONE);
        borders2.addNewTop().setVal(STBorder.NONE);
        borders2.addNewLeft().setVal(STBorder.NONE);
        borders2.addNewRight().setVal(STBorder.NONE);
        borders2.addNewInsideH().setVal(STBorder.NONE);
        borders2.addNewInsideV().setVal(STBorder.NONE);

        // 设置表格整体样式
        tblPr2.addNewTblW().setW(BigInteger.valueOf(width)); // 设置表格宽度
    }

    /**
     * 设置表格单元格宽度及文本居中
     *
     * @param cell 单元格
     * @param width 宽度占比
     */
    public static void setTheLandscapeHeader(XWPFTableCell cell, double width) {
        setsTheCellWidth(cell, width);
        // 获取单元格属性对象
        CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
        // 设置垂直对齐方式为居中
        CTVerticalJc vJc = tcPr.isSetVAlign() ? tcPr.getVAlign() : tcPr.addNewVAlign();
        vJc.setVal(STVerticalJc.CENTER);

    }

    /**
     * 设置表格单元格宽度
     *
     * @param cell  单元格
     * @param width 宽度占比
     */
    public static void setsTheCellWidth(XWPFTableCell cell, double width) {
        // 假设A4纸宽约为210mm,1mm=360EMU,则A4宽约为7920EMU
        int emuFor30Percent = (int) (7920 * width);
        CTTblWidth ctTblWidth = cell.getCTTc().addNewTcPr().addNewTcW();
        // 设置宽度为2000EMU,你可以根据需要调整这个值
        ctTblWidth.setW(BigInteger.valueOf(emuFor30Percent));
        // 设置宽度类型为字符单位(也可以是其他单位,如百分比等)
        ctTblWidth.setType(STTblWidth.PCT);
        // 设置垂直居中
        cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
        for (XWPFParagraph para : cell.getParagraphs()) {
            //居中
            para.setAlignment(ParagraphAlignment.CENTER);
        }
    }

    /**
     * 设置表格行的高度
     *
     * @param row      行
     * @param heightCm 高度占比
     */
    public static void setRowHeight(XWPFTableRow row, double heightCm) {
        int emuForHeight = (int) (360 * heightCm);
        CTTrPr trPr = row.getCtRow().addNewTrPr();
        CTHeight ht = trPr.addNewTrHeight();
        ht.setVal(BigInteger.valueOf(emuForHeight));
    }

    /**
     * 创建表格行
     *
     * @param table 表格
     * @param index 行索引
     * @return
     */
    public static XWPFTableRow createRow(XWPFTable table, int index) {
        return Objects.isNull(table.getRow(index)) ? table.createRow() : table.getRow(index);
    }

    /**
     * 创建单元格
     *
     * @param row   行
     * @param index 列索引
     * @return
     */
    public static XWPFTableCell createCell(XWPFTableRow row, int index) {
        return Objects.isNull(row.getCell(index)) ? row.createCell() : row.getCell(index);
    }

    /**
     * 获取文本在文档中的索引
     *
     * @param doc  文档
     * @param text 文本标识
     * @return
     */
    public static int findParagraphIndexByText(XWPFDocument doc, String text) {
        // 获取所有段落
        List<XWPFParagraph> paragraphs = doc.getParagraphs();
        // 查找目标段落
        int targetParagraphIndex = -1;
        for (int i = 0; i < paragraphs.size(); i++) {
            if (paragraphs.get(i).getText().contains(text)) {
                targetParagraphIndex = i;
                break;
            }
        }
        return targetParagraphIndex;
    }

}

3.3 导出效果

Word效果:
在这里插入图片描述

PDF效果:

在这里插入图片描述

3.4 动态表格

    /**
     * 个人健康报告模板
     */
    public static final String PERSONAL_HEALTH_REPORT_TEMPLATE = "classpath:templates/HealthReport.docx";

    private static final Pattern pattern = Pattern.compile("(\\d+、[^\\d]+)");

    private static final String HEADER_1_1 = "您的基本信息";
    private static final String HEADER_1_3 = "您目前的体育运动水平";
    private static final String HEADER_1_4 = "您的体重指数";
    private static final String HEADER_2_1 = "营养成分摄入比例";
    private static final String HEADER_2_2 = "心率血氧检查";
    private static final String HEADER_2_3 = "睡眠质量趋势";
    private static final String HEADER_3_1 = "专家建议";    

    @Override
    public XWPFDocument getHealthReport(HealthReportQuery query) {
        try {
            FileInputStream fileInputStream = SpringUtils.convertInputStreamToFileInputStream(resourceLoader.getResource(PERSONAL_HEALTH_REPORT_TEMPLATE).getInputStream());
            XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream);
            // 替换文本数据构建
            OfficeUtils.paragraphTextFilling(xwpfDocument, OfficeUtils.objectToMap(query));
            // 在基本信息表格中填充数据
            fillInTable(xwpfDocument, query);
            // 插入体育运动水平表格
            int index3 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_3);
            handleTableOne(xwpfDocument, index3, query.getSportsLevel());
            // 插入体重指数表格
            int index4 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_4);
            handleTableTwo(xwpfDocument, index4, query.getHeight(), query.getWeight());
            // 插入历史体重
            int index5 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_1);
            insertChartOne(xwpfDocument, index5);
            // 插入心率检查
            int index6 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_2);
            insertChartTwo(xwpfDocument, index6);
            // 插入睡眠质量趋势
            int index7 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_3);
            insertChartThree(xwpfDocument, index7);
            // 插入体格检查动态列表
            int index8 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_3_1);
            dynamicList(xwpfDocument, index8);
            return xwpfDocument;
        } catch (Exception e) {
            log.info("获取健康报告失败", e);
            return null;
        }
    }    

     /**
     * 动态列表 专家建议
     *
     * @param document 文档
     * @param index    索引
     */
    public void dynamicList(XWPFDocument document, Integer index) {
        // 数据源
        Map<String, String> map1 = new HashMap<>();
        map1.put("code", "⽢油三酯增⾼");
        map1.put("content", "1、建议限酒,低脂、低胆固醇饮⻝,如少吃油腻及煎烤类⻝物,少吃动物内脏等,多⻝蔬菜⽔果。加强运动,促进脂质代谢。2、每三-六个⽉复查⾎脂和肝脏B超⼀次,复查前请低脂饮⻝三天。如⾎脂持续增⾼,请在医⽣指导下使⽤调脂药物。");
        Map<String, String> map2 = new HashMap<>();
        map2.put("code", "肌酐增⾼");
        map2.put("content", "1、肌酐是临床常规肾功能试验之⼀。肌酐是肌酸的代谢产物,98%的肌酸存在于肌⾁,为肌⾁收缩时的能量来源,释放能量后变为肌酐,由肾脏排泄。2、肌酐增⾼⻅于肾脏损害,急、慢性肾功能不全及⼼功能不全等。3、建议到医院肾内科就诊进⼀步检查,明确诊断。");
        Map<String, String> map3 = new HashMap<>();
        map3.put("code", "屈光不正");
        map3.put("content", "注意⽤眼卫⽣,定期眼科随访。");
        List<Map<String, String>> list = List.of(map1, map2, map3);
        for (int i = 0; i < list.size(); i++) {
            XWPFParagraph xwpfParagraph = OfficeUtils.insertNewParagraph(document.getParagraphs(), document, index + i);
            // 创建表格
            XmlCursor cursor = xwpfParagraph.getCTP().newCursor();
            // 在指定游标位置插入表格
            XWPFTable table = document.insertNewTbl(cursor);
            CTTblPr tblPr = table.getCTTbl().getTblPr();
            // 设置表格整体样式
            tblPr.addNewTblW().setW(BigInteger.valueOf(7920)); // 设置表格宽度
            XWPFTableRow dataRow = OfficeUtils.createRow(table, 0);
            // 创建第一列
            XWPFTableCell cell1 = OfficeUtils.createCell(dataRow, 0);
            // 在段落中创建一个新的文本运行
            XWPFRun run1 = cell1.getParagraphs().get(0).createRun();
            // 设置字体为宋体
            run1.setFontFamily("宋体");
            // 设置字号为四号(14磅)
            run1.setFontSize(14);
            // 添加文本内容
            run1.setText(list.get(i).get("code"));
            OfficeUtils.setTheLandscapeHeader(cell1, 0.2);
            // 使用十六进制颜色码,这里是灰色
            cell1.setColor("C0C0C0");
            // 创建第二列
            XWPFTableCell cell2 = OfficeUtils.createCell(dataRow, 1);
            // 清空单元格内容(可选,如果需要)
            cell2.removeParagraph(0);
            // 截断内容为多个段落
            Matcher matcher = pattern.matcher(list.get(i).get("content").trim());
            List<String> matches = new ArrayList<>();
            while (matcher.find()) {
                matches.add(matcher.group());
            }
            if (matches.size() == 0) {
                matches.add(list.get(i).get("content").trim());
            }
            for (int j = 0; j < matches.size(); j++) {
                XWPFParagraph para = cell2.addParagraph();
                if (j != 0){
                    para = cell2.addParagraph();
                }
                // 添加文本内容
                para.createRun().setText(matches.get(j));
                para.setAlignment(ParagraphAlignment.LEFT); // 设置对齐方式
            }
            OfficeUtils.setsTheCellWidthLeft(cell2, 0.8);
        }
    }

在这里插入图片描述

4、自定义图表

4.1 思路

JFreeChart

JFreeChart是一个开源的Java图表库,专为JAVA平台设计,用于生成高质量的2D图表。

4.1.1 概述
  • JFreeChart是一个完全使用JAVA语言编写的图表绘制类库。
  • 它最初由David Gilbert创建,自2001年以来一直在持续开发和更新,目前已成为Java社区中广泛使用的图表库之一。
  • JFreeChart是一个开源项目,遵循GNU通用公共许可证(LGPL),允许在专有应用程序中使用。
4.1.2 支持的图表类型
  • JFreeChart支持多种图表类型,包括但不限于:
    • 饼图(Pie charts)
    • 柱状图(Bar charts)
    • 散点图(Scatter plots)
    • 时序图(Time series)
    • 甘特图(Gantt charts)
    • 线形图(Line charts)
    • 气泡图(Bubble charts)
    • 热力图(Heatmaps)
4.1.3 特性
  • 定制能力:提供大量的定制选项,包括颜色、字体、标签、图例、网格线、数据点等,以满足各种设计需求。
  • 数据源:接受各种数据结构作为输入,如数组、列表或CategoryDataset和TimeSeriesDataset对象。
  • 输出类型:支持多种输出类型,包括Swing组件、图像文件(PNG、JPEG)、矢量图形文件格式(PDF、EPS、SVG)等。
  • 交互性:具有一定的交互功能,如缩放、平移等。

通过 JFreeChart 创建图表,将图表转换为图像格式(如PNG或JPEG),然后将图像解析成InputStream 写入到Word文档的相应位置中。

4.2 准备模板

在这里插入图片描述

4.3 导入依赖

        <dependency>
            <groupId>org.jfree</groupId>
            <artifactId>jfreechart</artifactId>
            <version>1.5.3</version>
        </dependency>

4.4 图表生成工具类 ChartWithChineseExample

在使用org.jfree.chart库生成图表时,如果遇到中文无法正常显示的问题,通常是字体设置的问题。JFreeChart默认使用的字体可能不支持中文字符。要解决这个问题,你需要指定一个支持中文的字体。以下是解决此问题的一般步骤:

步骤 1: 准备字体文件

首先,你需要一个支持中文的TrueType字体文件(.ttf),如宋体(SimSun.ttf)、微软雅黑(msyh.ttf)等。这些字体文件通常可以在Windows系统的C:\Windows\Fonts目录下找到,或者你可以从互联网上下载。

字体文件包可以从这里下载:office字体文件包

步骤 2: 注册字体到FontFactory

在你的Java程序中,使用FontFactory.register()方法注册你的中文字体文件。例如,如果你有SimSun.ttf这个字体文件,可以这样做:

    /**
     * 注册中文字体
     */
    public static void registerChineseFont() {
        // 注册中文字体(这里假设已经将字体文件放置在项目的resources目录下)
        InputStream fontStream = ChartWithChineseExample.class.getResourceAsStream("/font/SIMSUN.TTC"); // 路径根据实际情况调整
        try {
            Font customFont = Font.createFont(Font.TRUETYPE_FONT, fontStream).deriveFont(Font.PLAIN, 12);
            GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
            ge.registerFont(customFont);
        } catch (FontFormatException | IOException e) {
            e.printStackTrace();
        }
    }
步骤 3: 设置图表具体位置的字体
柱状图:
        // 示例字体为宋体,常规,14号
        Font axisLabelFont = new Font("SimSun", Font.PLAIN, 14);
        // X轴
        chart.getCategoryPlot().getDomainAxis().setLabelFont(axisLabelFont);
        chart.getCategoryPlot().getDomainAxis().setTickLabelFont(axisLabelFont);
        // Y轴
        chart.getCategoryPlot().getRangeAxis().setLabelFont(axisLabelFont);
        chart.getCategoryPlot().getRangeAxis().setTickLabelFont(axisLabelFont);
饼图:
        chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
        chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 12));
        // 获取饼图的plot对象,以便进行进一步定制
        PiePlot3D plot = (PiePlot3D) chart.getPlot();
        // 设置标签字体
        plot.setLabelFont(new Font("SimSun", Font.PLAIN, 14));
        // 设置无数据信息字体(如果需要)
        plot.setNoDataMessageFont(new Font("SimSun", Font.PLAIN, 18));
折线图:
        // 设置字体
        chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
        chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 12));
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        plot.getDomainAxis().setLabelFont(new Font("SimSun", Font.PLAIN, 12));
        plot.getRangeAxis().setLabelFont(new Font("SimSun", Font.PLAIN, 12));
        plot.getDomainAxis().setTickLabelFont(new Font("SimSun", Font.PLAIN, 12));
        plot.getRangeAxis().setTickLabelFont(new Font("SimSun", Font.PLAIN, 12));
完整代码:
package com.example.demo.uitls;

import lombok.extern.slf4j.Slf4j;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PiePlot3D;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;
import org.springframework.stereotype.Component;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;

/**
 * ChartWithChineseExample : 图表生成工具类
 *
 * @author zyw
 * @create 2024-06-25  16:20
 */

@Slf4j
@Component
public class ChartWithChineseExample {

    // 柱状图临时文件名
    public final static String BAR_CHART_FILE_NAME = "BAR_CHART.png";
    // 饼图临时文件名
    public final static String PIE_CHART_FILE_NAME = "PIE_CHART.png";
    // 折线图临时文件名
    public final static String LINE_CHART_FILE_NAME = "LINE_CHART.png";

    public static InputStream lineChartGeneration(String title, String x, String y, DefaultCategoryDataset dataset) {
        registerChineseFont();

        JFreeChart chart = ChartFactory.createLineChart(
                title, // 图表标题
                x,       // X轴标签
                y,         // Y轴标签
                dataset,      // 数据集
                PlotOrientation.VERTICAL, // 图表方向
                true,        // 是否显示图例
                true,        // 是否生成工具提示
                false        // 是否生成URL链接
        );
        chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
        chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 14));
        // 示例字体为宋体,常规,14号
        Font axisLabelFont = new Font("SimSun", Font.PLAIN, 14);
        // X轴
        chart.getCategoryPlot().getDomainAxis().setLabelFont(axisLabelFont);
        chart.getCategoryPlot().getDomainAxis().setTickLabelFont(axisLabelFont);
        // Y轴
        chart.getCategoryPlot().getRangeAxis().setLabelFont(axisLabelFont);
        chart.getCategoryPlot().getRangeAxis().setTickLabelFont(axisLabelFont);
        try {
            // 将图表转换为字节数组
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            BufferedImage chartImage = chart.createBufferedImage(400, 300); // 设置图像的宽高
            ImageIO.write(chartImage, "png", outputStream);
            byte[] chartBytes = outputStream.toByteArray();
            // 将字节数组转换为InputStream
            InputStream inputStream = new ByteArrayInputStream(chartBytes);
            return inputStream;
        } catch (IOException e) {
            log.error("折线图图生成异常");
            return null;
        }
    }

    /**
     * 饼图生成
     *
     * @param title   标题
     * @param dataset 数据集
     * @return
     */
    public static InputStream pieChartGeneration(String title, DefaultPieDataset dataset) {
        registerChineseFont();
        // 使用数据集创建饼图
        JFreeChart chart = ChartFactory.createPieChart3D(
                title, // 图表标题
                dataset, // 数据集
                true, // 是否显示图例
                true, // 是否生成工具提示
                false // 是否生成URL链接
        );
        chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
        chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 12));
        // 获取饼图的plot对象,以便进行进一步定制
        PiePlot3D plot = (PiePlot3D) chart.getPlot();
        // 设置标签字体
        plot.setLabelFont(new Font("SimSun", Font.PLAIN, 14));
        // 设置无数据信息字体(如果需要)
        plot.setNoDataMessageFont(new Font("SimSun", Font.PLAIN, 18));

        try {
            // 将图表转换为字节数组
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            BufferedImage chartImage = chart.createBufferedImage(400, 300); // 设置图像的宽高
            ImageIO.write(chartImage, "png", outputStream);
            byte[] chartBytes = outputStream.toByteArray();
            // 将字节数组转换为InputStream
            InputStream inputStream = new ByteArrayInputStream(chartBytes);
            return inputStream;
        } catch (IOException e) {
            log.error("饼图生成异常");
            return null;
        }
    }

    /**
     * 注册中文字体
     */
    public static void registerChineseFont() {
        // 注册中文字体(这里假设已经将字体文件放置在项目的resources目录下)
        InputStream fontStream = ChartWithChineseExample.class.getResourceAsStream("/font/SIMSUN.TTC"); // 路径根据实际情况调整
        try {
            Font customFont = Font.createFont(Font.TRUETYPE_FONT, fontStream).deriveFont(Font.PLAIN, 12);
            GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
            ge.registerFont(customFont);
        } catch (FontFormatException | IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 创建柱状图表
     *
     * @param dataset 数据集
     * @return
     */
    public static InputStream createChartPanel(String title, String x, String y, DefaultCategoryDataset dataset) {
        registerChineseFont();
        // 创建图表
        JFreeChart chart = ChartFactory.createBarChart(
                title, // 图表标题
                x, // X轴标签
                y, // Y轴标签
                dataset,
                PlotOrientation.VERTICAL,
                true, // 是否显示图例
                true, // 是否使用工具提示
                false // 是否生成URL链接
        );

        // 设置字体
        chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
        chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 12));
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        plot.getDomainAxis().setLabelFont(new Font("SimSun", Font.PLAIN, 12));
        plot.getRangeAxis().setLabelFont(new Font("SimSun", Font.PLAIN, 12));
        plot.getDomainAxis().setTickLabelFont(new Font("SimSun", Font.PLAIN, 12));
        plot.getRangeAxis().setTickLabelFont(new Font("SimSun", Font.PLAIN, 12));
        try {
            // 将图表转换为字节数组
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            BufferedImage chartImage = chart.createBufferedImage(400, 300); // 设置图像的宽高
            ImageIO.write(chartImage, "png", outputStream);
            byte[] chartBytes = outputStream.toByteArray();
            // 将字节数组转换为InputStream
            InputStream inputStream = new ByteArrayInputStream(chartBytes);
            return inputStream;
        } catch (IOException e) {
            log.error("柱状图生成异常");
            return null;
        }
    }

}

4.5 业务层 OfficeServicel

在word中遍历所有段落,找到需要插入图表的段落索引。

此处省略上诉已展示代码。

/**
 * OfficeServiceImpl :
 *
 * @author zyw
 * @create 2024-06-24  15:41
 */
@Service
@Slf4j
public class OfficeServiceImpl implements OfficeService {

    private static final String HEADER_2_1 = "营养成分摄入比例";
    private static final String HEADER_2_2 = "心率血氧检查";
    private static final String HEADER_2_3 = "睡眠质量趋势";
    
    @Override
    public XWPFDocument getHealthReport(HealthReportQuery query) {
        try {
            FileInputStream fileInputStream = SpringUtils.convertInputStreamToFileInputStream(resourceLoader.getResource(PERSONAL_HEALTH_REPORT_TEMPLATE).getInputStream());
            XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream);
            // 替换文本数据构建
            OfficeUtils.paragraphTextFilling(xwpfDocument, OfficeUtils.objectToMap(query));
            // 在基本信息表格中填充数据
            fillInTable(xwpfDocument, query);
            // 插入体育运动水平表格
            int index3 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_3);
            handleTableOne(xwpfDocument, index3, query.getSportsLevel());
            // 插入体重指数表格
            int index4 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_4);
            handleTableTwo(xwpfDocument, index4, query.getHeight(), query.getWeight());
            // 插入历史体重
            int index5 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_1);
            insertChartOne(xwpfDocument, index5);
            // 插入心率检查
            int index6 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_2);
            insertChartTwo(xwpfDocument, index6);
            // 插入睡眠质量趋势
            int index7 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_3);
            insertChartThree(xwpfDocument, index7);
            return xwpfDocument;
        } catch (Exception e) {
            log.info("获取健康报告失败", e);
            return null;
        }
    }

    /**
     * 获取文本在文档中的索引
     *
     * @param doc  文档
     * @param text 文本标识
     * @return
     */
    public static int findParagraphIndexByText(XWPFDocument doc, String text) {
        // 获取所有段落
        List<XWPFParagraph> paragraphs = doc.getParagraphs();
        // 查找目标段落
        int targetParagraphIndex = -1;
        for (int i = 0; i < paragraphs.size(); i++) {
            if (paragraphs.get(i).getText().contains(text)) {
                targetParagraphIndex = i;
                break;
            }
        }
        return targetParagraphIndex;
    }
    
    /**
     * 插入图表 1
     *
     * @param document
     * @param index
     * @throws Exception
     */
    public void insertChartOne(XWPFDocument document, Integer index) throws Exception {
        // 填充图表数据
        DefaultPieDataset<String> dataset = new DefaultPieDataset<String>();
        dataset.setValue("碳水化合物(30%)", 30);
        dataset.setValue("蛋白质(30%)", 30);
        dataset.setValue("脂肪(25%)", 25);
        dataset.setValue("纤维等营养素(15%)", 15);
        // 创建图表示例
        InputStream chartPanel = ChartWithChineseExample.pieChartGeneration("营养成分摄入比例", dataset);
        // 获取所有段落
        List<XWPFParagraph> paragraphs = document.getParagraphs();
        // 在目标段落后添加一个新的段落
        XWPFParagraph paragraph = document.insertNewParagraph(paragraphs.get(index + 1).getCTP().newCursor().newCursor());
        // 设置段落的样式和属性,实现换行
        paragraph.setWordWrap(true); // 设置自动换行
        // 设置段落水平居中
        paragraph.setAlignment(ParagraphAlignment.CENTER);
        // 设置段落内文字(这里是空格)垂直居中
        paragraph.setVerticalAlignment(TextAlignment.CENTER);
        // 调整行距以确保图片上下居中,这一步可能需要根据实际情况调整
        XWPFRun run = paragraph.createRun();
        run.addPicture(chartPanel, XWPFDocument.PICTURE_TYPE_PNG, ChartWithChineseExample.BAR_CHART_FILE_NAME, Units.toEMU(400), Units.toEMU(300));
    }

    /**
     * 插入图表2 心率血氧
     *
     * @param document
     * @param index
     */
    public void insertChartTwo(XWPFDocument document, Integer index) throws Exception {
        // 填充图表数据
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();
        dataset.addValue(77, "心率", "2024-06-23");
        dataset.addValue(85, "心率", "2024-06-24");
        dataset.addValue(99, "心率", "2024-06-25");
        dataset.addValue(92.76, "血氧饱和度", "2024-06-23");
        dataset.addValue(98.74, "血氧饱和度", "2024-06-24");
        dataset.addValue(94.2, "血氧饱和度", "2024-06-25");
        // 创建图表示例
        InputStream chartPanel = ChartWithChineseExample.createChartPanel("心率和血氧饱和度图表", "日期", "心率(次/分)、血氧饱和度(%)", dataset);
        // 获取所有段落
        List<XWPFParagraph> paragraphs = document.getParagraphs();
        // 在目标段落后添加一个新的段落
        XWPFParagraph paragraph = document.insertNewParagraph(paragraphs.get(index + 1).getCTP().newCursor().newCursor());
        // 设置段落的样式和属性,实现换行
        paragraph.setWordWrap(true); // 设置自动换行
        // 设置段落水平居中
        paragraph.setAlignment(ParagraphAlignment.CENTER);
        // 设置段落内文字(这里是空格)垂直居中
        paragraph.setVerticalAlignment(TextAlignment.CENTER);
        // 调整行距以确保图片上下居中,这一步可能需要根据实际情况调整
        XWPFRun run = paragraph.createRun();
        run.addPicture(chartPanel, XWPFDocument.PICTURE_TYPE_PNG, ChartWithChineseExample.PIE_CHART_FILE_NAME, Units.toEMU(400), Units.toEMU(300));
    }

    /**
     * 插入图表3 睡眠质量趋势
     *
     * @param document
     * @param index
     * @throws Exception
     */
    public void insertChartThree(XWPFDocument document, Integer index) throws Exception {
        // 填充图表数据
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();
        dataset.addValue(7.8, "起床时间", "06/18");
        dataset.addValue(8, "起床时间", "06/19");
        dataset.addValue(7.5, "起床时间", "06/20");
        dataset.addValue(8.3, "起床时间", "06/21");
        dataset.addValue(9, "起床时间", "06/22");
        dataset.addValue(9.5, "起床时间", "06/23");
        dataset.addValue(23, "睡眠时间", "06/18");
        dataset.addValue(24, "睡眠时间", "06/19");
        dataset.addValue(22.6, "睡眠时间", "06/20");
        dataset.addValue(23.2, "睡眠时间", "06/21");
        dataset.addValue(21.8, "睡眠时间", "06/22");
        dataset.addValue(23.7, "睡眠时间", "06/23");
        // 创建图表示例
        InputStream chartPanel = ChartWithChineseExample.lineChartGeneration("睡眠质量趋势", "日期", "睡眠时间", dataset);
        // 获取所有段落
        List<XWPFParagraph> paragraphs = document.getParagraphs();
        // 在目标段落后添加一个新的段落
        XWPFParagraph paragraph = OfficeUtils.insertNewParagraph(paragraphs, document,index);
        // 设置段落的样式和属性,实现换行
        paragraph.setWordWrap(true); // 设置自动换行
        // 设置段落水平居中
        paragraph.setAlignment(ParagraphAlignment.CENTER);
        // 设置段落内文字(这里是空格)垂直居中
        paragraph.setVerticalAlignment(TextAlignment.CENTER);
        // 调整行距以确保图片上下居中,这一步可能需要根据实际情况调整
        XWPFRun run = paragraph.createRun();
        run.addPicture(chartPanel, XWPFDocument.PICTURE_TYPE_PNG, ChartWithChineseExample.LINE_CHART_FILE_NAME, Units.toEMU(400), Units.toEMU(300));
    }
}

4.6 导出效果

Word:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部