Java输出PPT文件(三) - 饼图数据替换

Java输出PPT文件(三) - 饼图数据替换

0. 前言

Java输出PPT文件(一) - 合并PPT

Java输出PPT文件(二) - 占位符数据替换

在这次的开发中,也遇到了PPT文件存在饼图的情况,需要对饼图的数据进行替换,一并记录下。

1. 依赖

<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml-full -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml-full</artifactId>
    <version>5.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml-schemas -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml-schemas</artifactId>
    <version>4.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/ooxml-schemas -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>ooxml-schemas</artifactId>
    <version>1.4</version>
</dependency>

注意:poi-ooxml、poi-ooxml-full目前最高版本是5.2.3,但需要Apache的commons-io也为高版本,所以这里使用了5.0.0,想试用5.2.3的朋友先解决下依赖问题,笔者遇到的报错如下:

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream

2. 代码

PowerPoint工具测试类:

import org.apache.poi.xslf.usermodel.*;
import org.springframework.util.CollectionUtils;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;

/**
 * Copyright: Horizon
 *
 * @ClassName PowerPointUtilTest
 * @Description PowerPoint工具测试类
 * @Author Nile (QQEmail:576109623)
 * @Date 15:48 2022/11/5
 * @Version 1.0.0
 */
/*@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@Slf4j*/
public class PowerPointUtilTest {
    public static void main(String[] args) throws IOException {
        // 文件路径及文件名称
        String rootDir = "src/main/resources/ppt/";
        String[] pptArray = {"Title.pptx", "Foreword.pptx", "Dependency.pptx"};
        // 参数map
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("${date}", "2022年11月13日");
        paramMap.put("${book}", "《马普尔小姐最后的案件》");
        paramMap.put("${thought}", "不想出去");
        paramMap.put("${drink}", "恩施绿茶");
        paramMap.put("${doing}", "写写blog");
        paramMap.put("${rent}", "25");
        paramMap.put("${dining}", "15");
        paramMap.put("${shopping}", "10");
        paramMap.put("${debt}", "49");
        paramMap.put("${saving}", "1");
        // 合并
        mergePPT(rootDir, Arrays.asList(pptArray), paramMap);
    }

    /**
     * 合并PPT
     * @Author Nile (QQEmail:576109623)
     * @Date 22:18 2022/11/13
     * @param rootDir 文件路径
     * @param fileNameList 文件名称列表
     * @param paramMap 参数map
     * @return void
     */
    private static void mergePPT(String rootDir, List<String> fileNameList, Map<String, String> paramMap) throws IOException {
        if (CollectionUtils.isEmpty(fileNameList)) {
            return;
        }
        // 1. 使用第1个PPT作为基础文件
        XMLSlideShow ppt = new XMLSlideShow(new FileInputStream(rootDir + fileNameList.get(0)));
        // 饼图处理(因为代码太占篇幅了,所以写文章时把通用处理移除了)
        pptPieHandle(ppt, paramMap);
        // 2. 从第2个文件开始遍历,合并
        for (int i = 1; i < fileNameList.size(); i++) {
            FileInputStream inputstream = new FileInputStream(rootDir + fileNameList.get(i));
            XMLSlideShow src = new XMLSlideShow(inputstream);
            // 饼图处理(因为代码太占篇幅了,所以写文章时把通用处理移除了)
            pptPieHandle(src, paramMap);
            // 遍历每张幻灯片
            for (XSLFSlide srcSlide : src.getSlides()) {
                // 合并
                ppt.createSlide().importContent(srcSlide);
            }
        }
        // 3. 输出
        String resultName = "Result.pptx";
        FileOutputStream out = new FileOutputStream(rootDir + resultName);
        ppt.write(out);
        out.close();
    }

    /**
     * PPT饼图处理
     * @Author Nile (QQEmail:576109623)
     * @Date 0:28 2022/11/16
     * @param pptx PPT
     * @param paramMap 参数map
     * @return void
     */
    private static void pptPieHandle(XMLSlideShow pptx, Map<String, String> paramMap) {
        PowerPointUtil powerPointUtil = new PowerPointUtil(pptx);
        // 遍历幻灯片
        List<XSLFSlide> slideList = pptx.getSlides();
        for (XSLFSlide slide : slideList) {
            List<XSLFChart> charts = powerPointUtil.getAllChartFromSlide(slide);
            if (CollectionUtils.isEmpty(charts)) {
                continue;
            }
            // 替换饼图数据
            XSLFChart chart = charts.get(0);
            CalculateUtil calculateUtil = new CalculateUtil();
            // 这里印象中开发的时候不需要处理的,忘记了。
            // 但为了输出效果处理下,用的是javax.script.ScriptEngine
            List<String> tempData = Arrays.asList(calculateUtil.calExpression(paramMap.get("${rent}") + "/100"),
                    calculateUtil.calExpression(paramMap.get("${dining}") + "/100"),
                    calculateUtil.calExpression(paramMap.get("${shopping}") + "/100"),
                    calculateUtil.calExpression(paramMap.get("${debt}") + "/100"),
                    calculateUtil.calExpression(paramMap.get("${saving}") + "/100"));
            if (!tempData.contains(null)) {
                powerPointUtil.updatePieDataCache(powerPointUtil.getPieChartFromChart(chart).get(0), 0, tempData);
            }
        }
    }
}

计算工具类:

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

/**
 * Copyright: Horizon
 *
 * @ClassName CalculateUtil
 * @Description 计算工具类
 * @Author Nile (QQEmail:576109623)
 * @Date 16:07 2022/11/19
 * @Version 1.0.0
 */
public class CalculateUtil {

    /**
     * 计算公式结果
     * @Author Nile (QQEmail:576109623)
     * @Date 16:10 2022/11/19
     * @param expression 公式
     * @return 计算结果,异常返回null
     */
    public String calExpression(String expression) {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        try {
            return engine.eval(expression).toString();
        } catch (Exception e) {
            return null;
        }
    }
}

PowerPoint工具类:

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFChart;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.openxmlformats.schemas.drawingml.x2006.chart.*;

import java.util.ArrayList;
import java.util.List;

/**
 * Copyright: Horizon
 *
 * @ClassName PowerPointUtil
 * @Description PowerPoint工具类
 * @Author Nile (QQEmail:576109623)
 * @Date 15:23 2022/11/5
 * @Version 1.0.0
 */
@Data
@Slf4j
public class PowerPointUtil {
    /**
     * PPT文件
     */
    private XMLSlideShow pptx;

    public PowerPointUtil(XMLSlideShow pptx) {
        this.pptx = pptx;
    }

    /**
     * 从当前图表中获取饼状图
     * @Author Nile (QQEmail:576109623)
     * @Date 16:57 2022/11/5
     * @param chart 图表
     * @return 饼图列表
     */
    public List<CTPieChart> getPieChartFromChart(XSLFChart chart) {
        CTPlotArea plotArea = getChartPlotArea(chart);
        return plotArea.getPieChartList();
    }

    /**
     * 更新饼图的缓存数据
     * @Author Nile (QQEmail:576109623)
     * @Date 17:04 2022/11/5
     * @param pieChart 饼图
     * @param serIndex 索引
     * @param data 数据
     * @return void
     */
    public void updatePieDataCache(CTPieChart pieChart, int serIndex, List<String> data) {
        CTPieSer ctPieSer = pieChart.getSerList().get(serIndex);
        CTNumRef numRef = ctPieSer.getVal().getNumRef();
        replaceVal(numRef, data);
        // 设置饼图显示精度
        numRef.getNumCache().setFormatCode("0.00%");
    }

    /**
     * 替换数据
     * @Author Nile (QQEmail:576109623)
     * @Date 17:05 2022/11/5
     * @param numRef 数字引用
     * @param data 数据
     * @return void
     */
    private void replaceVal(CTNumRef numRef, List<String> data) {
        numRef.unsetNumCache();
        CTNumData ctNumData = numRef.addNewNumCache();
        ctNumData.addNewPtCount().setVal(data.size());
        for (int i = 0; i < data.size(); i++) {
            CTNumVal ctNumVal = ctNumData.addNewPt();
            ctNumVal.setIdx(i);
            ctNumVal.setV(data.get(i));
        }
    }

    /**
     * 获取plotArea
     * @Author Nile (QQEmail:576109623)
     * @Date 17:15 2022/11/5
     * @param chart 图表
     * @return plotArea
     */
    private CTPlotArea getChartPlotArea(XSLFChart chart) {
        return chart.getCTChart().getPlotArea();
    }

    /**
     * 从幻灯片中获取图表列表
     * @Author Nile (QQEmail:576109623)
     * @Date 16:35 2022/11/13
     * @param slide 幻灯片
     * @return 图表列表
     */
    public List<XSLFChart> getAllChartFromSlide(XSLFSlide slide) {
        List<XSLFChart> charts = new ArrayList<>();
        for (POIXMLDocumentPart relation : slide.getRelations()) {
            if (relation instanceof XSLFChart) {
                charts.add((XSLFChart) relation);
            }
        }
        return charts;
    }
}

3. 测试

3.1 饼图数据

需要使用excel准备好饼图的基础数据,并插入饼图,调整好样式、标签等

饼图数据

3.2 模板准备

复制饼图,粘贴到PPT中,粘贴选项选择“使用目标主题和嵌入工作簿”。

饼图模板

3.3 替换结果

饼图更新

4. 问题

4.1 数据

上面的代码是写死的!!! 换句话说,上面这个代码只能用来处理这个特定的饼图。

没太多时间来写这个了,笔者初步的想法是,写一个策略模式,根据不同的饼图,获取到需要的占位符,通过paramMap获取实际值,然后构建tempData,作为pptPieHandle方法的入参。当然这就需要有一个管理功能来维护 PPT文件 - 饼图 - 数据 的关系了。

其实,在开发过程中,也查到了另外一种维护饼图数据的方式,就是在PPT中创建一个excel表格(内嵌),然后将excel表格数据同这个饼图绑定,通过更新excel表格的数据来更新饼图。但实际测试过程,要嘛样式有问题,要嘛数据有问题,要嘛替换失败。虽然也有成功的,最后还是使用了本文这种方式。

4.2 数据校验

这点就比较繁琐了,需要使用正则校验入参,必须保证每个数值的取值范围为:[0, 1],且总和要为1。

其实笔者也试过传入负数,负号会被忽略,当成正数切割饼图;传入字符,按0处理;总和不为1,根据各个数占总和的占比切割饼图…所以这里需要根据需要进行数据校验和处理。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值