上流工程から流れてきた PDF のフォルダを引き継ぎ、依頼は一見単純です。どのブックマークが外部 URL へ飛び、どれが JavaScript を実行し、内部移動のものは実際にどこへ着地するのかを教えてほしい。ところが API リファレンスを開くと、ライブラリはそれらのアクションをすべて作成できるのに、読み戻す手段は何も用意していないと分かります。この非対称性は PDF ツール全般にあります。開くブックマークを書き込むのは https://example.comhttps://example.com/A/S/S/URI/Dest/D
TPdfActionIDTPDFObject だけを返し、あとは自分で潜るしかありませんでした。v3.77.0 では、小さな型付きイントロスペクション API 群によってその一部が埋まり、アクション種別、アクションのペイロード、到達先のジオメトリを素のレコードとして返せるようになりました。この記事では、それらの呼び出しが ISO 32000-1 のアクションおよび destination モデルへどう対応しているか、そして手書き実装が静かに間違えやすい 3 つの具体的な落とし穴を扱います
アクションを読むことが書くことより難しい理由
PDF におけるアクションは、サブタイプを示す /S/SGoToGoToGoToRURIURILaunchLaunchJavaScriptNamedGoToRJavaScriptNamedURIURI/URI/URIGoToRLaunchLaunchGoToR/F/FJavaScriptJavaScript/JS/JSGoToGoTo/D/D
読む側では、まず /S/SGetOutlineActionInfoGetOutlineActionInfoGetAnnotActionInfoGetAnnotationActionInfoTPDFlibActionInfoTPdfActionInfo
type
TPDFlibActionKind = (akNone, akGoTo, akGoToR, akURI,
akLaunch, akNamed, akJavaScript);
TPDFlibActionInfo = record
Kind: TPDFlibActionKind;
URI: AnsiString; // populated for akURI
JavaScript: WideString; // populated for akJavaScript
FileName: AnsiString; // populated for akGoToR / akLaunch
OpenInNewWindow: Boolean; // akGoToR / akLaunch
end;
レコードを返します。KindKindKindKindakURIpakURIURIUriakGoTopakGoToakNone のときは、どのペイロードフィールドも意味を持たず、さらに下で扱う destination 側へ進みます
ブックマークを見つけるためにアウトラインツリーをたどる
ブックマークを解析するには、まずそのハンドルが必要です。PDFlibPas はアウトラインノードを整数 ID で識別し、FindOutlineByTitleFindOutlineByText
type
TPDFlibOutlineSearchDepth =
(osdSiblingsOnly, osdChildrenOnly, osdFullSubTree);
function FindOutlineByTitle(const Title: WideString;
StartOutlineID: Integer;
Depth: TPDFlibOutlineSearchDepth): Integer;
が表示テキストで検索します。どこまで探索するかは明示的に制御できます。DepthSearchModeosdSiblingsOnlyosmSiblingsosdChildrenOnlyosmChildrenosdFullSubTreeosmRecursiveGetFirstOutline0
var
Lib: TPDFlib;
FoundID: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') = 1 then
begin
// Search the whole tree from the root for a nested bookmark.
FoundID := Lib.FindOutlineByTitle('Appendix B',
Lib.GetFirstOutline, osdFullSubTree);
if FoundID <> 0 then
// FoundID is now a handle you can pass to the action and
// destination getters below.
;
end;
finally
Lib.Free;
end;
end;
UnicodeStringWideString として比較されるため、大文字小文字を区別し、保存されている Unicode テキストをそのまま尊重します。生成元がばらばらの PDF を相手にするなら、検索語も文書に保存されたのと同じ形に正規化しておかないと、存在しない取りこぼしを追いかけることになります
ブックマークのアクションとターゲットを解決する
ハンドルを得たら、GetOutlineActionInfoGetOutlineActionInfoKind を呼んで型付きビューを得ます。パターンは単純で、
var
Info: TPDFlibActionInfo;
begin
Info := Lib.GetOutlineActionInfo(FoundID);
case Info.Kind of
akURI:
Writeln('Opens URL: ', Info.URI);
akGoToR, akLaunch:
Writeln('Opens file: ', Info.FileName,
' (new window: ', Info.OpenInNewWindow, ')');
akJavaScript:
Writeln('Runs script: ', string(Info.JavaScript));
akGoTo:
Writeln('Jumps within this document'); // see destination below
akNamed:
Writeln('Named action (NextPage, Print, etc.)');
akNone:
Writeln('Bookmark has no action');
end;
end;
KindGetActionURLGetActionFileNameURIURIGetActionURLGetActionFileName/F/FGoToRLaunchLaunchGoToRURIURIURIURI/URI/URIURIURI/URI/URIakURIURIakGoToRLaunchakLaunchGoToR
destination の fit 種別と、その背後にあるジオメトリ
GoToakGoTo というアクションは「この文書内で移動する」を意味しますが、どこへ なのか、どのように 表示するのかまでは語りません。それを担うのが destination であり、人が思うより多くのニュアンスを持っています。PDF の destination は単なるページ番号ではなく、ページに加えて、そのページをビューアがどうフレーミングすべきかを表す fit 指定です(ISO 32000-1 §12.3.2.2)。GetOutlineDestinationInfoGetOutlineDestinationInfo
type
TPDFlibDestinationKind = (dkNone, dkXYZ, dkFit, dkFitH,
dkFitV, dkFitR, dkFitB, dkFitBH, dkFitBV);
TPDFlibDestinationInfo = record
Kind: TPDFlibDestinationKind;
Page: Integer; // 1-based; 0 when unresolved
Left, Top, Right, Bottom, Zoom: Double;
end;
8 種類の fit は、それぞれ違うフレーミングの問いに答えます。dkXYZpdfdXYZLeftLeftTopTopZoomZoomdkFitpdfdFitdkFitHpdfdFitHdkFitVpdfdFitVdkFitRpdfdFitRdkFitB*pdfdFitB

実装の内部では、信頼性の理由を説明してくれる意図的な整列が使われています。内部関数 GetDestTypeGetDestinationFitTypeTPDFlibDestinationKind は 8 種類の fit に対して XYZ/Fit/FitH/FitV/FitR/FitB/FitBH/FitBV の順でちょうど 1..8 の整数を返します。dkXYZpdfdkXYZdkFitBVpdfdkFitBVdkNonepdfdkUnknown
var
Dest: TPDFlibDestinationInfo;
begin
Dest := Lib.GetOutlineDestinationInfo(FoundID);
if Dest.Page = 0 then
Exit; // destination did not resolve
case Dest.Kind of
dkXYZ:
Writeln(Format('Page %d at (%.0f, %.0f), zoom %.2f',
[Dest.Page, Dest.Left, Dest.Top, Dest.Zoom]));
dkFitR:
Writeln(Format('Page %d, rect L%.0f T%.0f R%.0f B%.0f',
[Dest.Page, Dest.Left, Dest.Top, Dest.Right, Dest.Bottom]));
dkFit, dkFitB:
Writeln(Format('Page %d, fit whole page', [Dest.Page]));
else
Writeln(Format('Page %d, fit kind %d',
[Dest.Page, Ord(Dest.Kind)]));
end;
end;
PageIndexPage が 0 のときは destination が解決できなかった合図であり、通常はアクションが destination を持たないか、名前付き destination が見つからなかったことを意味します。座標を信用する前にここを確認してください。また GetOutlineDestinationInfoGetOutlineDestinationInfo/Dest/DestGoToGoTo/D/D
注釈アクションと SelectPage の落とし穴
リンク注釈はブックマークと同じようにアクションを持ち、GetAnnotActionInfoGetAnnotationActionInfoTPDFlibActionInfoTPdfActionInfo
を返します。種別を見てからペイロードを読むという流れも同じです。ただし、アウトラインにはない状態依存の落とし穴が 1 つあり、これが 3 つ目の罠です。GetAnnotActionInfoGetAnnotationActionInfoSelectPage(N)SelectPageakNonepakNone
var
P: Integer;
Info: TPDFlibActionInfo;
begin
for P := 1 to Lib.PageCount do
begin
Lib.SelectPage(P); // mandatory before touching annotations
// GetAnnotActionID(1) <> 0 is the reliable "has an action"
// test. CheckPageAnnots returns a boolean-style flag, not a
// count, so it is the weaker signal here.
if Lib.GetAnnotActionID(1) <> 0 then
begin
Info := Lib.GetAnnotActionInfo(1);
if Info.Kind = akURI then
Writeln(Format('Page %d link -> %s', [P, Info.URI]));
end;
end;
end;
このループ内で意図的にしていることは 2 つあります。1 つ目は、SelectPage(P)SelectPageGetAnnotActionID(1) <> 0GetAnnotationActionID(0) <> 0CheckPageAnnotsHasAnnotationsJavaScriptJavaScript/JS/JS
読み取り側イントロスペクションの位置づけ
これらの getter は意図的に狭い責務に留められています。既存の整数ハンドルベースのアクション層と destination 層の上に積み上がる純粋な読み取り API であり、書き込み経路には触れず、同時に編集中の文書へ追加のリスクも持ち込みません。報告するのはファイル内にある内容だけで、ポリシー検証や書き換えは行いません。逆に、こうしたアクションを持つブックマークやリンク注釈を最初から構築したいなら、それは書き込み側の仕事です。関連する記事 Delphi でインタラクティブフォームのアクションと JavaScript を扱う では作成方法を順に説明しています。ナビゲーショングラフではなく、PDF から可視コンテンツや構造コンテンツを引き出したいなら、PDFlibPas でテキスト、画像、フォントを抽出する を参照してください
覚えておくべき正直な境界はこれです。イントロスペクションが見えるのは、生成側が実際に書いたものだけです。ジェネレータが不正なまま残したブックマークアクションや、定義されていない名前付きターゲットを指す destination は、例外ではなく akNonepakNonePDFlibPas のネイティブ PDF ライブラリの一部です