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

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

0. 前言

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
 */
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)));
        // PPT通用处理
        pptCommentHandle(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);
            // PPT通用处理
            pptCommentHandle(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 23:15 2022/11/13
     * @param pptx PPT
     * @param paramMap 参数map
     * @return void
     */
    private static void pptCommentHandle(XMLSlideShow pptx, Map<String, String> paramMap) {
        PowerPointUtil powerPointUtil = new PowerPointUtil(pptx);
        // 遍历幻灯片
        List<XSLFSlide> slideList = pptx.getSlides();
        for (XSLFSlide slide : slideList) {
            // 1. 替换段落占位符
            // 1.1 获取所有的shape,并解析为文本段落
            List<XSLFShape> shapes = slide.getShapes();
            List<XSLFTextParagraph> paragraphsFromSlide = new ArrayList<>();
            for (XSLFShape shape : shapes) {
                List<XSLFTextParagraph> textParagraphs = powerPointUtil.parseParagraph(shape);
                paragraphsFromSlide.addAll(textParagraphs);
            }
            // 1.2 替换文本段落中的占位符
            for (XSLFTextParagraph paragraph : paragraphsFromSlide) {
                powerPointUtil.replaceTagInParagraph(paragraph, paramMap, -1);
            }
            // 2. 替换表格内占位符
            // 2.1 循环获取到表格的单元格,并获取到文本段落
            List<XSLFTable> allTableFromSlide = powerPointUtil.getAllTableFromSlide(slide);
            for (XSLFTable xslfTableRows : allTableFromSlide) {
                List<XSLFTableRow> rows = xslfTableRows.getRows();
                for (XSLFTableRow row : rows) {
                    for (XSLFTableCell cell : row.getCells()) {
                        List<XSLFTextParagraph> textParagraphs = cell.getTextParagraphs();
                        for (XSLFTextParagraph textParagraph : textParagraphs) {
                            // 2.2 替换文本段落中的占位符
                            powerPointUtil.replaceTagInParagraph(textParagraph, paramMap, -1);
                        }
                    }
                }
            }
        }
    }
}

工具类:

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.sl.usermodel.TextBox;
import org.apache.poi.xslf.usermodel.*;
import org.openxmlformats.schemas.drawingml.x2006.main.CTRegularTextRun;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 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:55 2022/11/5
     * @param slide 幻灯片
     * @return 表格列表
     */
    public List<XSLFTable> getAllTableFromSlide(XSLFSlide slide) {
        List<XSLFTable> tables = new ArrayList<>();
        for (XSLFShape shape : slide.getShapes()) {
            if (shape instanceof XSLFTable) {
                tables.add((XSLFTable) shape);
            }
        }
        return tables;
    }

    /**
     * 替换段落内的标签文本
     * @Author Nile (QQEmail:576109623)
     * @Date 16:55 2022/11/5
     * @param paragraph 段落
     * @param paramMap 参数Map
     * @param start 替换位置索引
     * @return void
     */
    public void replaceTagInParagraph(XSLFTextParagraph paragraph, Map<String, String> paramMap, int start) {
        String paraText = paragraph.getText();
        // 正则匹配,循环匹配替换
        String regEx = "\\$\\{.+?\\}";
        Pattern pattern = Pattern.compile(regEx);
        Matcher matcher = pattern.matcher(paraText);
        while (matcher.find()) {
            StringBuilder keyWord = new StringBuilder();
            // 获取占位符起始位置所在run的索引
            int s = getRunIndex(paragraph, "${", start);
            if (s < start) {
                // 重复递归,直接返回
                return;
            }
            // 获取占位符结束位置所在run的索引
            int e = getRunIndex(paragraph, "}", start);
            // 存放标签
            String rs = matcher.group(0);
            // 存放 key
            keyWord.append(rs);
            // 获取标签所在 run 的全部文字
            String text = getRunsT(paragraph, s, e + 1);
            // 如果没在 paramMap,则不做替换
            String v = nullToDefault(paramMap.get(keyWord.toString()), keyWord.toString());
            // 没有找到这个标签所对应的值,那么就直接替换成标签的值(业务需求来着,找不到不替换)
            setText(paragraph.getTextRuns().get(s), text.replace(rs, v));
            // 存在 ${ 和 } 不在同一个CTRegularTextRun内的情况,将其他替换为空字符
            for (int i = s + 1; i < e + 1; i++) {
                setText(paragraph.getTextRuns().get(i), "");
            }
            start = e + 1;
        }
    }

    /**
     * 解析一个shape内的所有段落
     * @Author Nile (QQEmail:576109623)
     * @Date 16:56 2022/11/5
     * @param shape shape
     * @return 文本段落列表
     */
    public List<XSLFTextParagraph> parseParagraph(XSLFShape shape) {
        if (shape instanceof XSLFAutoShape) {
            XSLFAutoShape autoShape = (XSLFAutoShape) shape;
            return autoShape.getTextParagraphs();
        } else if (shape instanceof XSLFTextShape) {
            XSLFTextShape textShape = (XSLFTextShape) shape;
            return textShape.getTextParagraphs();
        } else if (shape instanceof XSLFFreeformShape) {
            XSLFFreeformShape freeformShape = (XSLFFreeformShape) shape;
            return freeformShape.getTextParagraphs();
        } else if (shape instanceof TextBox) {
            TextBox textBox = (TextBox) shape;
            return textBox.getTextParagraphs();
        }
        return new ArrayList<>();
    }

    /**
     * 获取段落下特定索引的textRun的值
     * @Author Nile (QQEmail:576109623)
     * @Date 17:17 2022/11/5
     * @param paragraph 段落
     * @param start 起始位置
     * @param end 终止位置
     * @return run值
     */
    private String getRunsT(XSLFTextParagraph paragraph, int start, int end) {
        List<XSLFTextRun> textRuns = paragraph.getTextRuns();
        StringBuilder t = new StringBuilder();
        for (int i = start; i < end; i++) {
            t.append(textRuns.get(i).getRawText());
        }
        return t.toString();
    }

    /**
     * 设置run的值
     * @Author Nile (QQEmail:576109623)
     * @Date 17:18 2022/11/5
     * @param run run
     * @param t run值
     * @return void
     */
    private void setText(XSLFTextRun run, String t) {
        run.setText(t);
    }

    /**
     * 获取word在段落中出现第一次的run的索引
     * @Author Nile (QQEmail:576109623)
     * @Date 17:19 2022/11/5
     * @param paragraph 段落
     * @param word 目标值
     * @param start 索引
     * @return void
     */
    private int getRunIndex(XSLFTextParagraph paragraph, String word, int start) {
        List<CTRegularTextRun> rList = paragraph.getXmlObject().getRList();
        for (int i = (Math.max(start, 0)); i < rList.size(); i++) {
            String text = rList.get(i).getT();
            if (text.contains(word)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * toString方法,空则返回默认值
     * @Author Nile (QQEmail:576109623)
     * @Date 17:20 2022/11/5
     * @param o 对象
     * @param defaultStr 默认值
     * @return toString
     */
    private String nullToDefault(Object o, String defaultStr) {
        if (ObjectUtils.isEmpty(o)) {
            return defaultStr;
        }
        return o.toString();
    }
}

3. 测试

3.1 模板准备

占位符替换测试,测试文字和表格

占位符

3.2 替换结果

占位符替换结果

4. 一点分析

debug,做下简单分析。

4.1 parseParagraph

pptCommentHandle方法:

// 1. 替换段落占位符
// 1.1 获取所有的shape,并解析为文本段落
List<XSLFShape> shapes = slide.getShapes();
List<XSLFTextParagraph> paragraphsFromSlide = new ArrayList<>();
for (XSLFShape shape : shapes) {
    List<XSLFTextParagraph> textParagraphs = powerPointUtil.parseParagraph(shape);
    paragraphsFromSlide.addAll(textParagraphs);
}
// 1.2 替换文本段落中的占位符
for (XSLFTextParagraph paragraph : paragraphsFromSlide) {
    powerPointUtil.replaceTagInParagraph(paragraph, paramMap, -1);
}

debug查看paragraphsFromSlide

paragraphsFromSlide
可以看到,通过PowerPointUtil.parseParagraph方法解析幻灯片,可以获取到所有的文本段落,然后逐段解析处理。

4.2 getRunIndex

PowerPointUtil.replaceTagInParagraph方法,正则匹配成功后,会进入处理逻辑。

这里先说明下getRunIndex方法:

private int getRunIndex(XSLFTextParagraph paragraph, String word, int start) {
    List<CTRegularTextRun> rList = paragraph.getXmlObject().getRList();
    // debug查看rList不方便截图,for循环打印输出
    for (int i = 0; i < rList.size(); i++) {
        String text = rList.get(i).getT();
        System.out.println(text);
    }
    for (int i = (Math.max(start, 0)); i < rList.size(); i++) {
        String text = rList.get(i).getT();
        if (text.contains(word)) {
            return i;
        }
    }
    return -1;
}

输出结果

今天是
${date}
,
${book}
快看完了,
${thought}
,冲了一杯
${drink}
,
${doing}
  1. XSLFTextParagraph.getXmlObject方法,这个名字起得好,见名知意。其实PPT文件底层可以理解为是一个xml文件,即Mark Language,使用xml对内容进行标记(内容、文字大小、样式、动画等等),PowerPoint软件的工作就是解析xml文件后展示给我们看。

    简单做法就是将一个PPT文件的后缀名改为.zip,然后再解压查看,目录如下:
    在这里插入图片描述
    (因此笔者认为遇到的所有问题肯定都是可以解决的,包括上一篇文章合并后母版和版式变为空白的问题,关键就是要去研究底层的xml文件喽,然后找到对应的标记内容)

  2. 一句输入的语句被解析成了不同的小段语句,如上面这个例子是11个CTRegularTextRun。(但这也埋下了一个坑,看4.3)

4.3 replaceTagInParagraph

将占位符替换为目标值

public void replaceTagInParagraph(XSLFTextParagraph paragraph, Map<String, String> paramMap, int start) {
    String paraText = paragraph.getText();
    // 正则匹配,循环匹配替换
    String regEx = "\\$\\{.+?\\}";
    Pattern pattern = Pattern.compile(regEx);
    Matcher matcher = pattern.matcher(paraText);
    while (matcher.find()) {
        StringBuilder keyWord = new StringBuilder();
        // 获取占位符起始位置所在run的索引
        int s = getRunIndex(paragraph, "${", start);
        if (s < start) {
            // 重复递归,直接返回
            return;
        }
        // 获取占位符结束位置所在run的索引
        int e = getRunIndex(paragraph, "}", start);
        // 存放标签
        String rs = matcher.group(0);
        // 存放 key
        keyWord.append(rs);
        // 获取标签所在 run 的全部文字
        String text = getRunsT(paragraph, s, e + 1);
        // 如果没在 paramMap,则不做替换
        String v = nullToDefault(paramMap.get(keyWord.toString()), keyWord.toString());
        // 没有找到这个标签所对应的值,那么就直接替换成标签的值(业务需求来着,找不到不替换)
        setText(paragraph.getTextRuns().get(s), text.replace(rs, v));
        // 存在 ${ 和 } 不在同一个CTRegularTextRunt内的情况,将其他替换为空字符
        for (int i = s + 1; i < e + 1; i++) {
            setText(paragraph.getTextRuns().get(i), "");
        }
        start = e + 1;
    }
}

通过getRunIndex方法来找到 ${} 所在rList的位置,来确定需要替换的区间,然后替换为目标值即可。以${date}为例,起始位置s和结束位置e都是1,所以将索引为1的textRun替换为目标值2022年11月13日

这里笔者在开发的时候遇到了一个特别现象,就是 ${}不在同一个CTRegularTextRun里面。也就是打印结果是类似这样的:

今天是
${da
te}

因此补充了这段逻辑:

// 存在 ${ 和 } 不在同一个CTRegularTextRun内的情况,将其他替换为空字符
for (int i = s + 1; i < e + 1; i++) {
    setText(paragraph.getTextRuns().get(i), "");
}

然而笔者开发的时候还担心 ${ 不在同一个CTRegularTextRun内的情况😅。这种情况就需要再处理,偷懒,没有写。

4.4 XSLFTable

表格解析,到最后会发现也是调用PowerPointUtil.replaceTagInParagraph方法,也就是把单元格提取为文本段落。但因为表格是二维的,所以单独处理了。

5. 问题

不知大伙有没有发现,笔者在获取到每个PPT文件后就立马调用了pptCommentHandle方法。

其实一开始开发的时候,写的也是先合并PPT,然后再调用pptCommentHandle方法。

但出现了一个灵异现象,只有第一页替换成功,从第二页开始全部没有替换。而笔者还专门debug查看了第二页的替换情况,确确实实替换成功了,也看到了替换后的语句,但输出的PPT文件就是木有。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值