技術記事

PDFium Componentを使用してDelphiでPDFビューアを構築する

DelphiにおけるPDFビューアは、2つのコンポーネントとそれらの間の接続に帰結します。TPdf はドキュメントを所有します。ファイルを開き、復号化し、ページ数やメタデータに関する質問に答えます。TPdfView は、画面にページを描画し、スクロール、ズーム、およびユーザーが現在見ているページを処理するビジュアルコントロールです。PDFium Componentは、Chromeの内部に搭載されているのと同じレンダリングエンジンをラップしているため、キャンバス上に得られるグリフ、アンチエイリアシング、および色は、ユーザーがブラウザですでに見ているものと一致します。作業はレンダリングにあるのではありません。ドキュメントオブジェクトをビューに接続し、破損したファイルやパスワードで保護されたファイルでクラッシュすることなくロードし、ビューアを完成品と感じさせるためのいくつかのコントロール(ページをめくる、ズームを変更する、ページをウィンドウに合わせるなど)をユーザーに提供することにあります

ここでは、実際に構築する順序でその組み立てについて説明します。ここではすべてが1度に1ページずつレンダリングされます。これはほとんどのドキュメントワークフローが求めているものです。ページを1つの連続してスクロールする列に積み重ねる必要がある場合は、それは異なるレイアウトの決定であり、ここで説明するパスではありません

TPdfをTPdfViewに接続する

フォームに TPdf と TPdfView をドロップし、どのドキュメントを表示するかをビューに指示します。その単一の割り当てが、非ビジュアルなドキュメントとそれを描画するコントロールとの間のリンクのすべてです

TPdf がドキュメントを所有し、TPdfView が描画し、1 つのプロパティ代入が PDFium DLL の上で両者を結ぶ Delphi PDF ビューアーアーキテクチャ
TPdf がドキュメントを所有し、TPdfView がそれを描きます。1 回の代入が、共有の PDFium エンジンの上で両者を繋ぎます
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf と PdfView はデザイン時に配置されている。
  PdfView.Pdf := Pdf;                 // ビューはこのドキュメントが保持する内容を描画
  PdfView.FitMode := pfmFitWidth;     // ユーザーを適切なズームで開始
end;

これらのいずれかを実行する前に、PDFiumのネイティブライブラリがマシン上にある必要があります。PDFium Componentは、ターゲットプラットフォームに応じて pdfium32.dll または pdfium64.dll を呼び出します。DLLが見つからない場合、ドキュメントは単に開くことを拒否します。実行可能ファイルの横に一致するDLLを同梱するか、システムローダーが見つけることができる場所に配置してください。V8対応ビルドは、実行したいJavaScriptを保持しているPDFのためにのみ存在します。通常のビューアはそうではないため、具体的な理由がない限り、標準のDLLを使用してください

入力を信頼せずにドキュメントをロードする

直感的には、ロードを try/except でラップし、スローされた例外を失敗として扱いたくなります。しかしここでのその直感は間違っており、それを間違えると、誰かが壊れたファイルを手渡すまでは問題なく見えるビューアが生成されます。Active := True を設定しても、ロードの失敗時に例外は発生しません。PDFium Componentは内部エラーをキャッチし、Active を False のままにするため、ドキュメントが開いたかどうかを知る唯一の正直な方法は、プロパティを設定した後にプロパティを読み返すことです

Delphi PDFium ビューアーのロード分岐フロー。Active の設定は決して例外を発生させず、無音の false は誤ったパスワードか破損ファイルを意味し、1 回のパスワード再試行が続く
アクティベーションは失敗時に決して例外を投げないため、ビューアーは Active を読み返し、無言の false にパスワード再試行 1 回で応えます
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // 例外は発生しない。失敗時は Active が False のまま
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // ビューが自身の現在ページを追跡
  UpdatePageLabel;
end;

2つの点に注意が必要です。1つ目は、PageNumber が両方のオブジェクトに存在し、その2つは独立しているということです。Pdf.PageNumber はドキュメントにおける現在のページの概念です。PdfView.PageNumber はコントロールが実際に表示するページであり、ユーザーをファイル内で移動させるために設定するものです。一方を設定してももう一方は移動しないため、ビューアは常にビューのプロパティを駆動します。2つ目は、1ベースのインデックス作成です。ページは0からではなく1から Pdf.PageCount まで実行されるため、0ベースの配列に慣れている人は注意が必要です

暗号化されたファイルの処理

暗号化されたドキュメントも同じロードパスに組み込まれます。アクティベーションの前にオープンパスワードが設定されている場合、ドキュメントは開くときに復号化されます。パスワードが間違っていたり欠落している場合、破損したファイルの場合とまったく同じように Active は False のままになります。したがって、リカバリは、パスワードの入力を求めてアクティベーションを再試行することです

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // Active := True の前に設定する必要がある
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

無効なパスワードと破損したファイルの両方で失敗が静か(エラー表示なし)であるため、Active だけから2つを区別することはできません。実際には、それはビューアとして受け入れられます。ユーザーは正しいパスワードを提供するか、ファイルが開かないことを知るかのどちらかであり、メッセージはどちらの場合も同じ内容になります

ドキュメントのページング

ドキュメントが開いている状態でのナビゲーションは、Pdf.PageCount を境界とする PdfView.PageNumber での算術演算です。唯一の実際の作業はクランプ(範囲制限)です。これにより、ボタンがページを範囲外に押し出すことはなくなり、最初と最後のボタンはファイルの末尾で無効のままになります

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// 4つのナビゲーションボタンはそれぞれ1回の呼び出しに簡略化
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

「Nページに移動」のテキストボックスは、解析された整数から供給される同じ GoToPage の呼び出しであり、クランプはユーザーが10ページのファイルに9999と入力した場合をカバーします。読み取り値がビューに表示されるものと同期しなくなることが決してないように、「12ページ中3ページ」を書き込む単一の場所として UpdatePageLabel を保持します

ズーム:明示的なパーセンテージとフィットモード

TPdfView のズームには相互作用する2つの種類があり、その相互作用を理解することが、行儀の良いズームコントロールとユーザーと戦うズームコントロールの違いとなります。直接的なルートは Zoom プロパティであり、100が実際のサイズを意味するパーセンテージです。もう1つのルートは FitMode です。これは、ズームを計算し、ウィンドウのサイズが変更されたときに再計算を続けるようにビューに指示します

PDFium Delphi ビューアーにおける Zoom と FitMode の相互作用。正確な Zoom の代入が FitMode を pfmNone へクリアし、フィットモードの選択がズームをビューへ返す
正確なズームを代入すればフィットモードが消え、フィットモードを選べばズーム計算がビューへ返されます
// 固定の拡大率
PdfView.Zoom := 100;     // 実際のサイズ
PdfView.Zoom := 50;      // 半分
PdfView.Zoom := 200;     // 2倍

// ビューにページをウィンドウに合わせてサイズ変更させ、リサイズ時も維持させる
PdfView.FitMode := pfmFitWidth;   // ページ幅がコントロールいっぱい
PdfView.FitMode := pfmFitPage;    // ページ全体が表示
PdfView.FitMode := pfmActualSize; // ドキュメントのポイント単位で1:1

ここで人々がつまずく部分があります。Zoom を直接割り当てると、FitMode が pfmNone にリセットされます。これは正しい動作であり、バグではありません。ユーザーが正確に150%を選択した瞬間、2つのリクエストが競合するため、ビューはもはや「幅に合わせる」を優先することはできません。UIへの影響として、ズームインボタンとページに合わせるボタンは相互に排他的な状態になり、ツールバーはアクティブなモードを視覚的にわかるようにする必要があります。ユーザーが「ページに合わせる」をクリックした場合は FitMode を設定し、数値のズームをクリックした場合は Zoom を設定して、フィットモードを自動的にクリアさせます

現在のフィット率でズームスライダーにシードを提供するためなどにフィット値を自分で計算したい場合、ページごとのヘルパーはモードを変更せずに数値を提供します。PageWidthZoom[N]、PageZoom[N]、および ActualSizeZoom[N] は、ページNを幅に合わせる、全体に合わせる、または実際のサイズでレンダリングするパーセンテージを返します

// 現在ページの幅合わせ値からズーム表示を初期化
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

完成したビューアが実際に必要とするもの

上記のビューアは数十行ですが、ドキュメントワークフローに必要な作業(ファイルを開く、不正なファイルを生き残る、ページを表示する、ページ間を移動する、手動またはフィットで倍率を変更する)をすでに実行しています。PDFiumは難しい部分を静かに実行します。埋め込まれたフォントが解決され、注釈とフォームフィールドがドキュメントで配置された場所に描画されます。また、表示されるページはChromeユーザーが見るものと一致します。どちらも同じエンジンが描画しているためです

このベースから、追加は構造的というよりも漸進的です。テキストの選択と検索は、PDFiumがすでに構築しているのと同じテキストレイヤーから読み取ります。Pdf.Title や Pdf.Author などのメタデータは、プロパティを1つ読み取るだけで済みます。回転とグレースケールは、ページをビットマップに描画するときに渡すレンダリングオプションです。これらのどれも、ドキュメントオブジェクト、ビュー、およびそれらを接続するロードしてナビゲートするフローである、ここにある根幹を変更するものではありません。その根幹を正しく設定すれば、残りは装飾です

全体で使用されている TPdf および TPdfView コンポーネントは、DelphiおよびC++Builder用の PDFium Component の一部であり、製品ページに完全なビューアのリファレンスが掲載されています