poi生成的ppt,powerPoint打开提示内容错误解决方案
最近做了ppt的生成,使用poi制作ppt,出现一个问题。微软的powerPoint打不开,提示错误信息
通过xml对比工具发现只需要删除幻灯片的某些标签即可解决。
用的是XML Notepand
分析思路:
1.把poi生成的pptx用wps打开,正常,但是poi生成的pptx在powerPoint打不开。尝试用wps另存一份,ok,wps另存后的ppt,powerPoint可以打开了。然后ppt的大小也有了变化,肯定是wps对这个ppt做了一些格式化操作。具体是啥呢,不清楚,只能用这个 XML Notepand 对比看看吧。
2. ppt本质上是xml的压缩包集合。我们对ppt进行解压。
3.里面的东西挺多了,我自己也不怎么懂,但是我想既然是ppt的某个幻灯片打不开,那我就对比一下能幻灯片,看看poi生成的ppt和wps另存后的ppt的幻灯片有哪些区别。
我们进入这个目录
4.这个就是放置幻灯片的位置。
我们通过这个XML Notepand 进行一个对比观察,
我们先用xml notePand打开poi生成的一个幻灯片,我打开的是wsp另存的解压后的第4个幻灯片,ppt\slides\slide4.xml
然后选择这个,再次选择一个poi生成的ppt,选择同一个幻灯片,进行一个对比
然后进行两个文件对比
最后对比发现这个标签可能不可以被powerPoint正确识别,我们就把这个标签删除
<p:custDataLst>
<p:tags r:id="rId1"/>
</p:custDataLst>
最关键的来了,直接上代码
package com.ruoyi.ppt.utilsService;
import com.ruoyi.ppt.aopCustom.customAnnotations.LogExecutionTime;
import com.ruoyi.ppt.config.ThreadPoolDealTask;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.w3c.dom.*;
import javax.annotation.Resource;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@Slf4j
@Component
public class PptXmlFormate {
@Resource
private ThreadPoolDealTask threadPoolDealTask;
@LogExecutionTime("PPT XML标签调整耗时")
public void xmlFormate(String pptxFilePath, String useFont) {
useFont = StringUtils.isBlank(useFont) ? "宋体" : useFont;
// 使用 try-with-resources 自动关闭临时文件和 Zip 流
try {
// 创建临时文件来存储更新后的 PPTX 文件
File tempFile = File.createTempFile("pptx_temp" + UUID.randomUUID(), ".zip");
tempFile.deleteOnExit();
// 读取 PPTX 文件并更新 XML
try (FileInputStream fis = new FileInputStream(pptxFilePath);
ZipInputStream zis = new ZipInputStream(fis);
FileOutputStream fos = new FileOutputStream(tempFile);
ZipOutputStream zos = new ZipOutputStream(fos)) {
ZipEntry entry;
Map<String, ByteArrayOutputStream> updatedFiles = new HashMap<>();
// 遍历 PPTX 文件中的每个条目
while ((entry = zis.getNextEntry()) != null) {
String entryName = entry.getName();
ByteArrayOutputStream entryData = new ByteArrayOutputStream();
// 读取条目内容
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) > 0) {
entryData.write(buffer, 0, len);
}
// 仅处理幻灯片 XML 文件
if (entryName.startsWith("ppt/slides/slide") && entryName.endsWith(".xml")) {
// 解析 XML
try (ByteArrayInputStream bais = new ByteArrayInputStream(entryData.toByteArray());
ByteArrayOutputStream updatedXml = new ByteArrayOutputStream()) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(bais);
// 异步任务,并确保异常捕获
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
// 删除标签
String custDataLstXpath = "//*[local-name()='custDataLst']";
removeNodesUsingXPath(document, custDataLstXpath);
}, threadPoolDealTask.getThreadPoolExecutor());
// 捕获并处理异步任务中的异常
future.exceptionally(ex -> {
log.error("处理 XML 异步任务时发生错误: {}", ex.getMessage(), ex);
return null;
}).join(); // 等待任务完成
// 写入修改后的 XML 内容
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
DOMSource source = new DOMSource(document);
StreamResult result = new StreamResult(updatedXml);
transformer.transform(source, result);
updatedFiles.put(entryName, updatedXml);
}
} else {
// 对于其他条目,保持原样
updatedFiles.put(entryName, entryData);
}
}
// 写入更新后的 PPTX 文件
for (Map.Entry<String, ByteArrayOutputStream> fileEntry : updatedFiles.entrySet()) {
String entryName = fileEntry.getKey();
ByteArrayOutputStream fileData = fileEntry.getValue();
zos.putNextEntry(new ZipEntry(entryName));
fileData.writeTo(zos);
zos.closeEntry();
}
zos.finish();
}
// 将临时文件移动到原始 PPTX 文件路径,替换原文件
Files.move(tempFile.toPath(), new File(pptxFilePath).toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
log.error("处理文件时发生 IO 错误: {}", e.getMessage(), e);
} catch (Exception e) {
log.error("处理过程中发生错误: {}", e.getMessage(), e);
}
}
public void removeNodesUsingXPath(Document document, String xpathExpression) {
XPath xPath = XPathFactory.newInstance().newXPath();
// 设置命名空间前缀和 URI
NamespaceContext nsContext = new NamespaceContext() {
public String getNamespaceURI(String prefix) {
switch (prefix) {
case "a":
return "http://schemas.openxmlformats.org/drawingml/2006/main";
case "p":
return "http://schemas.openxmlformats.org/presentationml/2006/main";
default:
return XMLConstants.NULL_NS_URI;
}
}
public String getPrefix(String namespaceURI) {
return null;
}
public Iterator getPrefixes(String namespaceURI) {
return null;
}
};
xPath.setNamespaceContext(nsContext);
try {
NodeList nodes = (NodeList) xPath.evaluate(xpathExpression, document, XPathConstants.NODESET);
for (int i = nodes.getLength() - 1; i >= 0; i--) { // 从后向前遍历
Node node = nodes.item(i);
Node parentNode = node.getParentNode();
if (parentNode != null) {
parentNode.removeChild(node);
}
}
} catch (Exception e) {
log.error("Error removing nodes using XPath: {}", e.getMessage());
}
}
}
注意哈,这里使用了线程池,不需要的可以删除,需要的话不会配置的,这里也给出代码
package com.ruoyi.ppt.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import javax.annotation.Resource;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
@Slf4j
@Configuration
@Data
public class ThreadPoolDealTask {
private volatile ThreadPoolExecutor threadPool;
// 自定义 ThreadFactory,为线程池中的线程指定名称
private ThreadFactory createThreadFactory(String poolName) {
AtomicInteger counter = new AtomicInteger(0);
return r -> {
Thread thread = new Thread(r);
thread.setName(poolName + "-thread-" + counter.incrementAndGet());
return thread;
};
}
// 懒加载线程池,只有在第一次使用时才会创建 用于 业务处理
public void initializeThreadPool() {
if (this.threadPool == null) {
synchronized (this) {
if (this.threadPool == null) { // 双重检查锁定
this.threadPool = new ThreadPoolExecutor(
2,
10,
30L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(50),
createThreadFactory("CustomThreadPool"), // 使用自定义的 ThreadFactory
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 启用核心线程超时
this.threadPool.allowCoreThreadTimeOut(false);
}
}
}
}
public ThreadPoolExecutor getThreadPoolExecutor() {
return this.threadPool;
}
// 输出任务队列状态
private void printQueueStatus() {
if (threadPool instanceof ThreadPoolExecutor) {
ThreadPoolExecutor executor = (ThreadPoolExecutor) threadPool;
System.out.println("Submitting task to thread: " + Thread.currentThread().getName());
System.out.println("当前任务队列大小: " + executor.getQueue().size());
System.out.println("当前任务队列大小: " + executor.getQueue().size());
System.out.println("当前活动线程数: " + executor.getActiveCount());
System.out.println("当前线程池大小: " + executor.getPoolSize());
}
}
// 提交任务并返回Future对象
public <T> Future<T> submitTask(Callable<T> task) {
initializeThreadPool(); // 确保线程池已初始化
return threadPool.submit(task);
}
// 提交Runnable任务并返回Future
public Future<?> submitTask(Runnable task) {
initializeThreadPool(); // 确保线程池已初始化
return threadPool.submit(task);
}
// 优雅地关闭线程池
public void shutdown() {
if (threadPool != null) {
threadPool.shutdown();
try {
// 等待现有任务执行完毕
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
threadPool.shutdownNow(); // 强制关闭
// 等待任务响应中断
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未能在指定时间内关闭");
}
}
} catch (InterruptedException ie) {
// 在当前线程被中断时,也强制关闭线程池
threadPool.shutdownNow();
// 保留中断状态
Thread.currentThread().interrupt();
} finally {
threadPool = null; // 关闭后将线程池引用置为空
}
}
}
}
现在解决了xml的标签问题,
最后只需要生成后的ppt地址,传入这个方法,执行一次标签删除即可
处理前效果:
处理后效果: