package com.allen.doc.poi;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackagingURIHelper;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xslf.usermodel.*;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import static org.apache.poi.openxml4j.opc.TargetMode.INTERNAL;
/**
* 低版本POI(3.17)终极适配:
* 1. XSLFSlideMaster构造器protected → 反射实例化
* 2. 无createTheme/createSlideMaster/SlideLayoutType
* 3. 完整构建母版-布局-主题-幻灯片关系
*/
public class LowVersionPOIFinalBuilder {
// 低版本布局类型常量(替代SlideLayoutType)
private static final int LAYOUT_TITLE_AND_CONTENT = 1;
private static final int LAYOUT_BLANK = 6;
private static Integer index = 12;
public static void main(String[] args) {
XMLSlideShow ppt = new XMLSlideShow();
try {
// ========== 步骤1:手动创建XSLFTheme(无createTheme()) ==========
XSLFTheme customTheme = createTheme(ppt);
System.out.println("Theme创建完成:" + customTheme.getPackagePart().getPartName());
// ========== 步骤2:反射实例化XSLFSlideMaster(解决protected构造器) ==========
XSLFSlideMaster customMaster = createSlideMasterByReflection(ppt, customTheme);
System.out.println("母版反射实例化完成:" + customMaster.getPackagePart().getPartName());
// ========== 步骤3:手动创建XSLFSlideLayout并绑定母版 ==========
XSLFSlideLayout customLayout = createSlideLayout(ppt, customMaster, LAYOUT_TITLE_AND_CONTENT);
System.out.println("布局创建完成:" + customLayout.getPackagePart().getPartName());
String rId = "rId" + (customMaster.getPackagePart().getRelationships().size() + 1);
customMaster.getPackagePart().addRelationship(customLayout.getPackagePart().getPartName(), INTERNAL, XSLFRelation.SLIDE_LAYOUT.getRelation(), rId);
// ========== 步骤4:基于自定义布局创建幻灯片 ==========
XSLFSlide slide = createSlide(ppt, customLayout);
System.out.println("幻灯片创建完成:" + slide.getPackagePart().getPartName());
// ========== 保存验证 ==========
savePPT(ppt, "final_low_version_ppt.pptx");
System.out.println("PPT生成成功:final_low_version_ppt.pptx");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ppt.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 步骤1:手动创建XSLFTheme(低版本无createTheme())
*/
private static XSLFTheme createTheme(XMLSlideShow ppt) throws Exception {
OPCPackage opc = ppt.getPackage();
// 1. 创建Theme的PackagePart
// PackagePartName themePartName = PackagingURIHelper.createPartName("/ppt/theme/theme1.xml");
PackagePartName themePartName = PackagingURIHelper.createPartName("/ppt/theme/theme" + (ppt.getSlideMasters().size() + 1) + ".xml");
PackagePart themePart = opc.createPart(themePartName, XSLFRelation.THEME.getContentType());
// 2. 写入极简Theme XML(保证结构合法)
String themeXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<a:theme xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" name=\"CustomTheme\">" +
"<a:themeElements>" +
"<a:fontScheme name=\"CustomFont\">" +
"<a:majorFont><a:latin typeface=\"微软雅黑\"/></a:majorFont>" +
"<a:minorFont><a:latin typeface=\"微软雅黑\"/></a:minorFont>" +
"</a:fontScheme>" +
"</a:themeElements>" +
"</a:theme>";
try (OutputStream out = themePart.getOutputStream()) {
IOUtils.copy(new ByteArrayInputStream(themeXml.getBytes("UTF-8")), out);
}
// 3. 实例化XSLFTheme(构造器是public)
return new XSLFTheme(themePart);
}
/**
* 步骤2:反射调用XSLFSlideMaster的protected构造器实例化母版
* 核心:解决"构造器protected无法new"的问题
*/
private static XSLFSlideMaster createSlideMasterByReflection(XMLSlideShow ppt, XSLFTheme theme) throws Exception {
OPCPackage opc = ppt.getPackage();
// 1. 创建母版的PackagePart
// PackagePartName masterPartName = PackagingURIHelper.createPartName("/ppt/slideMasters/slideMaster1.xml");
PackagePartName masterPartName = PackagingURIHelper.createPartName("/ppt/slideMasters/slideMaster" + (ppt.getSlideMasters().size() + 1) + ".xml");
PackagePart masterPart = opc.createPart(masterPartName, XSLFRelation.SLIDE_MASTER.getContentType());
// 2. 写入母版基础XML(绑定主题关系)
String masterXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<p:sldMaster xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\">" +
"<p:themeOverride>" +
"<a:themeElements xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">" +
"<a:fontScheme name=\"\"><a:majorFont><a:latin typeface=\"微软雅黑\"/></a:majorFont>" +
"<a:minorFont><a:latin typeface=\"微软雅黑\"/></a:minorFont></a:fontScheme>" +
"</a:themeElements>" +
"</p:themeOverride>" +
"</p:sldMaster>";
try (OutputStream out = masterPart.getOutputStream()) {
IOUtils.copy(new ByteArrayInputStream(masterXml.getBytes("UTF-8")), out);
}
// 3. 母版添加主题关系(核心:母版依赖主题)
masterPart.addRelationship(theme.getPackagePart().getPartName(),
INTERNAL,
XSLFRelation.THEME.getRelation(),
"rId1");
// 4. 反射调用XSLFSlideMaster的protected构造器(关键修复点)
// 获取构造器:参数为(PackagePart)
Constructor<XSLFSlideMaster> masterConstructor = XSLFSlideMaster.class.getDeclaredConstructor(PackagePart.class);
masterConstructor.setAccessible(true); // 绕开protected限制
XSLFSlideMaster master = masterConstructor.newInstance(masterPart); // 实例化母版
// 5. 将母版加入PPT的母版列表
ppt.getSlideMasters().add(master);
// 6. 初始化母版的_layouts Map(避免null)
initMasterLayoutsMap(master);
return master;
}
/**
* 步骤3:手动创建XSLFSlideLayout(低版本无createSlideLayout())
*/
private static XSLFSlideLayout createSlideLayout(XMLSlideShow ppt, XSLFSlideMaster master, int layoutType) throws Exception {
OPCPackage opc = ppt.getPackage();
// 1. 生成布局PartName(序号递增)
int layoutIdx = master.getSlideLayouts().length + 1;
if (layoutIdx < 10) {
layoutIdx = index;
}
index++;
PackagePartName layoutPartName = PackagingURIHelper.createPartName("/ppt/slideLayouts/slideLayout" + layoutIdx + ".xml");
PackagePart layoutPart = opc.createPart(layoutPartName, XSLFRelation.SLIDE_LAYOUT.getContentType());
XSLFSlideLayout[] slideLayouts = new XMLSlideShow().getSlideMasters().get(0).getSlideLayouts();
XSLFSlideLayout defaultXSLFSlideLayout = slideLayouts[0];
// 2. 写入布局XML(适配标题+内容类型)
try (OutputStream out = layoutPart.getOutputStream()) {
// IOUtils.copy(new ByteArrayInputStream(layoutXml.getBytes("UTF-8")), out);
IOUtils.copy(defaultXSLFSlideLayout.getPackagePart().getInputStream(), out);
}
// 3. 布局添加母版关系
String rId = "rId" + (layoutPart.getRelationships().size() + 1);
layoutPart.addRelationship(master.getPackagePart().getPartName(),
INTERNAL,
XSLFRelation.SLIDE_MASTER.getRelation(),
rId);
// 4. 反射实例化XSLFSlideLayout(构造器同样protected)
Constructor<XSLFSlideLayout> layoutConstructor = XSLFSlideLayout.class.getDeclaredConstructor(PackagePart.class);
layoutConstructor.setAccessible(true);
XSLFSlideLayout layout = layoutConstructor.newInstance(layoutPart);
// 5. 反射绑定布局的master字段
bindLayoutMaster(layout, master);
// 6. 将布局加入母版的_layouts Map
Map<String, XSLFSlideLayout> layoutsMap = getMasterLayoutsMap(master);
String layoutKey = "slideLayout" + layoutIdx;
layoutsMap.put(layoutKey, layout);
return layout;
}
/**
* 步骤4:基于自定义布局创建幻灯片
*/
private static XSLFSlide createSlide(XMLSlideShow ppt, XSLFSlideLayout layout) throws Exception {
// 1. 创建空白幻灯片
// XSLFSlide slide = ppt.createSlide(layout);
XSLFSlide slide = ppt.createSlide();
// 2. 幻灯片添加布局关系
PackagePart slidePart = slide.getPackagePart();
slidePart.addRelationship(layout.getPackagePart().getPartName(),
INTERNAL,
XSLFRelation.SLIDE_LAYOUT.getRelation(),
"rId1");
// 3. 示例:添加内容验证
XSLFTextShape title = slide.createTextBox();
title.setAnchor(new java.awt.Rectangle(50, 50, 600, 80));
title.setText("低版本POI最终适配演示");
XSLFTextShape content = slide.createTextBox();
content.setAnchor(new java.awt.Rectangle(50, 150, 600, 300));
content.setText("1. 反射调用protected构造器实例化母版\n2. 无新增API依赖\n3. PPT可正常打开");
return slide;
}
// ===================== 工具方法 =====================
/**
* 初始化母版的_layouts Map(避免null)
*/
private static void initMasterLayoutsMap(XSLFSlideMaster master) throws Exception {
Field layoutsField = XSLFSlideMaster.class.getDeclaredField("_layouts");
layoutsField.setAccessible(true);
if (layoutsField.get(master) == null) {
layoutsField.set(master, new HashMap<String, XSLFSlideLayout>());
}
}
/**
* 获取母版的_layouts Map
*/
private static Map<String, XSLFSlideLayout> getMasterLayoutsMap(XSLFSlideMaster master) throws Exception {
Field layoutsField = XSLFSlideMaster.class.getDeclaredField("_layouts");
layoutsField.setAccessible(true);
return (Map<String, XSLFSlideLayout>) layoutsField.get(master);
}
/**
* 反射绑定布局的master字段
*/
private static void bindLayoutMaster(XSLFSlideLayout layout, XSLFSlideMaster master) throws Exception {
Field masterField = XSLFSlideLayout.class.getDeclaredField("_master");
masterField.setAccessible(true);
masterField.set(layout, master);
}
// /**
// * 根据布局类型返回XML模板
// */
// private static String getLayoutXml(int layoutType) {
// if (layoutType == LAYOUT_TITLE_AND_CONTENT) {
// return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
// "<p:sldLayout xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\" type=\"titleAndContent\">" +
// "<p:cSld><p:spTree>" +
// "<p:sp><p:nvSpPr><p:cNvPr id=\"1\" name=\"标题\"/><p:cNvSpPr/></p:nvSpPr>" +
// "<p:spPr><a:xfrm xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\"><a:off x=\"50000\" y=\"50000\"/><a:ext cx=\"600000\" cy=\"80000\"/></a:xfrm></p:spPr>" +
// "<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>标题占位符</a:t></a:r></a:p></p:txBody></p:sp>" +
// "<p:sp><p:nvSpPr><p:cNvPr id=\"2\" name=\"内容\"/><p:cNvSpPr/></p:nvSpPr>" +
// "<p:spPr><a:xfrm><a:off x=\"50000\" y=\"150000\"/><a:ext cx=\"600000\" cy=\"300000\"/></a:xfrm></p:spPr>" +
// "<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>内容占位符</a:t></a:r></a:p></p:txBody></p:sp>" +
// "</p:spTree></p:cSld></p:sldLayout>";
// } else {
// // 空白布局
// return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
// "<p:sldLayout xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\" type=\"blank\">" +
// "<p:cSld/></p:sldLayout>";
// }
// }
/**
* 保存PPT
*/
private static void savePPT(XMLSlideShow ppt, String path) throws IOException {
try (FileOutputStream fos = new FileOutputStream(path)) {
ppt.write(fos);
}
}
}
package com.allen.doc.poi;
import org.apache.poi.openxml4j.opc.*;
import org.apache.poi.sl.usermodel.Placeholder;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xslf.usermodel.*;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static org.apache.poi.openxml4j.opc.TargetMode.INTERNAL;
/**
* 修复版:低版本POI(3.17)构建合法PPT,解决打开报错问题
* 核心优化:
* 1. 修复布局XML流读取异常,使用内置合法模板
* 2. 清理重复关系绑定,遵循OPC规范
* 3. 规范文件命名/ID生成规则
* 4. 修复反射字段绑定、构造器参数错误
*/
public class FixedLowVersionPOIBuilder {
// 低版本布局类型常量
private static final int LAYOUT_TITLE_AND_CONTENT = 1;
// 全局计数:保证Layout文件命名连续递增(从1开始)
private static int layoutSeq = 12;
public static void main(String[] args) {
XMLSlideShow ppt = new XMLSlideShow();
try {
// 步骤1:创建合法Theme(绑定PPT上下文)
XSLFTheme customTheme = createValidTheme(ppt);
System.out.println("Theme创建完成:" + customTheme.getPackagePart().getPartName());
// 步骤2:反射实例化母版(修复构造器+关系绑定)
XSLFSlideMaster customMaster = createValidSlideMaster(ppt, customTheme);
System.out.println("母版创建完成:" + customMaster.getPackagePart().getPartName());
// 步骤3:创建合法布局(修复XML模板+ID规则)
XSLFSlideLayout customLayout = createValidSlideLayout(ppt, customMaster, LAYOUT_TITLE_AND_CONTENT);
System.out.println("布局创建完成:" + customLayout.getPackagePart().getPartName());
// 步骤4:基于布局创建幻灯片(移除重复绑定)
XSLFSlide slide = createValidSlide(ppt, customLayout);
System.out.println("幻灯片创建完成:" + slide.getPackagePart().getPartName());
// 步骤5:保存PPT(确保流正常关闭)
saveValidPPT(ppt, "fixed_low_version_ppt.pptx");
System.out.println("PPT生成成功,可正常打开:fixed_low_version_ppt.pptx");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ppt.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 修复点1:创建合法Theme(绑定PPT上下文+规范命名)
*/
private static XSLFTheme createValidTheme(XMLSlideShow ppt) throws Exception {
OPCPackage opc = ppt.getPackage();
// 规范Theme命名:从theme1.xml开始连续递增
int themeIdx = opc.getPartsByContentType(XSLFRelation.THEME.getContentType()).size() + 1;
PackagePartName themePartName = PackagingURIHelper.createPartName("/ppt/theme/theme" + themeIdx + ".xml");
PackagePart themePart = opc.createPart(themePartName, XSLFRelation.THEME.getContentType());
// 写入PowerPoint兼容的Theme XML(完整合法结构)
String validThemeXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
"<a:theme xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" name=\"CustomTheme\">" +
"<a:themeElements>" +
"<a:clrScheme name=\"CustomColor\">" +
"<a:dk1><a:srgbClr val=\"000000\"/></a:dk1>" +
"<a:lt1><a:srgbClr val=\"FFFFFF\"/></a:lt1>" +
"<a:dk2><a:srgbClr val=\"44546A\"/></a:dk2>" +
"<a:lt2><a:srgbClr val=\"E7E6E6\"/></a:lt2>" +
"<a:accent1><a:srgbClr val=\"4F81BD\"/></a:accent1>" +
"<a:accent2><a:srgbClr val=\"C0504D\"/></a:accent2>" +
"<a:accent3><a:srgbClr val=\"9BBB59\"/></a:accent3>" +
"<a:accent4><a:srgbClr val=\"8064A2\"/></a:accent4>" +
"<a:accent5><a:srgbClr val=\"4BACC6\"/></a:accent5>" +
"<a:accent6><a:srgbClr val=\"F79646\"/></a:accent6>" +
"<a:hlink><a:srgbClr val=\"0000FF\"/></a:hlink>" +
"<a:folHlink><a:srgbClr val=\"800080\"/></a:folHlink>" +
"</a:clrScheme>" +
"<a:fontScheme name=\"CustomFont\">" +
"<a:majorFont><a:latin typeface=\"微软雅黑\"/><a:ea typeface=\"微软雅黑\"/><a:cs typeface=\"微软雅黑\"/></a:majorFont>" +
"<a:minorFont><a:latin typeface=\"微软雅黑\"/><a:ea typeface=\"微软雅黑\"/><a:cs typeface=\"微软雅黑\"/></a:minorFont>" +
"</a:fontScheme>" +
"<a:fmtScheme name=\"CustomFormat\">" +
"<a:fillStyleLst>" +
"<a:solidFill><a:srgbClr val=\"FFFFFF\"/></a:solidFill>" +
"<a:gradFill><a:gsLst><a:gs pos=\"0\"><a:srgbClr val=\"E6E6E6\"/></a:gs><a:gs pos=\"100000\"><a:srgbClr val=\"FFFFFF\"/></a:gs></a:gsLst><a:lin ang=\"2700000\" scaled=\"1\"/></a:gradFill>" +
"</a:fillStyleLst>" +
"<a:lnStyleLst><a:ln w=\"10000\" cap=\"flat\" cmpd=\"sng\" algn=\"ctr\"><a:solidFill><a:srgbClr val=\"B7B7B7\"/></a:solidFill><a:prstDash val=\"solid\"/></a:ln></a:lnStyleLst>" +
"<a:effectStyleLst><a:effectStyle><a:efctLst><a:outerShdw blur=\"80000\" dist=\"30000\" dir=\"2700000\" algn=\"ctr\" rotWithShape=\"0\"><a:srgbClr val=\"000000\" alpha=\"30000\"/></a:outerShdw></a:efctLst></a:effectStyle></a:effectStyleLst>" +
"<a:bgFillStyleLst><a:solidFill><a:srgbClr val=\"FFFFFF\"/></a:solidFill></a:bgFillStyleLst>" +
"</a:fmtScheme>" +
"</a:themeElements>" +
"<a:objectDefaults/>" +
"<a:extraClrSchemeLst/>" +
"</a:theme>";
try (OutputStream out = themePart.getOutputStream()) {
// IOUtils.write(validThemeXml.getBytes("UTF-8"), out);
IOUtils.copy(new ByteArrayInputStream(validThemeXml.getBytes(StandardCharsets.UTF_8)), out);
}
// 修复:低版本XSLFTheme构造器需传入PPT上下文
return new XSLFTheme(themePart);
}
/**
* 修复点2:创建合法母版(修复构造器+规范XML+关系绑定)
*/
private static XSLFSlideMaster createValidSlideMaster(XMLSlideShow ppt, XSLFTheme theme) throws Exception {
OPCPackage opc = ppt.getPackage();
// 规范母版命名:从slideMaster1.xml开始
int masterIdx = ppt.getSlideMasters().size() + 1;
PackagePartName masterPartName = PackagingURIHelper.createPartName("/ppt/slideMasters/slideMaster" + masterIdx + ".xml");
PackagePart masterPart = opc.createPart(masterPartName, XSLFRelation.SLIDE_MASTER.getContentType());
// 写入完整合法的母版XML(包含布局ID列表+主题引用)
String validMasterXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
"<p:sldMaster xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\" xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
"<p:bg><p:bgPr><a:solidFill><a:srgbClr val=\"F5F5F5\"/></a:solidFill></p:bgPr></p:bg>" +
"<p:txStyles>" +
"<p:titleStyle><a:bodyPr lIns=\"50000\" tIns=\"50000\" rIns=\"50000\" bIns=\"50000\" anchor=\"ctr\" vert=\"ctr\" wrap=\"square\" rtlCol=\"0\"/>" +
"<a:lstStyle><a:defPPr algn=\"ctr\"><a:defRPr sz=\"3200\" b=\"1\" latin=\"微软雅黑\" ea=\"微软雅黑\" cs=\"微软雅黑\"/></a:defPPr></a:lstStyle>" +
"</p:titleStyle>" +
"<p:bodyStyle><a:bodyPr lIns=\"50000\" tIns=\"20000\" rIns=\"50000\" bIns=\"20000\" anchor=\"t\" vert=\"t\" wrap=\"square\" rtlCol=\"0\"/>" +
"<a:lstStyle><a:defPPr algn=\"l\"><a:defRPr sz=\"1800\" latin=\"微软雅黑\" ea=\"微软雅黑\" cs=\"微软雅黑\"/></a:defPPr></a:lstStyle>" +
"</p:bodyStyle>" +
"</p:txStyles>" +
"<p:themeOverride>" +
"<a:themeElements>" +
"<a:fontScheme name=\"CustomFont\">" +
"<a:majorFont><a:latin typeface=\"微软雅黑\"/><a:ea typeface=\"微软雅黑\"/><a:cs typeface=\"微软雅黑\"/></a:majorFont>" +
"<a:minorFont><a:latin typeface=\"微软雅黑\"/><a:ea typeface=\"微软雅黑\"/><a:cs typeface=\"微软雅黑\"/></a:minorFont>" +
"</a:fontScheme>" +
"</a:themeElements>" +
"</p:themeOverride>" +
"<p:sldLayoutIdLst>" +
"<p:sldLayoutId id=\"2147483648\" r:id=\"rId2\"/>" + // 预留布局ID,避免解析失败
"</p:sldLayoutIdLst>" +
"<p:transition/>" +
"<p:timing/>" +
"</p:sldMaster>";
try (OutputStream out = masterPart.getOutputStream()) {
// IOUtils.write(validMasterXml.getBytes("UTF-8"), out);
IOUtils.copy(new ByteArrayInputStream(validMasterXml.getBytes(StandardCharsets.UTF_8)), out);
}
// 母版绑定主题(唯一合法关系:母版→主题)
String themeRId = "rId1";
masterPart.addRelationship(theme.getPackagePart().getPartName(),
INTERNAL, XSLFRelation.THEME.getRelation(), themeRId);
// 修复:动态适配母版构造器(兼容所有POI 3.17子版本)
XSLFSlideMaster master = null;
Constructor<?>[] constructors = XSLFSlideMaster.class.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
Class<?>[] paramTypes = constructor.getParameterTypes();
if (paramTypes.length == 1 && paramTypes[0] == PackagePart.class) {
constructor.setAccessible(true);
master = (XSLFSlideMaster) constructor.newInstance(masterPart);
break;
} else if (paramTypes.length == 2) {
constructor.setAccessible(true);
try {
if (paramTypes[0] == XMLSlideShow.class && paramTypes[1] == PackagePart.class) {
master = (XSLFSlideMaster) constructor.newInstance(ppt, masterPart);
} else if (paramTypes[0] == PackagePart.class && paramTypes[1] == XMLSlideShow.class) {
master = (XSLFSlideMaster) constructor.newInstance(masterPart, ppt);
}
if (master != null) break;
} catch (Exception e) {
continue;
}
}
}
if (master == null) {
throw new RuntimeException("未找到可用的XSLFSlideMaster构造器");
}
// 加入母版列表+初始化_layouts Map
ppt.getSlideMasters().add(master);
initMasterLayoutsMap(master);
return master;
}
/**
* 修复点3:创建合法布局(移除无效流读取+规范XML+ID规则)
*/
private static XSLFSlideLayout createValidSlideLayout(XMLSlideShow ppt, XSLFSlideMaster master, int layoutType) throws Exception {
OPCPackage opc = ppt.getPackage();
// 修复:Layout命名从1开始连续递增,移除硬编码index
int layoutIdx = layoutSeq++;
PackagePartName layoutPartName = PackagingURIHelper.createPartName("/ppt/slideLayouts/slideLayout" + layoutIdx + ".xml");
PackagePart layoutPart = opc.createPart(layoutPartName, XSLFRelation.SLIDE_LAYOUT.getContentType());
// 写入PowerPoint兼容的标题+内容布局XML(替换无效流读取)
String validLayoutXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
"<p:sldLayout xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\" xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" type=\"titleAndContent\">" +
"<p:cSld>" +
"<p:spTree>" +
"<p:nvGrpSpPr><p:cNvPr id=\"1\" name=\"标题和内容布局\"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>" +
"<p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"9144000\" cy=\"6858000\"/><a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"9144000\" cy=\"6858000\"/></a:xfrm></p:grpSpPr>" +
// 标题占位符
"<p:sp>" +
"<p:nvSpPr><p:cNvPr id=\"2\" name=\"标题\"/><p:cNvSpPr><a:spLocks noGrp=\"1\"/><p:ph type=\"title\" idx=\"1\"/></p:cNvSpPr><p:nvPr/></p:nvSpPr>" +
"<p:spPr><a:xfrm><a:off x=\"500000\" y=\"500000\"/><a:ext cx=\"8144000\" cy=\"800000\"/></a:xfrm><a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom></p:spPr>" +
"<p:txBody><a:bodyPr lIns=\"50000\" tIns=\"20000\" rIns=\"50000\" bIns=\"20000\" anchor=\"t\" vert=\"t\" wrap=\"square\" rtlCol=\"0\"/>" +
"<a:lstStyle><a:defPPr algn=\"ctr\"><a:defRPr sz=\"3200\" b=\"1\" latin=\"微软雅黑\"/></a:defPPr></a:lstStyle>" +
"<a:p><a:r><a:t>标题占位符</a:t></a:r></a:p>" +
"</p:txBody>" +
"</p:sp>" +
// 内容占位符
"<p:sp>" +
"<p:nvSpPr><p:cNvPr id=\"3\" name=\"内容\"/><p:cNvSpPr><a:spLocks noGrp=\"1\"/><p:ph type=\"body\" idx=\"2\"/></p:cNvSpPr><p:nvPr/></p:nvSpPr>" +
"<p:spPr><a:xfrm><a:off x=\"500000\" y=\"1500000\"/><a:ext cx=\"8144000\" cy=\"4500000\"/></a:xfrm><a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom></p:spPr>" +
"<p:txBody><a:bodyPr lIns=\"50000\" tIns=\"20000\" rIns=\"50000\" bIns=\"20000\" anchor=\"t\" vert=\"t\" wrap=\"square\" rtlCol=\"0\"/>" +
"<a:lstStyle><a:defPPr algn=\"l\"><a:defRPr sz=\"1800\" latin=\"微软雅黑\"/></a:defPPr></a:lstStyle>" +
"<a:p><a:r><a:t>内容占位符</a:t></a:r></a:p>" +
"</p:txBody>" +
"</p:sp>" +
"</p:spTree>" +
"<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>" +
"</p:cSld>" +
"<p:transition/>" +
"</p:sldLayout>";
try (OutputStream out = layoutPart.getOutputStream()) {
// IOUtils.write(validLayoutXml.getBytes("UTF-8"), out);
IOUtils.copy(new ByteArrayInputStream(validLayoutXml.getBytes(StandardCharsets.UTF_8)), out);
}
// 布局绑定母版(唯一合法关系:布局→母版)
String masterRId = "rId1";
layoutPart.addRelationship(master.getPackagePart().getPartName(),
INTERNAL, XSLFRelation.SLIDE_MASTER.getRelation(), masterRId);
// 动态适配布局构造器
XSLFSlideLayout layout = null;
Constructor<?>[] constructors = XSLFSlideLayout.class.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
Class<?>[] paramTypes = constructor.getParameterTypes();
if (paramTypes.length == 1 && paramTypes[0] == PackagePart.class) {
constructor.setAccessible(true);
layout = (XSLFSlideLayout) constructor.newInstance(layoutPart);
break;
} else if (paramTypes.length == 2 && paramTypes[0] == XSLFSlideMaster.class && paramTypes[1] == PackagePart.class) {
constructor.setAccessible(true);
layout = (XSLFSlideLayout) constructor.newInstance(master, layoutPart);
break;
}
}
if (layout == null) {
throw new RuntimeException("未找到可用的XSLFSlideLayout构造器");
}
// 修复:兼容反射字段名(master/_master)
bindLayoutMaster(layout, master);
// 加入母版_layouts Map
Map<String, XSLFSlideLayout> layoutsMap = getMasterLayoutsMap(master);
layoutsMap.put("slideLayout" + layoutIdx, layout);
return layout;
}
/**
* 修复点4:创建合法幻灯片(移除重复关系绑定)
*/
private static XSLFSlide createValidSlide(XMLSlideShow ppt, XSLFSlideLayout layout) throws Exception {
// 修复:低版本POI需先创建空白幻灯片,再绑定布局(避免自动绑定失败)
XSLFSlide slide = ppt.createSlide();
PackagePart slidePart = slide.getPackagePart();
// 幻灯片绑定布局(仅需一次,避免重复)
if (!slidePart.getRelationshipsByType(XSLFRelation.SLIDE_LAYOUT.getRelation()).iterator().hasNext()) {
slidePart.addRelationship(layout.getPackagePart().getPartName(),
INTERNAL, XSLFRelation.SLIDE_LAYOUT.getRelation(), "rId1");
}
// 添加测试内容(使用占位符而非手动创建文本框,兼容布局)
XSLFTextShape titleShape = (XSLFTextShape)slide.getPlaceholder(Placeholder.TITLE);
// XSLFSimpleShape titleShape = slide.getPlaceholder(Placeholder.TITLE);
if (titleShape != null) {
titleShape.setText("低版本POI修复版演示");
}
XSLFTextShape contentShape = (XSLFTextShape)slide.getPlaceholder(Placeholder.BODY);
if (contentShape != null) {
contentShape.setText("1. 修复XML结构不合法问题\n2. 清理重复关系绑定\n3. 规范文件命名规则\n4. 兼容PowerPoint解析");
}
return slide;
}
// ===================== 工具方法(修复+兼容) =====================
private static void initMasterLayoutsMap(XSLFSlideMaster master) throws Exception {
Field layoutsField = getField(XSLFSlideMaster.class, "_layouts", "layouts");
layoutsField.setAccessible(true);
if (layoutsField.get(master) == null) {
layoutsField.set(master, new HashMap<String, XSLFSlideLayout>());
}
}
private static Map<String, XSLFSlideLayout> getMasterLayoutsMap(XSLFSlideMaster master) throws Exception {
Field layoutsField = getField(XSLFSlideMaster.class, "_layouts", "layouts");
layoutsField.setAccessible(true);
return (Map<String, XSLFSlideLayout>) layoutsField.get(master);
}
private static void bindLayoutMaster(XSLFSlideLayout layout, XSLFSlideMaster master) throws Exception {
Field masterField = getField(XSLFSlideLayout.class, "_master", "master");
masterField.setAccessible(true);
masterField.set(layout, master);
}
/**
* 兼容反射字段名(适配不同POI子版本的_master/master字段)
*/
private static Field getField(Class<?> clazz, String... fieldNames) throws NoSuchFieldException {
for (String fieldName : fieldNames) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
continue;
}
}
throw new NoSuchFieldException("未找到字段:" + String.join(",", fieldNames));
}
private static void saveValidPPT(XMLSlideShow ppt, String path) throws IOException {
// 修复:确保所有流刷新并正常写入
try (FileOutputStream fos = new FileOutputStream(path)) {
ppt.write(fos);
fos.flush();
}
}
}