管理 Java 中的簡報超連結

簡介

超連結將投影片內容連接到網站或投影片內的某個位置。在 PowerPoint 中,超連結通常有兩種用途:

  • 從文字、圖形或媒體框架開啟網站。
  • 從目錄等處導向另一張投影片。

Aspose.Slides for Java 讓您可以新增這些連結、控制其外觀與音效、更新屬性,並移除連結。以下範例示範如何在個別元素上操作超連結,以及如何在投影片、投影片或文字框層級存取超連結。

新增 URL 超連結

您可以將網站 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,將第一張投影片上的「第 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 框架集中要導向的目標框架(如適用)。
  • 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();
}

在此範例中,投影片與投影片查詢各回傳三個點擊容器、兩個滑鼠懸停容器,以及三個任一動作的容器。文字框查詢在每個類別各回傳一個容器。

分類動作與目的地

使用 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 匯出可能會保留超連結;光柵化的圖片與影片則不會。請參閱 報告、清理與驗證超連結 中的匯出考量。