HotPDF Componentは、DelphiおよびC++Builderから既存のPDF内のテキストを検索および置換できます。SearchLoadedPageTextおよびSearchLoadedDocumentTextはグリフレベルの精度で文字列のすべての出現箇所を特定し、ReplaceLoadedPageTextおよびReplaceLoadedDocumentTextは一致したバイトデータをその場で書き換えます。ただし、置換後のすべての文字が元のフォントを介して再エンコードできる必要があり、本記事ではこの物理的な制約について注記に隠すことなく率率に解説します
PDF内のテキスト置換が困難なのは、PDFページに編集可能なテキストが含まれておらず、位置指定されたグリフが含まれているためです。ISO 32000-1 §9.4のテキスト表示モデルでは、コンテンツストリームがTjやTJなどのオペレータを駆動し、テキスト行列によって設定された座標に文字コードのシーケンスを描画します。これらのコードはUnicodeではなく、ページのフォントが宣言するエンコーディングへのインデックスであり、読み取り可能な文字へのマッピングは/ToUnicode CMap、エンコーディングの差分配列、またはCIDマッピングチェーン内に存在します。段落オブジェクトやテキストフローは存在せず、視覚的な1単語が1つの文字列として保存されている保証すらありません
置換処理は、デコードに加えてさらなる難しさを伴います。元のストリームのどのバイトが各グリフを生成したかを正確に把握し、そのスパンのみに新しいバイトを正確に挿入する必要があります。テキスト抽出ツールは、Unicodeを出力した後はバイト位置を破棄できますが、置換ツールではそれができません。そのため、HotPDFはこの機能を2つのリリースに分割しました。v2.251.0でオフセット追跡および検索レイヤーを構築し、v2.252.0でその上に書き換えレイヤーを構築しました
テキストの検索:バイトオフセット追跡によるグリフレベルの検索
HotPDFのSearchLoadedDocumentTextは、生のストリームバイトではなく、各ページのデコードされたUnicodeグリフシーケンスに対してマッチングを行うことで、検索文字列のすべての出現箇所を検出します。そのため、フォントがどのようにエンコードしたかに関係なく確実に一致を検出できます。ベースとなるインフラはv2.251.0で導入されました。コンテンツストリームのトークナイザは、すべての文字列オペランド(( ) や < > などのデリミタを含む)についてStartOfs/EndOfsバイトスパンを記録し、デコードされたすべてのグリフは、それを生成した正確なオペランド、TJ配列の項目、およびコードユニットを指し示すTokenIndex/ItemIndex/ByteOffsetのトリプル情報を保持します。これと同じグリフインタープリタが、Delphiでのロード済みPDFからのテキスト抽出で説明している抽出APIも支えています。検索処理は、抽出処理が破棄するソース情報を保持する点が異なります
一致した各箇所は、ページインデックス、該当するグリフ範囲、ヒットした箇所のユーザ空間におけるX/Y座標原点と幅、ソースのトークンおよび項目インデックス、および一致したテキスト自体を保持するTHPDFTextMatchレコードとして返されます。これは、ハイライト表示、確認用のUI、または置換ステップを処理するのに十分な情報です。検索結果が見つからない場合は、エラーを返すのではなく空の配列を返すため、呼び出しパターンをシンプルに維持できます
var
Matches: THPDFTextMatchArray;
Expected, Replaced: Integer;
begin
Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
Expected := Length(Matches);
Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
if Replaced < Expected then
WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
'from the font subset, or match spans multiple operands',
[Expected - Replaced]));
end;
テキストの置換:逆エンコードと部分書き換え
HotPDF v2.252.0で追加されたReplaceLoadedDocumentTextは、デコード機構を逆方向に実行することで、検索文字列のすべての出現箇所を書き換えます。HPDFEncodeUnicode関数は文字コードデコーダの逆の処理を行います。同じ解決チェーンを逆順(/ToUnicodeのbfcharおよびbfrangeのルックアップ、エンコーディングストリームのCIDマッピング、Type0のIdentityマッピング、および定義済みのWinAnsiおよびMacRomanテーブル)でスキャンし、置換後の各文字を元のフォントが想定する文字コードバイトに変換します。再エンコードされたバイトは、トークナイザ自体のエスケープルールを反映した適切な文字列リテラルまたは16進数文字列にシリアル化されるため、パースから再シリアル化への往復処理が安定して機能します
書き換え処理自体は、全体を一括で置き換えるのではなく、部分的に実行されます。文字列オペランド内の、一致した箇所に対応するコードバイト範囲のみが置換され、同一オペランド内の不一致のバイト、トークン間の空白、および周囲のすべてのオペレータはバイト単位でそのまま維持されます。例えば、abcabc内のbcaを置換すると、オペランドが破壊されることなく、a + 置換文字列 + bcになります。置換文字列は元の検索文字列より短くても長くてもよく、リテラルが再シリアル化されてストリームの/Lengthが更新されます。複数ストリームページの各/Contentsストリームは個別に処理されるため、ページの構造は正しく維持されます
var
Pdf: THotPDF;
Matches: THPDFTextMatchArray;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
begin
if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
for I := 0 to Length(Matches) - 1 do
WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
[Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
Matches[I].Text]));
end;
finally
Pdf.Free;
end;
end;
なぜフォントサブセットに含まれていない文字への置換はできないのか?
埋め込まれたフォントサブセットに一度も含まれていない文字へテキストを置換することはできません。その文字を選択するためのバイトシーケンスが、フォントのマッピングテーブルに存在しないためです。PDF生成ツールがサブセットフォントを埋め込むとき、その/ToUnicode CMapやエンコーディング構造は、元のドキュメントが実際に使用していたグリフのみをカバーします。HPDFEncodeUnicodeは存在するマッピングのみを逆変換できます。元のドキュメントのそのフォントに文字Eが一度も含まれていなかった場合、Eに変換するための文字コードは存在しません。これはファイルの物理的な性質であり、特定のライブラリの制限ではありません。どのようなツールであっても、埋め込まれていないグリフマッピングを作り出すことはできません
HotPDFはこの失敗を安全側に処理します。置換文字列内の1文字でも再エンコードできない場合、その該当箇所全体の置換がスキップされます。例外は発生せず、不完全で文字化けしたテキストが生成されることもありません。単にReplaceCountにカウントされなくなります。実務上の対策として、事前に検索して得られた一致件数とReplaceCountを比較し、不足している場合は警告として扱ってください。上記の日の例では、書き換えを成功させるために、ドキュメントの同じフォントを使用するテキストのどこかに数字6が現れている必要があります(請求書などではあり得ますが、一般的には保証されません)。必要な文字が利用できず、目的がテキストの言い換えよりも機密情報の削除である場合は、情報の削除自体が適しています。その方法については、Delphiでのロード済みPDFの黒塗りと再構築を参照してください
var
Pdf: THotPDF;
ReplaceCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
begin
if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
True, ReplaceCount) then
WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
Pdf.SaveLoadedDocument('contract-final.pdf');
end;
finally
Pdf.Free;
end;
end;
保存時にファイル内で何が変更されるのか?
置換された/Contentsストリームは非圧縮で保存されます。FlateDecode圧縮されたストリームは編集のために解凍され、HotPDFが再構築されたバイトデータを書き込む際、再圧縮を行う代わりにストリームの/Filterエントリを削除し、/Lengthを更新します。生成されたPDFは完全に有効であり、主要なビューアで正常にレンダリングされますが、トレードオフとして編集されたストリームのファイルサイズが大きくなります。数千のドキュメントを処理するバッチパイプラインでは、このサイズ増加を考慮するか、後続の処理で個別に圧縮処理を実行してください。保存時に書き換えられたオブジェクトがドキュメントの相互参照構造とどのように相互作用するかについては、HotPDFにおけるオブジェクトストリームとインクリメンタルアップデートで解説しています
ファイル内の他のすべての部分はそのまま維持されます。変更されていないストリームは圧縮状態を維持し、フォントや画像は再書き込みされず、オペランドレベルの部分書き換えにより、編集されたストリームであっても一致箇所のみが元のファイルと異なります。この保守的なアプローチは意図的なものです。ライブラリがロードされたドキュメントを書き換える範囲が広ければ広いほど、PDF生成ツールの想定外の癖を破壊してしまうリスクが高まるためです
テキストの検索および置換は、抽出、黒塗り、およびページレンダリングとともに、HotPDFのロード済みドキュメント処理ツールセットに含まれており、すべて同じコンテンツストリームインタープリタによって駆動されます。Delphi 5から現在のRAD Studioのリリースまで、外部依存なしで利用可能です。完全なAPIリファレンスと評価版のダウンロードは、HotPDF Componentの製品ページに掲載されています