PDFlibPasは、Office自動化を使わずにPDFコンテンツを2種類の編集可能な形式へ変換します。ExportPageMarkdownとExportDocumentMarkdownは、見出し、番号付き・番号なしリスト、パイプ形式の表を推定した意味的なMarkdownを返し、SaveDOCXToFileとSaveDOCXToStreamは、段落、見出し、ネイティブなリスト番号付け、検出済みの表、フォントスタイル、改ページ、配置済みのPNG画像を含むWordprocessingMLパッケージを書き出します
どちらもすべてPascalだけで完結し、サーバー上でWordのインストールもCOMも不要で動作します。この制約こそが、この機能がデスクトップツールではなくPDFライブラリに存在する理由です
なぜ「PDFからWordへの変換」は本当に難しいのか
なぜなら、PDFページには段落というものが存在しないからです。PDFに含まれるのは、生成側が出力した順序のまま座標上にグリフの連なりを配置するテキスト表示演算子であり、2つのランが同じ文章に属することを示す義務すら、まして同じリスト項目であることを示す義務もありません。このフォーマットは印刷ページを正確に記述するために設計されており、そのページを作り出した構造を捨て去ることによってその目的を達成しています
そのため、どのコンバーターも、生成側が捨てたものを再構築しなければなりません。行のグループ化は、行間の間隔とベースラインの位置合わせから導き出されます。段落の境界は、間隔の変化とインデントから導き出されます。見出しとは、本文よりも大きい、または太いフォントで、後に続く部分から独立して見える行のことです。リストとは、箇条書き記号や番号パターンで始まる段落の連なりのことです。表とは、行と列にまたがって端が揃うテキストブロックの格子のことです。そのどれもが推定にすぎず、推定であるということは、通常のタイポグラフィの慣習に従うドキュメントでは良い結果になり、従わないドキュメントでは平凡な結果になるということです
タグ付きPDFは例外であり、しかも大きな例外です。ドキュメントが構造ツリーを保持している場合、段落、見出し、リスト、表の役割は推測されるのではなく記録されているため、タグ付きPDFのアクセシビリティ構造で説明されているアクセシビリティ対応の作業が、変換品質にも見返りをもたらします。生成側を自分でコントロールできるなら、出力にタグを付けておくことは、後でそれを変換する必要がある誰にとっても、最も効果の大きい単一の対策です
Markdownエクスポート:1ページずつ処理する
Markdown経路は、変換先がテキストパイプライン、たとえばドキュメントサイト、検索インデックス、アシスタント用の検索コーパスである場合に選ぶべき手段です。オプションはビットマスクとして指定します。PDF_MARKDOWN_INCLUDE_PAGE_MARKERS、PDF_MARKDOWN_DETECT_HEADINGS、PDF_MARKDOWN_PRESERVE_STYLESがあり、PDF_MARKDOWN_DEFAULTはこの3つをすべて組み合わせたものです
var
Pdf: TPDFlib;
Md: WideString;
begin
Pdf := TPDFlib.Create;
try
Pdf.LoadFromFile('handbook.pdf', '');
// 1ページ分を文字列として取得
Md := Pdf.ExportPageMarkdown(1, PDF_MARKDOWN_DEFAULT);
// ページ範囲を指定し、BOMなしのUTF-8としてディスクへストリーミング
Pdf.SaveMarkdownToFile('1-40',
PDF_MARKDOWN_DETECT_HEADINGS or PDF_MARKDOWN_PRESERVE_STYLES,
'handbook.md');
finally
Pdf.Free;
end;
end;
ページマーカーは検索用途において真価を発揮します。出典ページの情報を持つテキストチャンクは正確に引用でき、その引用をたどった読者は、その主張が実際にある場所へたどり着けます。Markdownが人間の閲読を目的とする場合は、元のレイアウトに由来するページ境界はノイズになるため、これをオフにしてください
ストリーミング用のエントリポイントは、大きなドキュメントにとって重要です。SaveMarkdownToStreamとSaveMarkdownToFileは1ページずつUTF-8を書き出し、出力全体をバッファリングしないため、900ページのマニュアルがまずメモリ上で900ページ分の文字列になるようなことはありません。バイトオーダーマークが存在しないのも意図的な設計です。MarkdownファイルにBOMが付いていると、驚くほど多くの静的サイトジェネレーターやdiffツールが混乱してしまいます
マシンにOfficeがなくてもDOCXを生成できる
DOCXライターは、パッケージそのものを生成します。CRCチェック付きの生のDeflateとして書き込まれるZIPエントリ、WordprocessingMLの各パーツ、そしてそれらを結びつけるリレーションシップです。Wordを一切呼び出さないため、変換はヘッドレスサーバー上でも、サービスアカウント内でも、コンテナ内でも、つまりOffice自動化がライセンス上使えない、不安定、あるいは禁止されているあらゆる場所で実行できます
var
Pdf: TPDFlib;
Target: TFileStream;
begin
Pdf := TPDFlib.Create;
Target := TFileStream.Create('handbook.docx', fmCreate);
try
Pdf.LoadFromFile('handbook.pdf', '');
Pdf.SaveDOCXToStream('1-40',
PDF_DOCX_INCLUDE_IMAGES or PDF_DOCX_DETECT_HEADINGS or
PDF_DOCX_PRESERVE_STYLES or PDF_DOCX_PRESERVE_PAGE_BREAKS,
Target);
finally
Target.Free;
Pdf.Free;
end;
end;
画像データは、末尾でまとめて追加するのではなく、各ページを処理するたびに書き込まれるため、ピークメモリ使用量はドキュメント全体ではなく1ページ分に収まります。明示的なページ順序は保持され、選択中のPDFページも処理後に元通り復元されるため、このエクスポートが、別の理由ですでにページが選択されているより長い処理の一部として組み込まれている場合にも意味を持ちます
決定論的なパッケージングは何をもたらすのか
バイト単位での再現性です。同じ入力を同じオプションで2回変換すると、まったく同じパッケージが生成されます。つまり、出力をハッシュ化して変更を検出したり、生成ドキュメントの2つのビルドをdiffしたり、同一の入力から異なる成果物が生まれる心配なく積極的にキャッシュしたりできるということです
Office自動化にはこれを保証できません。タイムスタンプ、リビジョン識別子、マシン依存のメタデータが埋め込まれるため、同じドキュメントを2回変換しても、ハッシュ比較を無意味にする形で結果が食い違います。同じ理屈が、再現可能なビルドのための決定論的PDF IDで論じている決定論的なファイル識別子の背景にもあります。出力が再現可能であれば、検証は目視確認ではなく単純な比較で済むようになります
出力の品質が高い場面と、そうでない場面
この点についてはユーザーに正直に伝えるべきです。変換品質は、コンバーターの性能よりも入力の性質によって大きく左右されるからです。タグ付きPDFや、請求書、レポート、契約書のようなきれいに生成されたビジネス文書は良好に変換されます。見出しは見出しとして着地し、表は保持され、リストはWord上で正しく番号が振り直されます。2段組みの学術系レイアウトは、段組みの幾何構造が規則的であればまずまず妥当に変換されます。改ページをまたぐ表は推定によって再構成されるため、分割されてしまうこともあります。読む順序ではなく視覚効果のためにテキストが配置されている作り込まれたマーケティング資料は変換品質が低く、どれだけ推定を重ねてもこれを解決することはできません
スキャンされたドキュメントはまったく別の問題です。1枚の大きな画像でしかないページにはテキストオブジェクトが存在しないため、テキストレイヤーが生成されるまでエクスポートできるものが何もありません。それを生成するOCR処理はオプションではなく前提条件です。大量バッチを実行する前に、代表的なファイルを十数件サンプリングして出力を確認し、テキスト検索とページ要素の列挙で説明されている方法でまずページ要素を列挙し、ページに実際に何が含まれているかを把握することを検討してください
アシスタントや検索パイプライン向けには、Markdown経路のほうが良い選択肢になるのが通常です。見出しはチャンクの境界となり、表はパイプ形式のまま読みやすく保たれ、ページマーカーによってすべてのチャンクに引用可能な位置情報が与えられます。人間による編集には、DOCXが答えになります。ユーザーが求めているのはテキストそのものではなく、それを変更できる能力だからです
PDFlibPasはDelphi、C++Builder、LazarusのPDFライブラリで、対応するDLLおよびActiveXインターフェースも備えているため、同じエクスポート呼び出しはC#、C++、スクリプトホストからも利用できます。完全なドキュメントと評価版はPDFlibPas Delphi PDFライブラリページにあります