PDFium Componentは、TPdf.CreateAnnotationを介して、テキストマークアップ注釈(ハイライト、下線、取り消し線、波線)を作成します。TPdfAnnotationレコードのHasAttachmentPoints := Trueを設定し、その四角形領域AttachmentPointsにデータを格納すると、コンポーネントはISO 32000-1 §12.5.6.10で定義されているQuadPointsエントリを書き込みます。これがAPI全体の機能です。本記事を作成した理由は、その裏で動作する生のPDFium呼び出しチェーンで、極めて不親切なエラー挙動が発生するためです。具体的には、新規作成された注釈に対してFPDFAnnot_SetAttachmentPointsを実行すると、エラーコードやヒントも出力されずに毎回Falseが返されます。この記事は、同じ構造を逆方向に辿る既存の注釈の読み取りと確認に関する記事の、作成側の姉妹編に当たります
デバッグ時の現象は常に同じです。ハイライト注釈を作成し、インデックス0を指定してアタッチメントポイント設定関数を呼び出すと、関数はFalseを返し、開発者は指定した座標データを疑い始めます。ポイントの順序を入れ替えたり、Y軸を反転させたり、ページ空間からデバイス空間へ変換してみたりしますが、どれも解決には至りません。なぜなら、座標値自体が原因ではないからです。本当の問題はC APIのインデックス設計にあり、仕組みを理解すれば修正はわずか2行で完了します
ISO 32000-1におけるQuadPointsの意味
QuadPointsは、n個の四角形を記述する8×n個の数値の配列であり、ISO 32000-1 §12.5.6.10により、すべてのテキストマークアップ注釈で必須とされています。各四角形は、ハイライト、下線、取り消し線が適用される単語または連続する単語のグループをマークします。注釈のRectエントリも依然として存在しますが、マークアップのサブタイプでは領域を囲む境界線としてのみ機能し、レンダラーが実際に描画するのはQuadPointsで指定された四角形領域です。テキストが回転または変形している可能性があるため、矩形(長方形)ではなく四角形が用いられ、4つの角が4つの独立した点(x1 y1 x2 y2 x3 y3 x4 y4)として保存されます
これら4つの点の並び順について、仕様書と実際の実装との間に乖離があります。仕様書では点を反時計回りに結ぶと記述されていますが、Adobeのレンダラーは伝統的にZパターンの順序、すなわち、最初に上端を左から右へ、次に下端を左から右へ結ぶように解釈してきました。すべての開発者がAcrobatを基準にテストを行ってきたため、PDFiumを含むほぼすべてのレンダラーがこのZパターンを踏襲しています。仕様書の文字通りの順序に従って書き込まれたファイルは、一部のビューアで歪んだハイライトとしてレンダリングされてしまいます。PDFiumのFS_QUADPOINTSF構造体は、まさにこの慣例をエンコードしており、Y軸が上方向に向かって増加するページ座標において、(x1,y1)が左上、(x2,y2)が右上、(x3,y3)が左下、(x4,y4)が右下を示します。この順序に従うだけで問題は回避されます。レンダラーの多くは仕様の曖昧さに寛容ですが、頂点順序の誤りには寛容ではありません
なぜFPDFAnnot_SetAttachmentPointsはFalseを返すのか?
FPDFAnnot_SetAttachmentPointsが新しい注釈に対して失敗するのは、この関数の機能が、指定されたインデックスにある四角形データを置換(更新)することだからです。新規作成されたばかりの注釈には、置換対象となる四角形がまだ1つも登録されていません。この関数は注釈のハンドル、quad_index、および頂点座標を受け取ります。インデックス0は、「必要に応じて作成する最初のスロット」という意味ではなく、「すでに存在している0番目の四角形」を指すため、FPDFAnnot_CountAttachmentPointsが0を返す状態では該当データがなくFalseが返されます。スロットを新規作成するための関数はFPDFAnnot_AppendAttachmentPointsです。FPDFPage_CreateAnnotで作成された注釈の初期の四角形数は常に0であるため、作成時は最初にAppendを呼び出す必要があり、その後の更新時のみSetを呼び出すことができます
この挙動は、PDFium Component自体にも影響を及ぼしていました。v1.79.0までのCreateAnnotationおよびSetAnnotationで共有されていた内部ルーチンでは、FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...)がハードコーディングされていました。これは既存の注釈を更新する処理としては正しかったのですが、新しい注釈に対しては確実に失敗し、「Cannot set attachment points」というメッセージを伴うEPdfException例外を発生させていました。v1.79.1で提供された修正プログラムでは、現在の四角形数に応じて処理を分岐させています
// Inside the component's annotation writer (v1.79.1+):
// a new annotation has no quad slots yet, so Append creates
// the first one; Set only replaces a slot that already exists
if FPDFAnnot_CountAttachmentPoints(Annotation) = 0 then
Check(FPDFAnnot_AppendAttachmentPoints(Annotation, QuadPoints) <> 0,
'Cannot set attachment points')
else
Check(FPDFAnnot_SetAttachmentPoints(Annotation, 0, QuadPoints) <> 0,
'Cannot set attachment points');
コンポーネントはすべてのFPDFAnnot_*エントリポイントをPDFium.pas内にエクスポートしているため、エクスポートされたC関数を直接呼び出す場合も同様のパターンが適用されます。FPDF_ANNOTATIONハンドルに対して四角形データを書き込みたい場合は、まずFPDFAnnot_CountAttachmentPointsで現在数を確認し、それに応じて呼び出す関数を切り替えてください。「FPDFAnnot_SetAttachmentPoints returns false」という問題でお困りの場合、この個数チェックとAppendへの分岐処理がほぼ間違いなく解決策となります
TPdf.CreateAnnotationによるハイライトの作成
コンポーネントがAppendとSetの振り分け処理を内部で行ってくれるため、開発者はレコードに必要なデータを格納するだけでハイライトを作成できます。以下の例では、A4ページを作成し、200×20ポイントの領域に半透明の黄色のハイライトを配置します。四角形の頂点は上述のZ順に従い、さらにRectangleがその四角形を囲むように設定されている点に注目してください。これにより、注釈の選択判定をRectで行うビューアでも正常に動作します
var
Pdf: TPdf;
A: TPdfAnnotation;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
Pdf.AddPage(0, 595, 842);
FillChar(A, SizeOf(A), 0);
A.Subtype := anHighlight;
A.HasColor := True;
A.Color := clYellow;
A.ColorAlpha := $80; // 50% opacity
A.HasAttachmentPoints := True;
A.AttachmentPoints[1].X := 50; A.AttachmentPoints[1].Y := 700; // top-left
A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // top-right
A.AttachmentPoints[3].X := 50; A.AttachmentPoints[3].Y := 680; // bottom-left
A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // bottom-right
A.Rectangle.Left := 50; A.Rectangle.Top := 700;
A.Rectangle.Right := 250; A.Rectangle.Bottom := 680;
A.ContentsText := 'Highlighted region';
Pdf.CreateAnnotation(A);
Pdf.SaveAs('highlighted.pdf');
finally
Pdf.Free;
end;
end;
サブタイプの切り替えは1行の変更で行えます。anUnderline(下線)、anStrikeout(取り消し線)、およびanSquiggly(波線)は、四角形データも含めて完全に同じレコード構造を使用します。なぜなら、ISO 32000-1では、これら3つを四角形領域の装飾スタイルのみが異なる同一の注釈ファミリーとして扱っているためです。テキストマークアップ以外のサブタイプ、例えばanSquare(四角形)、anCircle(楕円)、anText(テキスト注釈)などは、Rectangleの位置データのみから配置されます。これらについてはHasAttachmentPointsをFalseに設定しておけば、QuadPointsの処理は実行されません
なぜDelphiではAttachmentPoints[0]がコンパイルを通り、FPCではエラーになるのか?
TQuadrilateralPointは、1から始まる配列であるarray [1..4] of TPdfPointとして宣言されています。これは、配列インデックスを0から数える習慣のある開発者が間違いやすい箇所です。A.AttachmentPoints[0]と書き込んでも、Delphiのdcc32コンパイラはデフォルトで範囲チェックが無効になっているため、警告なしでコンパイルを通します。しかし実行時には、この記述は配列の直前にあるメモリ領域、すなわちTPdfAnnotationレコードの隣接するフィールドのデータを暗黙的に読み書きすることになります。その結果、ハイライトの1つの角の座標が壊れたり、隣のフィールド値が破損したりしますが、エラー例外は発生しません。Free PascalによるLazarusへのポーティング時に、デモソース内に含まれていたこのバグが検出されました。fpcコンパイラは定数のインデックスに対してコンパイル時に範囲チェックを実行するため、AttachmentPoints[0..3]という記述をエラーとしてコンパイルを拒否しました。これにより、インデックスのズレと、上述のSetとAppendに関するライブラリ内のバグが同時に発見されました
この問題から、2つの良い開発習慣が導き出されます。第1に、上述のコード例のように四角形のインデックスには1から4を使用すること、第2に、安全性を確認するために、Delphiの{$R+}ディレクティブやfpcを用いて、範囲チェックを有効にした状態で少なくとも1回はコードをビルドすることです。デフォルト設定のdcc32ビルドが成功したことはインデックスが正しい証明にはなりず、単にその位置にあったメモリデータにアクセスした際にフリーズしなかったという事実にすぎません
実際のテキストからの四角形座標の取得
デモプログラムでは座標の直書きで問題ありませんが、実際の製品でテキストをハイライトする場合は本物の文字の輪郭に合わせる必要があり、座標値は推測ではなくPDFiumのテキスト構造から取得すべきです。PDFium Componentによるテキスト抽出ガイドで解説しているルーチンを使用すれば、四角形と同じページ座標空間において文字ごとの境界ボックスデータを取得できるため、検索結果のヒット位置を、最初の文字の左端、最後の文字 of 右端、および行の上端と下端からなる頂点座標に直接変換できます。独自のテキストデータを描画する際に、配置前に各行の配置位置を特定したい場合は、テキスト測定と自動折り返しの記事で、あらかじめサイズ情報を計算する方法を説明しています
動作上の明確な境界事項として、TPdfAnnotationレコードは単一のTQuadrilateralPointしか保持できないため、1回のCreateAnnotationの呼び出しで作成できる四角形は1つだけです。3行にまたがる選択範囲には、仕様書§12.5.6.10に基づき、行ごとに1つずつ計3つの四角形データが必要になります。これを実現するには2つのアプローチがあります。簡単な方法は行ごとに個別の注釈を1つずつ作成することであり、これはすべてのビューアで正常にレンダリングされ、コンポーネントレベルのAPIのみで実装できます。もう1つの方法として、1つの注釈に3つの四角形を含める場合は、コンポーネントを通じて注釈を作成した後に、エクスポートされているFPDFAnnot_AppendAttachmentPointsを直接呼び出して、2番目と3番目の四角形データを追加します。Appendは置換ではなく新規のスロットを作成するため、この処理が正しく動作します。複数登録を行うためにSetAttachmentPointsを繰り返し呼び出すアプローチは避けてください。現在の登録数を超えるインデックスを指定すると、新規作成時にインデックス0で失敗したのと同様にすべてFalseが返されるためです
書き込み処理の完了後は、戻り値のステータスだけを信用するのではなく、実際のPDFビューアで検証を行ってください。AcrobatやPDFiumベースのビューアでファイルを開き、マークアップがテキストの正しい位置に描画されているか、意図した通りの透明度になっているか、保存して再読み込みした後もデータが壊れていないかを確認します。ここで説明した注釈タイプ、四角形領域の処理、およびアロケーション個数を自動判別する書き込み処理は、Delphi、C++Builder、およびLazarus向けの標準のPDFium Componentに実装されています。製品ページでは、ライブラリの他の機能とともに注釈APIのリファレンスを掲載しています