Java でプレゼンテーション ハイパーリンクを管理する

はじめに

ハイパーリンクはプレゼンテーションのコンテンツを Web サイトまたはプレゼンテーション内の場所に接続します。PowerPoint では、ハイパーリンクは一般的に次の 2 つの目的に使用されます。

  • テキスト、図形、またはメディア フレームから Web サイトを開く。
  • たとえば目次から別のスライドへ移動する。

Aspose.Slides for Java を使用すると、これらのリンクを追加し、外観やサウンドを制御し、プロパティを更新し、削除できます。以下の例では、個々の要素のハイパーリンクを操作する方法と、プレゼンテーション、スライド、テキスト フレームレベルでハイパーリンクにアクセスする方法を示します。

URL ハイパーリンクの追加

テキスト、図形、またはメディア フレームに Web サイトの URL を割り当てることができます。ハイパーリンクを割り当てた要素がクリック可能領域を決定します。テキストの一部に割り当てた場合は選択したテキストがリンクになり、図形やフレームに割り当てた場合はスライドオブジェクトがリンクになります。

相対ハイパーリンクを含むプレゼンテーションについては、ハイパーリンク ベースの管理 を参照し、ドキュメント レベルのベースを読み取り、変更、またはクリアしてください。

テキストへの URL ハイパーリンクの追加

テキストを Web サイトにリンクするには、以下に示すように 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」テキストを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 で導入され、古いバージョンではこの設定は適用されません。

以下の例は、同じスライドに 2 つのテキストハイパーリンクを追加します。1 つ目は赤いテキスト塗りつぶしを使用し、2 つ目はデフォルトのハイパーリンク色を保持します。

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

サウンド

ハイパーリンクは、アクティブ化されたときにサウンドを再生したり、再生中のサウンドを停止したりできます。以下のメソッドを使用してこれらの動作を設定します。

  • IHyperlink.setSound はハイパーリンクに関連付けられたオーディオを指定します。
  • IHyperlink.setStopSoundOnClick はハイパーリンクをアクティブ化したときに前のサウンドを停止するかどうかを制御します。

ハイパーリンク サウンドの追加

以下の例は sampleaudio.wav をロードし、最初のスライドのボタンに関連付けます。ボタンをクリックするとサウンドが再生され、次のスライドへ移動します。同じスライドの 2 つ目の図形はクリック時に前のサウンドを停止し、ナビゲーションは行いません。

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 を使用して、テキスト部分のリンクを含むハイパーリンクコンテナを変更前に収集します。以下の例は、最初のスライドから両方のアクティベーションタイプを削除します。1 つだけ削除したい場合は、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 が選択したスコープですべてのアクティベーションタイプを一度に削除します。マスター、レイアウト、ノートの選択的クリーンアップとカバレッジについては、Report, Sanitize, and Verify Hyperlinks を参照してください。

ハイパーリンクインベントリの作成

プレゼンテーションを配布する前に、インタラクティブなアクションと Web リンクをインベントリ化します。getAnyHyperlinks は URL 文字列のフラットなリストではなく、IHyperlinkContainer オブジェクトを返します。各コンテナで getHyperlinkClick と getHyperlinkMouseOver の両方を調べます。これらは独立しており、同じコンテナが両方のアクションを持つことがあるため、完全なレポートにはコンテナごとに最大 2 行が必要です。

図形レベルのハイパーリンクだけをスキャンすると、テキスト部分に付随するリンクが見逃される可能性があります。代わりに適切なスコープをクエリし、返されたコンテナを保持して後でアクションを更新または削除できるようにしてください。

プレゼンテーション、スライド、テキスト フレームのスコープをクエリする

IHyperlinkQueries インターフェイスは、IPresentation.getHyperlinkQueries、IBaseSlide.getHyperlinkQueries および ITextFrame.getHyperlinkQueries を通じて利用できます。各スコープは同じクエリをサポートします。

  • getHyperlinkClicks はクリックアクションを持つコンテナを返します。
  • getHyperlinkMouseOvers はマウスオーバーアクションを持つコンテナを返します。
  • getAnyHyperlinks はいずれか、または両方のアクションを持つコンテナを返します。

以下の例は、外部クリックリンク、ファイルのマウスオーバーリンク、内部スライドナビゲーション、テキストのマウスオーバーリンク、マクロアクションを含む hyperlink-audit-input.pptx を作成します。これらのアクションは実行されません。同じ 3 つのクエリはすべてのスコープで機能し、カウントはコンテナ数を示し、アクション総数ではありません。テキストフレームのスコープは、囲む図形自身のリンクは除外します。

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 の値は Web ナビゲーションだけでなく、以下を含みます:

値 監査時の意味
Hyperlink 外部ハイパーリンク; URL とそのスキームを検査します。
JumpSpecificSlide 特定のスライドへの内部ナビゲーションです。
JumpFirstSlide, JumpPreviousSlide, JumpNextSlide, JumpLastSlide, JumpLastViewedSlide スライドショーコンテキストで解決される組み込みスライドショーナビゲーションです。
JumpEndShow, StartCustomSlideShow 現在のショーを終了するか、カスタムショーを開始します。
StartMacro マクロを実行します。
StartProgram プログラムを起動します。
OpenFile, OpenPresentation ファイルまたは別のプレゼンテーションを開きます。Web URL とは別にレビューしてください。
StartStopMedia メディアの再生を開始または停止します。
NoAction, Unknown ナビゲーションアクションがない、または認識できないアクションで、レビューが必要です。

getExternalUrl から外部宛先を読み取り、getTargetSlide から特定の内部宛先を取得します。内部アクションや組み込みコマンドには外部 URL がない場合があります。空の URL がコンテナにアクションがないことを意味するわけではありません。正規化された URL と異なる場合は getExternalUrlOriginal が返す値を保持し、利用可能な場合は getTooltip が返すツールチップも含めてください。

ハイパーリンクのレポート、サニタイズ、検証

以下の Java の例は、既存のプレゼンテーションを読み取り(上記で作成したファイルを使用)、hyperlink-audit.json を書き出し、ポリシーを適用し、hyperlink-sanitized.pptx を保存し、再度開いて両方のアクティベーションタイプをチェックします。変更前にコンテナを収集し、同一コンテナの二重処理を回避するために参照等価性を使用します。プレゼンテーションのクエリは通常のスライドを対象とし、パッケージ全体のインベントリの場合は、マスター、レイアウト、ノート、および存在する場合はノートマスターとハンドアウトマスターも明示的にクエリします。

レポートは、利用可能な場合に 1 から始まるスライドインデックスと getSlideId を記録します。ISlideComponent.getSlide は対応するコンテナの所有スライドを提供します。マスター、レイアウト、ノートは通常のスライドインデックスを持たず、スコープで識別されます。図形コンテナとテキスト部分の書式コンテナは別々にラベル付けされ、他のコンテナタイプはランタイムの型名を保持します。各コンテナにはレポート内でローカル ID が付与され、2 つのアクションを相関付けられます。レポートは、アクションタイプを Java 列挙体で定義された整数定数として保存します。

この意図的に制限的なアプリケーション ポリシーは、絶対 HTTPS URL と有効な内部スライドターゲットのみを許可します。マクロ、プログラム、ファイルアクション、その他のスライドショーアクション、未知のアクション、およびその他の URL スキームは拒否されます。これらの拒否はポリシー上の決定であり、Aspose.Slides の安全性判定ではありません。HTTPS だけでは信頼性を保証できないため、ホストの許可リストやその他のチェックをアプリケーションに追加してください。元の URL と正規化された外部 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();
}

上記で作成した入力を使用すると、レポートには 5 行のアクションが含まれます。ファイルのマウスオーバーリンクとマクロのクリックは削除され、HTTPS リンクと内部スライドナビゲーションは残ります。検証では禁止アクションは 0 件と出力されます。禁止された外部クリック URL を含む入力は、置換分岐も実行します。許可されたクリックと禁止されたマウスオーバーを持つコンテナは、クリックアクションを保持します。

この選択的クリーンアップは、ポリシーに関係なく選択されたスコープ全体の両方のアクティベーションタイプを削除する removeAllHyperlinks とは異なります。ここでの検証はハイパーリンクのアクションのみをチェックし、埋め込まれた VBA プロジェクト、OLE オブジェクト、その他のアクティブ コンテンツは削除せず、エクスポートされた PDF や HTML ファイルの検証も行いません。

よくある質問

セクションまたはその最初のスライドにリンクするにはどうすればよいですか?

PowerPoint のセクションはスライドをグループ化しますが、内部ハイパーリンクは個々のスライドを対象とします。セクションへのナビゲーションを作成するには、そのセクションの最初のスライドにリンクしてください。

マスタースライドの要素にハイパーリンクを付けて、すべてのスライドで機能させることはできますか?

はい。マスタースライドやレイアウトの要素はハイパーリンクをサポートします。これらの要素に付けたリンクは、対応するマスターまたはレイアウトを使用するスライドショー中に利用可能です。

PDF、HTML、画像、またはビデオにエクスポートした際にハイパーリンクは保持されますか?

サポートされている PDF および HTML のエクスポートはハイパーリンクを保持する場合がありますが、ラスタ画像やビデオは保持できません。エクスポートに関する考慮事項は Report, Sanitize, and Verify Hyperlinks を参照してください。