在 Java 中管理演示文稿超链接

简介

超链接将演示文稿内容连接到网站或演示文稿内的某个位置。在 PowerPoint 中,超链接通常有两个用途:

  • 从文本、形状或媒体框打开网站。
  • 从目录等跳转到另一张幻灯片。

Aspose.Slides for Java 允许您添加这些链接、控制其外观和声音、更新属性并将其删除。下面的示例展示了如何在单个元素上使用超链接以及如何在演示文稿、幻灯片或文本框级别访问超链接。

添加 URL 超链接

您可以将网站 URL 分配给文本、形状或媒体框。分配超链接的元素决定可点击区域:文本部分链接所选文本,形状或框则链接整个幻灯片对象。

向文本添加 URL 超链接

要将文本链接到网站,请将Hyperlink传递给文本段落的setHyperlinkClick方法,如下所示。只有该段文本可点击。

import com.aspose.slides.*;

Presentation presentation = new Presentation();
try {
    IAutoShape textShape = presentation.getSlides().get_Item(0).getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 600, 50, false);
    textShape.addTextFrame("Aspose: File Format APIs");
    IPortionFormat portionFormat = textShape.getTextFrame().getParagraphs().get_Item(0).getPortions().get_Item(0).getPortionFormat();
    portionFormat.setHyperlinkClick(new Hyperlink("https://www.aspose.com/"));
    portionFormat.getHyperlinkClick().setTooltip("Explore Aspose file format APIs");
    portionFormat.setFontHeight(32);

    presentation.save("presentation-out.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

向形状和媒体框添加 URL 超链接

要使形状或框可点击,调用其setHyperlinkClick方法。超链接属于对象本身,而不是其中的文本段落。

同样的方法适用于图片、音频和视频框:将超链接分配给框并在需要时调用setTooltip。

下面的示例使一个矩形可点击:

import com.aspose.slides.*;

Presentation presentation = new Presentation();
try {
    IAutoShape shape = presentation.getSlides().get_Item(0).getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 600, 50);

    shape.setHyperlinkClick(new Hyperlink("https://www.aspose.com/"));
    shape.getHyperlinkClick().setTooltip("Explore Aspose file format APIs");

    presentation.save("presentation-out.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

使用超链接创建目录

内部超链接允许读者从目录跳转到特定幻灯片。下面的示例使用setInternalHyperlinkClick将第一张幻灯片上的“Page 2”文本链接到第二张幻灯片。

import com.aspose.slides.*;
import java.awt.Color;

Presentation presentation = new Presentation();
try {
    ISlide firstSlide = presentation.getSlides().get_Item(0);
    ISlide secondSlide = presentation.getSlides().addEmptySlide(firstSlide.getLayoutSlide());

    IAutoShape tableOfContents = firstSlide.getShapes().addAutoShape(ShapeType.Rectangle, 40, 40, 300, 100);
    tableOfContents.getFillFormat().setFillType(FillType.NoFill);
    tableOfContents.getLineFormat().getFillFormat().setFillType(FillType.NoFill);
    tableOfContents.getTextFrame().getParagraphs().clear();

    Paragraph paragraph = new Paragraph();
    paragraph.getParagraphFormat().getDefaultPortionFormat().getFillFormat().setFillType(FillType.Solid);
    paragraph.getParagraphFormat().getDefaultPortionFormat().getFillFormat().getSolidFillColor().setColor(Color.BLACK);
    paragraph.setText("Title of slide 2 .......... ");

    Portion linkPortion = new Portion();
    linkPortion.setText("Page 2");
    linkPortion.getPortionFormat().getHyperlinkManager().setInternalHyperlinkClick(secondSlide);

    paragraph.getPortions().add(linkPortion);
    tableOfContents.getTextFrame().getParagraphs().add(paragraph);

    presentation.save("link_to_slide.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

格式化超链接

颜色

IHyperlink的setColorSource方法决定超链接是使用演示文稿的超链接颜色还是文本段落的格式。要应用自定义文本颜色,请选择HyperlinkColorSource.PortionFormat并设置段落的填充颜色。此功能在 PowerPoint 2019 中引入;旧版本不支持此设置。

以下示例向同一张幻灯片添加了两个文本超链接。第一个使用红色文本填充,第二个保留默认超链接颜色。

import com.aspose.slides.*;
import java.awt.Color;

Presentation presentation = new Presentation();
try {
    IAutoShape coloredShape = presentation.getSlides().get_Item(0).getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 450, 50, false);
    coloredShape.addTextFrame("This hyperlink uses a custom color.");
    IPortionFormat coloredPortionFormat = coloredShape.getTextFrame().getParagraphs().get_Item(0).getPortions().get_Item(0).getPortionFormat();
    coloredPortionFormat.setHyperlinkClick(new Hyperlink("https://www.aspose.com/"));
    coloredPortionFormat.getHyperlinkClick().setColorSource(HyperlinkColorSource.PortionFormat);
    coloredPortionFormat.getFillFormat().setFillType(FillType.Solid);
    coloredPortionFormat.getFillFormat().getSolidFillColor().setColor(Color.RED);

    IAutoShape defaultShape = presentation.getSlides().get_Item(0).getShapes().addAutoShape(ShapeType.Rectangle, 100, 200, 450, 50, false);
    defaultShape.addTextFrame("This hyperlink uses the default color.");
    defaultShape.getTextFrame().getParagraphs().get_Item(0).getPortions().get_Item(0).getPortionFormat().setHyperlinkClick(new Hyperlink("https://www.aspose.com/"));

    presentation.save("presentation-out-hyperlink.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

声音

超链接在激活时可以播放声音,或停止已在播放的声音。使用以下方法配置这些行为:

添加超链接声音

以下示例加载 sampleaudio.wav 并将其关联到第一张幻灯片上的一个按钮。单击该按钮会播放声音并跳转到下一张幻灯片。同一幻灯片上的第二个形状在单击时停止先前的声音,但不执行导航操作。

import com.aspose.slides.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

Presentation presentation = new Presentation();
try {
    byte[] audioData = Files.readAllBytes(Paths.get("sampleaudio.wav"));
    IAudio hyperlinkSound = presentation.getAudios().addAudio(audioData);

    ISlide firstSlide = presentation.getSlides().get_Item(0);

    IAutoShape playButton = firstSlide.getShapes().addAutoShape(ShapeType.SoundButton, 100, 100, 100, 50);
    playButton.setHyperlinkClick(Hyperlink.getNextSlide());

    if (!playButton.getHyperlinkClick().getStopSoundOnClick() && playButton.getHyperlinkClick().getSound() == null)
    {
        playButton.getHyperlinkClick().setSound(hyperlinkSound);
    }

    ISlide secondSlide = presentation.getSlides().addEmptySlide(firstSlide.getLayoutSlide());

    IAutoShape stopButton = secondSlide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 100, 50);
    stopButton.setHyperlinkClick(Hyperlink.getNoAction());

    stopButton.getHyperlinkClick().setStopSoundOnClick(true);

    presentation.save("hyperlink-sound.pptx", SaveFormat.Pptx);
} catch (IOException exception) {
    System.out.println("Unable to read the audio file: " + exception.getMessage());
} finally {
    presentation.dispose();
}

提取超链接声音

以下示例打开前面创建的演示文稿,并通过getSound 和 getBinaryData 将第一个形状的超链接音频读取到内存中。

import com.aspose.slides.*;

Presentation presentation = new Presentation("hyperlink-sound.pptx");
try {
    if (presentation.getSlides().size() > 0 && presentation.getSlides().get_Item(0).getShapes().size() > 0) {
        IHyperlink hyperlink = presentation.getSlides().get_Item(0).getShapes().get_Item(0).getHyperlinkClick();
        IAudio sound = hyperlink == null ? null : hyperlink.getSound();
        if (sound != null) {
            byte[] audioData = sound.getBinaryData();
            System.out.println("Extracted " + audioData.length + " bytes of hyperlink audio.");
        } else {
            System.out.println("The first shape has no hyperlink sound.");
        }
    } else {
        System.out.println("The presentation has no first slide or shape to inspect.");
    }
} finally {
    presentation.dispose();
}

提示文本和交互设置

在为文本或形状分配超链接后,您可以调用以下IHyperlink方法:

  • setTooltip 设置观众在悬停时看到的提示文本。
  • setTargetFrame 在适用时指定父 HTML frameset 中的目标帧。
  • setHistory 控制激活链接后是否将其目标加入已查看超链接列表。
  • setHighlightClick 控制点击时是否突出显示该超链接。

从演示文稿中删除超链接

使用getAnyHyperlinks 收集包括文本段落链接在内的所有超链接容器,然后再进行修改。下面的示例删除第一张幻灯片的两种激活类型。若只想删除一种类型,只调用removeHyperlinkClick或removeHyperlinkMouseOver;删除点击操作不会同时删除鼠标悬停对应的操作。

import com.aspose.slides.*;
import java.util.ArrayList;
import java.util.List;

Presentation presentation = new Presentation("pres.pptx");
try {
    if (presentation.getSlides().size() > 0) {
        List<IHyperlinkContainer> containers = new ArrayList<>();
        for (IHyperlinkContainer container : presentation.getSlides().get_Item(0).getHyperlinkQueries().getAnyHyperlinks()) {
            containers.add(container);
        }
        for (IHyperlinkContainer container : containers) {
            container.getHyperlinkManager().removeHyperlinkClick();
            container.getHyperlinkManager().removeHyperlinkMouseOver();
        }
        presentation.save("pres-removed-hyperlinks.pptx", SaveFormat.Pptx);
    } else {
        System.out.println("The presentation has no slides to process.");
    }
} finally {
    presentation.dispose();
}

若要无条件删除,removeAllHyperlinks 在一次调用中删除所选范围内的两种激活类型。若需有选择地清理并覆盖母版、版式和备注,请参见报告、清理和验证超链接。

构建完整的超链接清单

在分发演示文稿之前,需清点其交互操作以及网络链接。getAnyHyperlinks 返回IHyperlinkContainer对象,而不是单纯的 URL 字符串列表。请检查每个容器的getHyperlinkClick 和getHyperlinkMouseOver。它们是独立的:同一容器可以同时暴露两种操作,因此完整报告可能需要每个容器两行。

仅扫描形状级别的超链接可能遗漏附加在文本段落上的链接。请改为查询适当的范围,并保留返回的容器,以便后续更新或删除其操作。

查询演示文稿、幻灯片和文本框范围

IHyperlinkQueries 接口可通过IPresentation.getHyperlinkQueries、IBaseSlide.getHyperlinkQueries 和ITextFrame.getHyperlinkQueries 访问。每个范围支持相同的查询:

以下示例创建 hyperlink-audit-input.pptx,其中包含外部点击链接、文件鼠标悬停链接、内部幻灯片导航、文本鼠标悬停链接和宏操作。示例本身不执行任何这些操作。三种查询在每个范围内均可使用;计数描述的是容器数量,而非操作总数。文本框范围不包括其所属形状的链接。

import com.aspose.slides.*;

class QueryCounts {
    void print(String scope, IHyperlinkQueries queries) {
        int clickCount = queries.getHyperlinkClicks().size();
        int mouseOverCount = queries.getHyperlinkMouseOvers().size();
        int anyCount = queries.getAnyHyperlinks().size();
        System.out.println(scope + ": click=" + clickCount + ", mouse-over=" + mouseOverCount + ", any=" + anyCount);
    }
}

QueryCounts counts = new QueryCounts();
Presentation presentation = new Presentation();
try {
    ISlide slide = presentation.getSlides().get_Item(0);
    ISlide destination = presentation.getSlides().addEmptySlide(slide.getLayoutSlide());
    IAutoShape shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 20, 20, 400, 60);
    shape.getTextFrame().setText("Click the text to go to slide 2");
    shape.getHyperlinkManager().setExternalHyperlinkClick("https://example.com/");
    shape.getHyperlinkClick().setTooltip("Public website");
    shape.getHyperlinkManager().setExternalHyperlinkMouseOver("file:///C:/private/report.xlsx");

    IPortionFormat portionFormat = shape.getTextFrame().getParagraphs().get_Item(0).getPortions().get_Item(0).getPortionFormat();
    portionFormat.getHyperlinkManager().setInternalHyperlinkClick(destination);
    portionFormat.getHyperlinkManager().setExternalHyperlinkMouseOver("https://example.com/help");
    IAutoShape macroButton = slide.getShapes().addAutoShape(ShapeType.Rectangle, 20, 120, 200, 60);
    macroButton.getHyperlinkManager().setMacroHyperlinkClick("ReviewPresentation");

    counts.print("Presentation", presentation.getHyperlinkQueries());
    counts.print("Slide 1", slide.getHyperlinkQueries());
    counts.print("Text frame", shape.getTextFrame().getHyperlinkQueries());
    presentation.save("hyperlink-audit-input.pptx", SaveFormat.Pptx);
} finally {
    presentation.dispose();
}

在本示例中,演示文稿和幻灯片查询各报告 3 个点击容器、2 个鼠标悬停容器以及 3 个任一操作的容器。文本框查询在每个类别中各报告 1 个容器。

对操作和目标进行分类

使用 IHyperlink.getActionType 在解析目标之前先解释操作。HyperlinkActionType 包含的值超出单纯的网络导航:

值 审计含义
Hyperlink 外部超链接;检查 URL 及其协议。
JumpSpecificSlide 跳转到特定幻灯片的内部导航。
JumpFirstSlide, JumpPreviousSlide, JumpNextSlide, JumpLastSlide, JumpLastViewedSlide 幻灯片放映内置导航,在放映上下文中解析。
JumpEndShow, StartCustomSlideShow 结束当前放映或启动自定义放映。
StartMacro 执行宏。
StartProgram 启动程序。
OpenFile, OpenPresentation 打开文件或其他演示文稿;需单独审查,区别于网络 URL。
StartStopMedia 开始或停止媒体播放。
NoAction, Unknown 无导航操作,或需审查的未识别操作。

通过 getExternalUrl 读取外部目标,使用 getTargetSlide 读取具体内部目标。内部操作和内置命令可能没有外部 URL;空 URL 并不表示容器没有操作。若 getExternalUrlOriginal 返回的值与规范化 URL 不同,请保留原始值;若有提示文本,请使用 getTooltip 返回的内容。

报告、清理和验证超链接

下面的 Java 示例读取已有演示文稿(使用上面创建的文件),写入 hyperlink-audit.json,应用策略,保存为 hyperlink-sanitized.pptx,随后重新打开再次检查两种激活类型。示例在修改前收集容器并使用引用相等性避免重复处理。演示文稿查询覆盖普通幻灯片;若需对整个包进行清点,还显式查询母版、版式、备注以及备注和讲义母版(若存在)。

报告记录基于 1 的幻灯片索引以及在可能的情况下的 getSlideId。对于受支持的容器,可通过 ISlideComponent.getSlide 获取所属幻灯片。母版、版式和备注没有普通幻灯片索引,以其范围标识。形状容器和文本段落格式容器分别标记;其他容器类型保留其运行时类型名。每个容器获得报告本地 ID,以便关联其两种操作。报告将操作类型存为 Java 枚举定义的整数常量。

该策略仅允许绝对 HTTPS URL 和有效的内部幻灯片目标。它会拒绝宏、程序、文件操作、其他放映操作、未知操作以及其他 URL 协议。这些拒绝是策略决定,而非 Aspose.Slides 安全性判断。仅 HTTPS 并不等同于信任:请在您的应用中加入主机白名单等检查。原始和规范化的外部 URL 均会被检查。示例在不实际访问链接或执行操作的情况下审计元数据。

若需修正,容器的 getHyperlinkManager 支持 setExternalHyperlinkClick、removeHyperlinkClick 和 removeHyperlinkMouseOver。示例中,将被禁止的外部点击链接替换为固定的 HTTPS 着陆页;其他被禁止的点击和鼠标悬停操作则分别删除。将 replaceExternalClicks 设置为 false 可删除所有违规项。请在部署前准备好应用拥有的替换页面。

报告的导出标记采用保守的 PDF 审核策略:将鼠标悬停操作以及除外部链接或特定幻灯片跳转之外的所有操作标记为可能不受支持。这只是审查提示,而非功能测试,也不保证未标记的链接在导出后一定能保存。受支持的PDF和HTML 导出可能保留超链接,具体取决于操作、导出选项和阅读器。栅格图像和视频 则无法保留交互式超链接;在审计这些输出时请对每个操作都进行标记。

import com.aspose.slides.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

class HyperlinkAudit {
    Integer slideIndex(IPresentation presentation, IBaseSlide slide) {
        for (int index = 0; index < presentation.getSlides().size(); index++) {
            if (presentation.getSlides().get_Item(index) == slide) return index + 1;
        }
        return null;
    }

    boolean isHttps(String value) {
        if (value == null || value.isEmpty()) return false;
        try {
            URI uri = new URI(value);
            return uri.isAbsolute() && "https".equalsIgnoreCase(uri.getScheme()) && uri.getHost() != null;
        } catch (URISyntaxException exception) {
            return false;
        }
    }

    String policyViolation(IHyperlink link) {
        if (link == null) return null;
        if (link.getActionType() == HyperlinkActionType.JumpSpecificSlide) {
            return link.getTargetSlide() == null ? "Missing target slide" : null;
        }
        if (link.getActionType() != HyperlinkActionType.Hyperlink) return "Action is not allowed";
        if (!isHttps(link.getExternalUrl())) return "Normalized URL is not absolute HTTPS";
        String original = link.getExternalUrlOriginal();
        if (original != null && !original.isEmpty() && !isHttps(original)) return "Original URL is not absolute HTTPS";
        return null;
    }

    void addScope(List<IHyperlinkContainer> found, IBaseSlide slide) {
        if (slide != null) {
            for (IHyperlinkContainer container : slide.getHyperlinkQueries().getAnyHyperlinks()) {
                found.add(container);
            }
        }
    }

    List<IHyperlinkContainer> collectContainers(IPresentation presentation) {
        List<IHyperlinkContainer> found = new ArrayList<>();
        for (IHyperlinkContainer container : presentation.getHyperlinkQueries().getAnyHyperlinks()) {
            found.add(container);
        }
        for (IMasterSlide master : presentation.getMasters()) addScope(found, master);
        for (ILayoutSlide layout : presentation.getLayoutSlides()) addScope(found, layout);
        for (ISlide slide : presentation.getSlides()) addScope(found, slide.getNotesSlideManager().getNotesSlide());
        addScope(found, presentation.getMasterNotesSlideManager().getMasterNotesSlide());
        addScope(found, presentation.getMasterHandoutSlideManager().getMasterHandoutSlide());
        Set<IHyperlinkContainer> seen = Collections.newSetFromMap(new IdentityHashMap<IHyperlinkContainer, Boolean>());
        List<IHyperlinkContainer> unique = new ArrayList<>();
        for (IHyperlinkContainer container : found) {
            if (seen.add(container)) unique.add(container);
        }
        return unique;
    }

    void addRow(List<Map<String, Object>> rows, IPresentation presentation, IHyperlink link, String activation, IHyperlinkContainer container, int containerId) {
        if (link == null) return;
        IBaseSlide ownerSlide = container instanceof ISlideComponent ? ((ISlideComponent) container).getSlide() : null;
        ISlide targetSlide = link.getTargetSlide();
        String violation = policyViolation(link);
        String ownerType = container instanceof IShape ? "Shape" : container instanceof IPortionFormat ? "Text portion" : container.getClass().getSimpleName();
        boolean ordinaryAction = link.getActionType() == HyperlinkActionType.Hyperlink || link.getActionType() == HyperlinkActionType.JumpSpecificSlide;
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("ContainerId", containerId);
        row.put("SlideIndex", slideIndex(presentation, ownerSlide));
        row.put("SlideId", ownerSlide == null ? null : ownerSlide.getSlideId());
        row.put("Scope", ownerSlide == null ? null : ownerSlide.getClass().getSimpleName());
        row.put("OwnerType", ownerType);
        row.put("Activation", activation);
        row.put("ActionType", link.getActionType());
        row.put("ExternalUrl", link.getExternalUrl());
        row.put("TargetSlideIndex", slideIndex(presentation, targetSlide));
        row.put("TargetSlideId", targetSlide == null ? null : targetSlide.getSlideId());
        row.put("Tooltip", link.getTooltip());
        row.put("OriginalExternalUrl", Objects.equals(link.getExternalUrlOriginal(), link.getExternalUrl()) ? null : link.getExternalUrlOriginal());
        row.put("PotentiallyUnsafe", violation != null);
        row.put("PolicyViolation", violation);
        row.put("TargetExport", "PDF");
        row.put("PotentiallyUnsupportedByExport", "mouse-over".equals(activation) || !ordinaryAction);
        rows.add(row);
    }

    // 在不使用额外 JSON 依赖的情况下序列化此报告的平面行。
    String jsonValue(Object value) {
        if (value == null) return "null";
        if (value instanceof Number || value instanceof Boolean) return value.toString();
        StringBuilder escaped = new StringBuilder("\"");
        for (char character : value.toString().toCharArray()) {
            if (character == '"' || character == '\\') {
                escaped.append('\\').append(character);
            } else if (character < 0x20 || Character.isSurrogate(character)) {
                escaped.append(String.format("\\u%04x", (int) character));
            } else {
                escaped.append(character);
            }
        }
        return escaped.append('"').toString();
    }

    String toJson(List<Map<String, Object>> rows) {
        List<String> objects = new ArrayList<>();
        for (Map<String, Object> row : rows) {
            List<String> fields = new ArrayList<>();
            for (Map.Entry<String, Object> field : row.entrySet()) {
                fields.add("    " + jsonValue(field.getKey()) + ": " + jsonValue(field.getValue()));
            }
            objects.add("  {\n" + String.join(",\n", fields) + "\n  }");
        }
        return "[\n" + String.join(",\n", objects) + "\n]\n";
    }
}

boolean replaceExternalClicks = true;
String replacementUrl = "https://example.com/blocked-link";
HyperlinkAudit audit = new HyperlinkAudit();
Presentation presentation = new Presentation("hyperlink-audit-input.pptx");
try {
    List<IHyperlinkContainer> containers = audit.collectContainers(presentation);
    List<Map<String, Object>> rows = new ArrayList<>();
    for (int index = 0; index < containers.size(); index++) {
        IHyperlinkContainer container = containers.get(index);
        audit.addRow(rows, presentation, container.getHyperlinkClick(), "click", container, index + 1);
        audit.addRow(rows, presentation, container.getHyperlinkMouseOver(), "mouse-over", container, index + 1);
    }
    String json = audit.toJson(rows);
    byte[] jsonData = json.getBytes(StandardCharsets.UTF_8);
    Files.write(Paths.get("hyperlink-audit.json"), jsonData);

    for (IHyperlinkContainer container : containers) {
        IHyperlink click = container.getHyperlinkClick();
        if (audit.policyViolation(click) != null) {
            if (replaceExternalClicks && click.getActionType() == HyperlinkActionType.Hyperlink) {
                container.getHyperlinkManager().setExternalHyperlinkClick(replacementUrl);
            } else {
                container.getHyperlinkManager().removeHyperlinkClick();
            }
        }
        if (audit.policyViolation(container.getHyperlinkMouseOver()) != null) {
            container.getHyperlinkManager().removeHyperlinkMouseOver();
        }
    }
    presentation.save("hyperlink-sanitized.pptx", SaveFormat.Pptx);

    Presentation reopened = new Presentation("hyperlink-sanitized.pptx");
    try {
        List<IHyperlinkContainer> remainingContainers = audit.collectContainers(reopened);
        int violations = 0;
        for (IHyperlinkContainer container : remainingContainers) {
            if (audit.policyViolation(container.getHyperlinkClick()) != null) violations++;
            if (audit.policyViolation(container.getHyperlinkMouseOver()) != null) violations++;
        }
        System.out.println("Audit rows: " + rows.size() + "; prohibited actions after reopening: " + violations);
        if (violations != 0) {
            System.out.println("Verification failed: do not distribute the saved presentation.");
        }
    } finally {
        reopened.dispose();
    }
} catch (IOException exception) {
    System.out.println("Unable to write the audit report: " + exception.getMessage());
} finally {
    presentation.dispose();
}

使用上述输入,报告包含五行操作。文件鼠标悬停链接和宏点击被删除,HTTPS 链接和内部幻灯片导航保留。验证阶段打印零个违规操作。若输入包含被禁止的外部点击 URL,示例会演示替换分支。一个容器若同时具备允许的点击和被禁止的鼠标悬停,则保留点击操作并删除鼠标悬停。

此类有选择的清理不同于 removeAllHyperlinks,后者会在选定范围内不顾策略一律删除两种激活类型。这里的验证仅检查超链接操作,不会删除嵌入的 VBA 项目、OLE 对象或其他活动内容,也不对导出的 PDF 或 HTML 文件进行校验。

常见问题解答

如何链接到某个章节或其第一张幻灯片?

PowerPoint 中的章节用于分组幻灯片,但内部超链接只能定位到单张幻灯片。要实现跳转到章节,请链接到该章节的第一张幻灯片。

我可以将超链接附加到母版幻灯片元素,使其在所有幻灯片上生效吗?

可以。母版幻灯片和版式元素都支持超链接。使用这些元素的幻灯片在放映时会保留相应链接。

导出为 PDF、HTML、图像或视频时超链接会被保留吗?

受支持的 PDF 和 HTML 导出可能保留超链接;栅格图像和视频则不会。请参阅报告、清理和验证超链接 中的导出注意事项。