poi-主题&&母版&&布局&&幻灯片的关系

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();
        }
    }
}

  

posted @ 2025-12-18 00:37  Allen_Hao  阅读(13)  评论(0)    收藏  举报