feat: 增加了word文档导出工具方法

This commit is contained in:
tangwei 2026-09-07 18:03:22 +08:00
parent 750445cbdd
commit 1c80c33bd6
10 changed files with 668 additions and 0 deletions

View File

@ -38,6 +38,7 @@
<jsoup.version>1.11.3</jsoup.version>
<jasypt.version>1.16</jasypt.version>
<ip2region.version>1.7.2</ip2region.version>
<jfreechart.version>1.5.4</jfreechart.version>
<easy.captcha.version>1.6.2</easy.captcha.version>
<useragentutils.version>1.21</useragentutils.version>
<gson.version>2.10.1</gson.version>
@ -256,6 +257,13 @@
<version>${poi.ooxml.version}</version>
</dependency>
<!-- JFreeChart 图表渲染Word 图表导出) -->
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>${jfreechart.version}</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>

View File

@ -0,0 +1,40 @@
package com.yfd.platform.qgc_export.controller;
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
import com.yfd.platform.qgc_export.service.IWordChartExportService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Word 图表导出控制器
* <p>
* 支持饼图柱状图折线图折柱混合图堆叠柱状图堆叠柱状图归一化百分比等图形导出到 Word
*/
@RestController
@RequestMapping("/qgcExport/word")
@Tag(name = "Word 图表导出")
@Validated
public class WordChartExportController {
@Resource
private IWordChartExportService wordChartExportService;
/**
* 导出 Word 文档含多种图表
*
* @param request 文档标题 + 图表列表
* @param response HttpServletResponse
*/
@PostMapping("/export")
@Operation(summary = "导出 Word 文档(饼图/柱状图/折线图/折柱混合/堆叠/归一化堆叠)")
public void exportWord(@RequestBody WordChartExportRequest request, HttpServletResponse response) {
wordChartExportService.exportWord(request, response);
}
}

View File

@ -0,0 +1,52 @@
package com.yfd.platform.qgc_export.domain.word;
import lombok.Getter;
/**
* Word 导出支持的图表类型
*/
@Getter
public enum ChartType {
/** 饼图 */
PIE("饼图"),
/** 柱状图 */
BAR("柱状图"),
/** 折线图 */
LINE("折线图"),
/** 折柱混合图 */
BAR_LINE("折柱混合图"),
/** 堆叠柱状图 */
STACKED_BAR("堆叠柱状图"),
/** 堆叠柱状图(归一化/百分比) */
PERCENT_STACKED_BAR("堆叠柱状图归一化");
private final String desc;
ChartType(String desc) {
this.desc = desc;
}
/**
* 忽略大小写解析图表类型
*/
public static ChartType from(String value) {
if (value == null || value.isBlank()) {
return null;
}
String normalized = value.trim().replace("-", "_").toUpperCase();
for (ChartType type : values()) {
if (type.name().equals(normalized)) {
return type;
}
}
// 兼容别名
return switch (normalized) {
case "BARLINE", "BAR_LINE", "LINECOLUMN", "MIXED" -> BAR_LINE;
case "STACKED", "STACKEDBAR" -> STACKED_BAR;
case "PERCENT_STACKED", "PERCENTSTACKEDBAR", "NORMALIZED" -> PERCENT_STACKED_BAR;
case "COLUMN" -> BAR;
default -> null;
};
}
}

View File

@ -0,0 +1,24 @@
package com.yfd.platform.qgc_export.domain.word;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* Word 图表导出请求
*/
@Data
@Schema(description = "Word 图表导出请求")
public class WordChartExportRequest {
@Schema(description = "文档标题")
private String title;
@Schema(description = "文档副标题/说明(可选)")
private String subtitle;
@Schema(description = "图表列表,按顺序依次插入文档")
private List<WordChartVo> charts = new ArrayList<>();
}

View File

@ -0,0 +1,39 @@
package com.yfd.platform.qgc_export.domain.word;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* Word 文档中的单个图表描述
*/
@Data
@Schema(description = "Word 图表")
public class WordChartVo {
@Schema(description = "图表类型pie/bar/line/barLine/stackedBar/percentStackedBar")
private String type;
@Schema(description = "图表标题")
private String title;
@Schema(description = "X 轴名称")
private String xAxisName;
@Schema(description = "Y 轴(左轴)名称")
private String yAxisName;
@Schema(description = "Y 轴(右轴)名称,折柱混合图用")
private String yAxisNameRight;
@Schema(description = "X 轴分类(如月份、站点名等)")
private List<String> categories = new ArrayList<>();
@Schema(description = "系列数据列表")
private List<WordSeriesVo> series = new ArrayList<>();
@Schema(description = "是否显示数值标签")
private boolean showValues = true;
}

View File

@ -0,0 +1,24 @@
package com.yfd.platform.qgc_export.domain.word;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 图表中的单个系列折柱混合图中可指定主轴/副轴
*/
@Data
@Schema(description = "图表系列")
public class WordSeriesVo {
@Schema(description = "系列名称(图例)")
private String name;
@Schema(description = "系列数值,与 categories 一一对应")
private List<Double> values = new ArrayList<>();
@Schema(description = "Y 轴索引0=主轴(左), 1=副轴(右),默认 0。折柱混合图用")
private Integer yAxisIndex = 0;
}

View File

@ -0,0 +1,18 @@
package com.yfd.platform.qgc_export.service;
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* Word 图表导出服务
*/
public interface IWordChartExportService {
/**
* 根据图表配置生成 Word 文档并写入响应
*
* @param request 文档标题 + 图表列表
* @param response HttpServletResponse
*/
void exportWord(WordChartExportRequest request, HttpServletResponse response);
}

View File

@ -0,0 +1,52 @@
package com.yfd.platform.qgc_export.service.impl;
import cn.hutool.core.util.StrUtil;
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
import com.yfd.platform.qgc_export.service.IWordChartExportService;
import com.yfd.platform.qgc_export.util.WordChartExportUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* Word 图表导出服务实现
*/
@Service
public class WordChartExportServiceImpl implements IWordChartExportService {
@Override
public void exportWord(WordChartExportRequest request, HttpServletResponse response) {
byte[] data;
try {
data = WordChartExportUtil.generateWord(request);
} catch (IOException e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
String fileName = resolveFileName(request);
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentLength(data.length);
response.setHeader("Content-Disposition",
"attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
try (OutputStream os = response.getOutputStream()) {
os.write(data);
os.flush();
} catch (IOException e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
private String resolveFileName(WordChartExportRequest request) {
if (request != null && StrUtil.isNotBlank(request.getTitle())) {
return request.getTitle() + ".docx";
}
return "图表报告.docx";
}
}

View File

@ -0,0 +1,308 @@
package com.yfd.platform.qgc_export.util;
import com.yfd.platform.qgc_export.domain.word.ChartType;
import com.yfd.platform.qgc_export.domain.word.WordChartVo;
import com.yfd.platform.qgc_export.domain.word.WordSeriesVo;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.StandardChartTheme;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.DatasetRenderingOrder;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.renderer.category.StackedBarRenderer;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import java.util.List;
/**
* 图表渲染工具将结构化图表数据渲染为 BufferedImage Word 导出插入
* <p>
* 支持饼图柱状图折线图折柱混合图堆叠柱状图堆叠柱状图归一化百分比
*/
public final class ChartRenderUtil {
/** 默认渲染宽度 */
private static final int DEFAULT_WIDTH = 900;
/** 默认渲染高度 */
private static final int DEFAULT_HEIGHT = 520;
/** 中文字体 */
private static final String FONT_NAME = "SimSun";
static {
// 统一主题避免中文乱码去除阴影
StandardChartTheme theme = (StandardChartTheme) StandardChartTheme.createJFreeTheme();
Font extraLarge = new Font(FONT_NAME, Font.BOLD, 20);
Font large = new Font(FONT_NAME, Font.BOLD, 14);
Font regular = new Font(FONT_NAME, Font.PLAIN, 12);
Font small = new Font(FONT_NAME, Font.PLAIN, 10);
theme.setExtraLargeFont(extraLarge);
theme.setLargeFont(large);
theme.setRegularFont(regular);
theme.setSmallFont(small);
theme.setShadowVisible(false);
ChartFactory.setChartTheme(theme);
}
private ChartRenderUtil() {
}
/**
* 渲染图表为图片
*/
public static BufferedImage render(WordChartVo chart) {
ChartType chartType = ChartType.from(chart.getType());
if (chartType == null) {
throw new IllegalArgumentException("不支持的图表类型: " + chart.getType());
}
JFreeChart jfreeChart = switch (chartType) {
case PIE -> buildPieChart(chart);
case BAR -> buildBarChart(chart);
case LINE -> buildLineChart(chart);
case BAR_LINE -> buildBarLineChart(chart);
case STACKED_BAR -> buildStackedBarChart(chart, false);
case PERCENT_STACKED_BAR -> buildStackedBarChart(chart, true);
};
if (jfreeChart == null) {
return null;
}
jfreeChart.setBackgroundPaint(Color.WHITE);
return jfreeChart.createBufferedImage(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
private static JFreeChart buildPieChart(WordChartVo chart) {
DefaultPieDataset<String> dataset = new DefaultPieDataset<>();
List<String> categories = chart.getCategories();
WordSeriesVo series = chart.getSeries().isEmpty() ? null : chart.getSeries().get(0);
if (categories == null || categories.isEmpty() || series == null || series.getValues() == null) {
return null;
}
List<Double> values = series.getValues();
for (int i = 0; i < categories.size(); i++) {
Double value = i < values.size() ? values.get(i) : 0.0;
dataset.setValue(categories.get(i), value == null ? 0.0 : value);
}
JFreeChart jfreeChart = ChartFactory.createPieChart(
chart.getTitle(), dataset, true, true, false);
PiePlot<String> plot = (PiePlot<String>) jfreeChart.getPlot();
plot.setLabelGenerator(new StandardPieSectionLabelGenerator(
"{0}: {1} ({2})", new DecimalFormat("0.##"), new DecimalFormat("0.0%")));
plot.setLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
plot.setLegendLabelGenerator(new StandardPieSectionLabelGenerator("{0}"));
plot.setBackgroundPaint(Color.WHITE);
plot.setOutlineVisible(false);
plot.setLabelBackgroundPaint(new Color(255, 255, 255, 200));
plot.setLabelOutlinePaint(null);
plot.setLabelShadowPaint(null);
return jfreeChart;
}
private static JFreeChart buildBarChart(WordChartVo chart) {
DefaultCategoryDataset dataset = buildDataset(chart);
JFreeChart jfreeChart = ChartFactory.createBarChart(
chart.getTitle(),
chart.getXAxisName(),
chart.getYAxisName(),
dataset,
PlotOrientation.VERTICAL,
true, true, false);
CategoryPlot plot = jfreeChart.getCategoryPlot();
stylePlot(plot);
BarRenderer renderer = (BarRenderer) plot.getRenderer();
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
renderer.setDefaultItemLabelGenerator(
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
renderer.setItemMargin(0.1);
return jfreeChart;
}
private static JFreeChart buildLineChart(WordChartVo chart) {
DefaultCategoryDataset dataset = buildDataset(chart);
JFreeChart jfreeChart = ChartFactory.createLineChart(
chart.getTitle(),
chart.getXAxisName(),
chart.getYAxisName(),
dataset,
PlotOrientation.VERTICAL,
true, true, false);
CategoryPlot plot = jfreeChart.getCategoryPlot();
stylePlot(plot);
LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
renderer.setDefaultShapesVisible(true);
renderer.setDefaultShapesFilled(true);
renderer.setDefaultStroke(new BasicStroke(2.0f));
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
renderer.setDefaultItemLabelGenerator(
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
return jfreeChart;
}
private static JFreeChart buildStackedBarChart(WordChartVo chart, boolean percent) {
DefaultCategoryDataset dataset = buildDataset(chart);
if (percent) {
dataset = normalizePercent(dataset);
}
JFreeChart jfreeChart = ChartFactory.createStackedBarChart(
chart.getTitle(),
chart.getXAxisName(),
percent ? (chart.getYAxisName() == null ? "占比(%)" : chart.getYAxisName()) : chart.getYAxisName(),
dataset,
PlotOrientation.VERTICAL,
true, true, false);
CategoryPlot plot = jfreeChart.getCategoryPlot();
stylePlot(plot);
StackedBarRenderer renderer = (StackedBarRenderer) plot.getRenderer();
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
if (percent) {
renderer.setDefaultItemLabelGenerator(
new StandardCategoryItemLabelGenerator("{2}%", new DecimalFormat("0.##")));
NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
rangeAxis.setRange(0.0, 100.0);
rangeAxis.setNumberFormatOverride(new DecimalFormat("0'%'"));
} else {
renderer.setDefaultItemLabelGenerator(
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
}
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
return jfreeChart;
}
private static JFreeChart buildBarLineChart(WordChartVo chart) {
DefaultCategoryDataset barDataset = new DefaultCategoryDataset();
DefaultCategoryDataset lineDataset = new DefaultCategoryDataset();
boolean hasBar = false;
boolean hasLine = false;
for (WordSeriesVo series : chart.getSeries()) {
if (series == null || series.getName() == null || series.getValues() == null) {
continue;
}
boolean onRight = series.getYAxisIndex() != null && series.getYAxisIndex() == 1;
DefaultCategoryDataset target = onRight ? lineDataset : barDataset;
if (onRight) {
hasLine = true;
} else {
hasBar = true;
}
for (int i = 0; i < chart.getCategories().size(); i++) {
Double value = i < series.getValues().size() ? series.getValues().get(i) : null;
target.addValue(value == null ? 0.0 : value, series.getName(), chart.getCategories().get(i));
}
}
// 若全部被归到一个轴至少保证主轴有数据
if (!hasBar && hasLine) {
return buildLineChart(chart);
}
CategoryAxis domainAxis = new CategoryAxis(chart.getXAxisName());
domainAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
NumberAxis leftAxis = new NumberAxis(chart.getYAxisName());
leftAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
NumberAxis rightAxis = new NumberAxis(chart.getYAxisNameRight());
rightAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
BarRenderer barRenderer = new BarRenderer();
barRenderer.setDefaultItemLabelsVisible(chart.isShowValues());
barRenderer.setDefaultItemLabelGenerator(
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
barRenderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
LineAndShapeRenderer lineRenderer = new LineAndShapeRenderer();
lineRenderer.setDefaultShapesVisible(true);
lineRenderer.setDefaultShapesFilled(true);
lineRenderer.setDefaultStroke(new BasicStroke(2.0f));
lineRenderer.setDefaultItemLabelsVisible(chart.isShowValues());
lineRenderer.setDefaultItemLabelGenerator(
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
lineRenderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
CategoryPlot plot = new CategoryPlot(barDataset, domainAxis, leftAxis, barRenderer);
plot.setDataset(1, lineDataset);
plot.setRenderer(1, lineRenderer);
plot.setRangeAxis(1, rightAxis);
plot.mapDatasetToRangeAxis(0, 0);
plot.mapDatasetToRangeAxis(1, 1);
plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
plot.setBackgroundPaint(Color.WHITE);
plot.setDomainGridlinesVisible(true);
plot.setRangeGridlinesVisible(true);
plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
return new JFreeChart(chart.getTitle(), JFreeChart.DEFAULT_TITLE_FONT, plot, true);
}
private static DefaultCategoryDataset buildDataset(WordChartVo chart) {
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
List<String> categories = chart.getCategories();
if (categories == null || categories.isEmpty()) {
return dataset;
}
for (WordSeriesVo series : chart.getSeries()) {
if (series == null || series.getName() == null || series.getValues() == null) {
continue;
}
for (int i = 0; i < categories.size(); i++) {
Double value = i < series.getValues().size() ? series.getValues().get(i) : null;
dataset.addValue(value == null ? 0.0 : value, series.getName(), categories.get(i));
}
}
return dataset;
}
/**
* 将数据按列分类归一化为百分比用于归一化堆叠柱状图
*/
private static DefaultCategoryDataset normalizePercent(DefaultCategoryDataset source) {
DefaultCategoryDataset result = new DefaultCategoryDataset();
int rowCount = source.getRowCount();
int columnCount = source.getColumnCount();
if (rowCount == 0 || columnCount == 0) {
return result;
}
for (int col = 0; col < columnCount; col++) {
double sum = 0.0;
for (int row = 0; row < rowCount; row++) {
Number value = source.getValue(row, col);
sum += value == null ? 0.0 : value.doubleValue();
}
for (int row = 0; row < rowCount; row++) {
Number value = source.getValue(row, col);
double v = value == null ? 0.0 : value.doubleValue();
double percent = sum == 0.0 ? 0.0 : v / sum * 100.0;
result.addValue(percent, source.getRowKey(row), source.getColumnKey(col));
}
}
return result;
}
private static void stylePlot(CategoryPlot plot) {
plot.setBackgroundPaint(Color.WHITE);
plot.setDomainGridlinesVisible(true);
plot.setRangeGridlinesVisible(true);
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
CategoryAxis domainAxis = plot.getDomainAxis();
domainAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
rangeAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
}
}

View File

@ -0,0 +1,103 @@
package com.yfd.platform.qgc_export.util;
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
import com.yfd.platform.qgc_export.domain.word.WordChartVo;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.Document;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
/**
* Word 图表导出工具将多个图表渲染为图片并按顺序写入 .docx 文档
*/
public final class WordChartExportUtil {
/** 插入图片的目标宽度单位1 磅 = 1/72 英寸) */
private static final double IMAGE_WIDTH_PT = 460.0;
private WordChartExportUtil() {
}
/**
* 生成 Word 文档字节数组
*
* @param request 文档标题 + 图表列表
* @return .docx 字节数组
*/
public static byte[] generateWord(WordChartExportRequest request) throws IOException {
List<WordChartVo> charts = request == null ? List.of() : request.getCharts();
try (XWPFDocument document = new XWPFDocument();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
appendTitle(document, request);
for (WordChartVo chart : charts) {
appendChart(document, chart);
}
document.write(out);
return out.toByteArray();
}
}
private static void appendTitle(XWPFDocument document, WordChartExportRequest request) {
String title = request == null ? null : request.getTitle();
if (title != null && !title.isBlank()) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setAlignment(ParagraphAlignment.CENTER);
XWPFRun run = paragraph.createRun();
run.setText(title);
run.setBold(true);
run.setFontSize(22);
run.setFontFamily("SimSun");
}
String subtitle = request == null ? null : request.getSubtitle();
if (subtitle != null && !subtitle.isBlank()) {
XWPFParagraph paragraph = document.createParagraph();
paragraph.setAlignment(ParagraphAlignment.CENTER);
XWPFRun run = paragraph.createRun();
run.setText(subtitle);
run.setFontSize(12);
run.setColor("808080");
run.setFontFamily("SimSun");
}
}
private static void appendChart(XWPFDocument document, WordChartVo chart) throws IOException {
BufferedImage image = ChartRenderUtil.render(chart);
if (image == null) {
return;
}
byte[] png = toPngBytes(image);
double imageHeightPt = IMAGE_WIDTH_PT * image.getHeight() / image.getWidth();
XWPFParagraph paragraph = document.createParagraph();
paragraph.setAlignment(ParagraphAlignment.CENTER);
XWPFRun run = paragraph.createRun();
try (ByteArrayInputStream in = new ByteArrayInputStream(png)) {
try {
run.addPicture(in, Document.PICTURE_TYPE_PNG, "chart.png",
Units.toEMU(IMAGE_WIDTH_PT), Units.toEMU(imageHeightPt));
} catch (org.apache.poi.openxml4j.exceptions.InvalidFormatException e) {
throw new IOException("插入图片失败", e);
}
}
// 每个图表后空一行避免图片粘连
XWPFParagraph spacer = document.createParagraph();
spacer.setSpacingAfter(120);
}
private static byte[] toPngBytes(BufferedImage image) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", out);
return out.toByteArray();
}
}
}