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
可以看到,通过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}
。
-
XSLFTextParagraph.getXmlObject方法,这个名字起得好,见名知意。其实PPT文件底层可以理解为是一个xml文件,即Mark Language,使用xml对内容进行标记(内容、文字大小、样式、动画等等),PowerPoint软件的工作就是解析xml文件后展示给我们看。
简单做法就是将一个PPT文件的后缀名改为.zip
,然后再解压查看,目录如下:
(因此笔者认为遇到的所有问题肯定都是可以解决的,包括上一篇文章合并后母版和版式变为空白的问题,关键就是要去研究底层的xml文件喽,然后找到对应的标记内容)
-
一句输入的语句被解析成了不同的小段语句,如上面这个例子是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内的情况
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)