技術記事

TPDFlibViewerを使用したDelphiでのインタラクティブPDFビューアの構築

Delphiの業務アプリケーションにPDF閲覧パネルを組み込む際、レンダリングループ、スクロールエンジン、ズームモデルなどを独自に開発する必要はありません。losLab PDF Library は、1回の呼び出しでドキュメントを開き、テキスト選択、パン(手のひらツール)、ドラッグによるズーム、スナップショット、領域ハイライト、全文検索マッチングのハイライト、アノテーション処理、および印刷を標準動作として提供するVCLコントロール TPDFlibViewer を同梱しています。この記事では、空のフォームから動作するビューアを構築する5つのステップ(コントロールの配置、定義済みアクションの紐付け、検索ハイライトの追加、アノテーションクリックの処理、および印刷)について順を追って説明します。

レンダリングループを記述する代わりにビューアコントロールを使用する理由

まず、レイヤー(層構造化)の設計思想について説明します。これにより、何を購入しているかが明確になります。ページをビットマップにレンダリングする処理は、全体のわずか20%に過ぎず、残りの80%はその周囲を囲む多くの機能(破棄バジェットを備えたページビットマップキャッシュ、異なるサイズのページをまたぐスクロールレイアウト、サイズ変更時に再計算されるズームモード、任意のズームや回転でクライアント上のピクセルをページの座標にマッピングするヒットテスト、チラつきを防ぐ再描画スケジュールなど)で占められています。TPDFlibViewer はこれらすべての処理を引き受け、実際の描画処理はマルチエンジンレンダリングに関する記事で説明されているのと同じエンジンに委譲します。これにより、コントロールレイヤーとレンダリングレイヤーをそれぞれ独立して変更できます。フォーム側のコードは、レンダラーと直接通信するのではなく、スクロール可能な通常のWindowsコントロールとして扱います。

DelphiアプリケーションにPDFビューアを組み込む方法

TPDFlibViewer.LoadFromFile は、コントロールの内部にある TPDFlib インスタンスを介してドキュメントを開き、成功時に1を返します。したがって、最小限のビューアは、コンストラクタの呼び出し、Parent プロパティの割り当て、および1回のロード処理だけで構築できます。アプリケーションの別の場所ですでに TPDFlib インスタンスを保持している場合は、AttachLibrary を呼び出して指定されたドキュメントを表示できます。この場合、インスタンスの所有権はアプリケーション側に残ります。

uses PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  if FViewer.LoadFromFile('quarterly-report.pdf', '') = 1 then
    FViewer.ViewerMode := vmHand;  // pan by dragging; a plain click still follows links
end;

ViewerMode プロパティは、マウスが実行する動作を5つの値から選択します。vmSelect はデフォルトのテキスト選択ツールです。vmHand は手のひらツールで、グラブカーソルでドラッグしてパン操作を行います(プレーンクリックした場合はリンクの追跡が維持されます)。vmZoom はドラッグした矩形範囲を拡大してビューポート全体に表示します。vmSnapshot はドラッグした範囲の画像をクリップボードにキャプチャします(OnSnapshot イベントと SnapshotToClipboard 切り替えを使用)。vmHighlight は半透明の領域ハイライトを描画し続けます。ハイライトツールについては、座標に関して1点注目すべき点があります。領域はクライアント上のピクセルではなく、ページのユーザー空間に保存されるため、ビューポートのズーム、回転、ページ移動を行っても、座標計算を自分で行うことなくハイライト表示が元の位置に維持されます。回転された表示状態においてドラッグ方向がページ軸と逆になっている場合でも、逆回転マッピングの後に負の幅や高さが算出されますが、コントロールがその矩形を自動的に正規化して処理します。

Delphiでグルーコードを書かずにPDFツールバーを紐付ける方法

PDFlibViewerActions ユニットを使用すれば、ツールバーを繋ぐグルーコード(仲介コード)は不要になります。このユニットは、ページ移動(TPDFlibViewerFirstPageTPDFlibViewerPriorPageTPDFlibViewerNextPageTPDFlibViewerLastPage)、ズーム(TPDFlibViewerZoomInTPDFlibViewerZoomOutTPDFlibViewerFitWidthTPDFlibViewerFitPageTPDFlibViewerActualSize)、表示回転(TPDFlibViewerRotateClockwiseTPDFlibViewerRotateAntiClockwise)、印刷、ファイルを開く・名前を付けて保存ダイアログ、選択テキストのコピー、およびマウスツールごとの切り替えアクション(TPDFlibViewerSelectModeTPDFlibViewerHandModeTPDFlibViewerMarqueeZoomModeTPDFlibViewerSnapshotModeTPDFlibViewerHighlightMode)を含む、20個の定義済み TAction 子クラスを提供します。各アクションは、公開されている Viewer プロパティを介してコントロールを指し、自身の状態を管理します。ドキュメントが開かれていないときはすべてのアクションが自動的に無効化され、移動アクションはページの現在位置を追跡し、コピーアクションは選択範囲が存在するかを監視し、モードやズーム幅の合わせ方(Fit)のアクションはアクティブなツールやズーム状態と同期して checked 状態を維持します。設計時には、アクションリストエディタの「losLab PDF」カテゴリに登録され、実行時のコードはそれぞれわずか3行です。

uses System.Actions, Vcl.ActnList, PDFlibViewer, PDFlibViewerActions;

procedure TMainForm.BuildToolbar;
var
  NextPage: TPDFlibViewerNextPage;
  FitWidth: TPDFlibViewerFitWidth;
  HandMode: TPDFlibViewerHandMode;
begin
  NextPage := TPDFlibViewerNextPage.Create(Self);
  NextPage.ActionList := ActionList1;
  NextPage.Viewer := FViewer;
  btnNext.Action := NextPage;

  FitWidth := TPDFlibViewerFitWidth.Create(Self);
  FitWidth.ActionList := ActionList1;
  FitWidth.Viewer := FViewer;
  btnFitWidth.Action := FitWidth;

  HandMode := TPDFlibViewerHandMode.Create(Self);
  HandMode.ActionList := ActionList1;
  HandMode.Viewer := FViewer;
  btnHand.Action := HandMode;
end;

このパターンの上に独自のアクション登録を構築する場合、ユニットスコープに関する罠が1つあります。RegisterActionsVcl.ActnList ではなく System.Actions で宣言されており、uses 句に Vcl.ActnList のみしか記述していない場合にコンパイラが返すエラーは非常に紛らわしいものです。クラスの配列リテラル [TAction1, TAction2, ...] が集合(Set)として解析され、アクションクラスの型不一致を示す E2010 Incompatible types: 'Integer' and 'class of ...' エラーが発生します。解決策は uses 句に 1 行追加するだけですが、エラー診断はそれ以外のあらゆる箇所を指し示します。ズームツールバーについては、TPDFlibViewer.EffectiveZoomPercent が実際に適用されているズーム率(幅合わせやページ合わせモードで算出された倍率を含む)を報告するため、前回の設定値ではなく、実際の現在の倍率に基づいてズームイン等の処理を実行できます。

検索マッチングのハイライト表示と、その背後にある座標系の反転

HighlightSearchHits は、ドキュメント全体からクエリ文字列を検索し、一致するすべての箇所に半透明のバーを描画して、一致数を返します。また、ClearSearchHighlightsSearchHighlightCount、および SearchHighlightColor プロパティが提供されています。一致した箇所の矩形はページ空間に保存され、描画のたびに再投影されるため、ユーザーがスクロール、ズーム、回転を行っても、バーはテキストと正しく重なった状態を維持します。同梱されている TPDFlibSearchPanel を併用する場合、その RunSearch は、デフォルトで有効になっている HighlightInView プロパティを介して、紐付けられたビューア上に同じハイライトバーを自動的に描画します。

procedure TMainForm.btnSearchClick(Sender: TObject);
var
  Hits: Integer;
begin
  FViewer.SearchHighlightColor := clYellow;
  Hits := FViewer.HighlightSearchHits(edtQuery.Text, False);  // case-insensitive
  StatusBar1.SimpleText := Format('%d matches', [Hits]);
end;

独自の検索結果からハイライトを描画するよりも標準機能を使用すべきである理由は、座標系の変換にあります。テキストエンジンが報告する一致箇所の矩形は、ライブラリの Origin(原点)設定に依存します。Origin 0 または 3 では、ネイティブPDFのユーザー空間と同様に Y 軸は下から上に向かって測定され、Origin 2 または 3 では X 軸は右端から測定されます。ビューアのオーバーレイは、上から下、左から測定するページ空間で動作するため、一致した矩形を保存する前に反転処理(Y = PageHeight - Bottom、必要に応じて X = PageWidth - Right)を実行する必要があります。デフォルトの原点設定のままで報告された上の値をそのまま使用すると、ページ上の上下反転した位置にハイライトが描画されてしまいます。TPDFlibViewer は、一致箇所を記録する際に現在のアクティブな原点設定に従ってこの変換を実行し、描画時に回転マッピングを適用するため、どのような角度でも正しくバーが描画されます。また、VCLキャンバスにはネイティブなアルファ合成機能がないため、半透明の処理は一定のアルファ値を持つ Windows AlphaBlend API を使用しています。基礎となるテキスト検索と図形APIの詳細については、テキスト検索とページ要素の列挙に関する記事を参照してください。

アノテーションのクリック、ヒント、および添付ファイルのエクスポート

TPDFlibViewer は、アノテーションをファーストクラスのインタラクティブオブジェクトとして扱います。リンク以外のアノテーションをクリックすると、ページ番号、1から始まるアノテーションインデックス、およびサブタイプ文字列を伴って OnAnnotClick イベントが発生します。AnnotAtPagePoint は同じヒットテストをプログラムから実行し、ポップアップ(Popup)関連のアノテーションを自動的にスキップするため、ユーザーが実際に目にするアノテーションのみを取得できます。アノテーションの上にカーソルを置くと、デフォルトで有効になっている AnnotHints 切り替えと通常の VCL ShowHint プロパティとの連携により、その Contents(内容)テキストがコントロールのヒント(ツールチップ)として表示されます。添付ファイル(FileAttachment)アノテーションについては、SaveAnnotAttachmentToFile が埋め込まれたファイルをディスクに書き出して成功時に1を返すため、数行のコードで「クリックして保存」するワークフローを構築できます。

procedure TMainForm.ViewerAnnotClick(Sender: TObject; APage, AnnotIndex: Integer;
  const Subtype: WideString);
begin
  if Subtype = 'FileAttachment' then
    if FViewer.SaveAnnotAttachmentToFile(APage, AnnotIndex,
        'C:\Temp\attached-invoice.xml') = 1 then
      StatusBar1.SimpleText := 'Attachment saved';
end;

印刷機能と、あらかじめ考慮すべき制限事項

印刷の統合は非常に簡単です。PrintDoc は、プリンタの選択、ページ範囲、部数、丁合いを指定する標準的な Windows 印刷ダイアログを表示し、処理をライブラリの印刷エンジンに渡します。PrintDoc(False) はダイアログを表示せずにすべてのページをデフォルトのプリンタに即座に送信します。PrintDocRange は、明示的なプリンタ名、開始・終了ページ、部数、丁合いフラグを指定して完全にプログラムから制御できるため、同じビューアインスタンスからバッチ処理やサーバー起動による印刷ジョブを実行する場合に最適です。

あらかじめ考慮すべき制限事項が2点あります。第一に、TPDFlibViewer は Windows VCL コントロールです。ビューアレイヤーは NOVCL ビルドからは除外されるため、コンソールサービスや VCL 以外のプラットフォームでは、基礎となる TPDFlib レンダリングAPIを直接使用してください。第二に、ヘッドレス環境での使用はサポートされていますが制限があります。CaptureRegion は指定されたクライアント領域(ページおよびハイライト)を呼び出し元所有 of ビットマップにレンダリングし、ウィンドウハンドルのない(Parent が割り当てられていない)コントロール上でも動作します。これは描画パスがハンドル作成を強制する VCL 呼び出しを回避しているためです。これにより自動テストでのピクセルレベルの検証が実用的になりますが、マウスツールなどのウィンドウメッセージループを必要とする機能を使用する場合は、依然として実際のUIセッションが必要です。パフォーマンス面については、コントロールは SetCacheLimit を介して設定可能な容量制限付きのメモリページキャッシュを保持し、セッション間でレンダリング済みページを維持することができます。これについては、ビューアのディスクページキャッシュに関する記事で、マルチモニターDPI動作を含めて詳しく解説されています。

まとめると、コントロールを配置して LoadFromFile を呼び出し、PDFlibViewerActions から定義済みアクションを繋ぎ、検索ハイライトには HighlightSearchHits を呼び出し、アノテーション処理には OnAnnotClick を実装し、印刷には PrintDoc を提供する、という5つのステップです。ここで示した機能はすべて、ビューアコントロール、アクションクラス、および描画エンジンが一緒にインストールされる、Delphi向けの標準のlosLab PDF Libraryに含まれています。