💡 フルワーキングサンプルは GitHub で入手できます:
extract-annotations-from-pdf-using-groupdocs-parser-dotnet

Introduction

レビューが行われた PDF には、目に見えるテキスト以外にも、付箋、ハイライトされたコメント、インラインコメントなど、レビュアーが残した注釈が多数含まれています。ページをすべてスクロールして探すのは、ドキュメントが何度もフィードバックを受けた場合には現実的ではありません。GroupDocs.Parser は、ドキュメントに埋め込まれた注釈をプログラムから読み取ることができる .NET ライブラリで、散在するレビュアーコメントを構造化データに変換し、コードから操作できるようにします。 本チュートリアルでは、PDF 全体から注釈を抽出し、ページ単位に分解し、ドキュメントのテキストと一緒に取得し、結果を CSV または JSON にエクスポートする方法を示します。

ドキュメントチーム向けのレビュー追跡ツールを作成しているときにこの問題に直面しました。40 ページのリリースノートが 3 人のレビュアーを経ており、手作業でファイルを開いてすべてのコメントを探すのに、実際に指摘された問題を修正する時間よりも長くかかっていました。数行のコードで注釈を抽出すれば、作業は 2 分で完了しました。

以下のセクションで学べること:

  • PDF からすべての注釈を一度に抽出する方法
  • 各注釈に所属ページをタグ付けする方法
  • ドキュメントテキストと注釈テキストを同時に取得する方法
  • 結果を CSV または JSON にシリアライズして下流ツールで利用する方法

Why Extracting PDF Annotations Matters

PDF の注釈をプログラムから取得することは次のような場面で有用です。

  • レビュー ワークフロー:PDF ビューアを開かずに、すべてのレビュアーコメントを収集できます。
  • コラボレーション:ハイライトや付箋で示された箇所を自分のツール内に直接表示できます。
  • 監査:文書がフラット化または最終化された後でも、時間経過に伴うマークアップの履歴を保持できます。

GroupDocs.Parser はバージョン 26.7 で PDF 文書向けにネイティブな注釈抽出機能を追加し、GetAnnotations メソッドと、注釈テキストを通常のテキスト取得に組み込むための TextOptionsIncludeAnnotations オプションを提供しています。

Prerequisites

  • .NET 6.0 以降
  • GroupDocs.Parser for .NET 26.7+(temporary license
  • 既存の注釈が付いた PDF ファイル(例:document-with-annotations.pdf

NuGet でインストール:

dotnet add package GroupDocs.Parser

How do I extract annotations from a PDF document?

Answer: Parser でファイルを読み込み、ドキュメント全体の場合は GetAnnotations()、単一ページの場合は GetAnnotations(pageIndex) を呼び出します。返されるコレクションは AnnotationItem オブジェクトの集合で、Value プロパティにコメントテキストが格納されています。コメントを文書の通常テキストと同時に取得したい場合は、TextOptionsIncludeAnnotationstrue に設定し、GetText を呼び出します。

Whole‑Document Extraction

以下のスニペットは、ファイル全体からすべての注釈を一度の呼び出しで取得します。これが、文書にコメントが存在するかどうかを最速で確認する方法です。

// Extract every annotation from the whole document
var result = new List<string>();
using (var parser = new Parser(path))
{
    IEnumerable<AnnotationItem> annotations = parser.GetAnnotations();
    if (annotations == null)
    {
        return result; // format doesn't support annotations
    }

    foreach (var item in annotations)
    {
        result.Add(item.Value); // annotation text
    }
}
return result;

Key points:

  • GetAnnotations() は、ドキュメントが注釈抽出に対応していない場合に null を返し、コメントが全くない場合は空コレクションを返します。
  • AnnotationItemValue プロパティでテキストを公開します。現在 SDK が提供できる唯一のデータです。
  • ページ情報は含まれません。ページ単位が必要な場合は下記のオーバーロードを使用してください。

Per‑Page Extraction

コメントの位置が重要な場合は、ドキュメントのページをループし、各ページに対して GetAnnotations(pageIndex) を呼び出します。

// Tag each annotation with its zero-based page index
var result = new List<AnnotationRecord>();
using (var parser = new Parser(path))
{
    if (!parser.Features.Annotations)
    {
        return result;
    }

    var info = parser.GetDocumentInfo();
    if (info == null || info.PageCount == 0)
    {
        return result;
    }

    for (int pageIndex = 0; pageIndex < info.PageCount; pageIndex++)
    {
        IEnumerable<AnnotationItem> pageAnnotations = parser.GetAnnotations(pageIndex);
        if (pageAnnotations == null)
        {
            continue;
        }

        foreach (var item in pageAnnotations)
        {
            result.Add(new AnnotationRecord { PageIndex = pageIndex, Value = item.Value });
        }
    }
}
return result;

Key points:

  • GetDocumentInfo().PageCount がループの上限となります。注釈専用のページ数は存在しません。
  • GetAnnotations(pageIndex) は 0 ベースのインデックスを使用し、API の他のページレベルメソッドと同様です。
  • 生成された AnnotationRecord リストは CSV または JSON エクスポートにそのまま利用できます。

Extracting Text Together with Annotations

ドキュメントを二回走査する代わりに、注釈テキストを通常のテキスト抽出結果に直接組み込むことができます。

// Read document text with annotation text included
using (var parser = new Parser(path))
{
    var options = new TextOptions
    {
        IncludeAnnotations = true
    };

    using (TextReader reader = parser.GetText(options))
    {
        return reader?.ReadToEnd() ?? string.Empty;
    }
}

Key points:

  • IncludeAnnotationsTextOptions のプロパティであるため、プレーンテキスト抽出に使用している GetText 呼び出しと同じ形で機能します。
  • コメントリストを別に作成せず、1 つのトランスクリプト形式の出力が欲しいときに便利です。
  • 特定のページだけが対象の場合は GetText(pageIndex, options) を併用してください。

Checking Annotation Support First

すべてのフォーマットが注釈をサポートしているわけではないため、GetAnnotations を呼び出す前に事前チェックすると安全です。

// Returns true if the loaded document format supports annotation extraction
using (var parser = new Parser(path))
{
    return parser.Features.Annotations;
}

Key points:

  • Features.AnnotationsParser インスタンス上のシンプルなブールフラグです。
  • 事前にチェックしておくことで、ロジックが意図的であることが明示されます。GetAnnotations 自体も null を返すことで失敗を穏やかに処理します。

Exporting the Annotations to CSV

CSV エクスポートにより、レビュー担当者はコメントリストを直接 Excel で開くことができます。以下のメソッドは、先ほど作成したページタグ付きレコードから 2 列(page,value)のファイルを書き出します。

var sb = new StringBuilder();
sb.AppendLine("page,value");

foreach (var record in records)
{
    sb.AppendLine($"{record.PageIndex},{CsvEscape(record.Value)}");
}

File.WriteAllText(outputPath, sb.ToString());

Key points:

  • CsvEscape はカンマ、ダブルクオート、改行を含むフィールドを安全にクオートします。
  • 生成されたファイルは Excel で直接開くことができ、チケットツールにパイプすることも可能です。

Helper: CsvEscape

if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
    return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;

Exporting the Annotations to JSON

プログラムからコメントを消費するパイプライン向けには、JSON 配列の方が一般的に適しています。

var sb = new StringBuilder();
sb.AppendLine("[");

for (int i = 0; i < records.Count; i++)
{
    var comma = i < records.Count - 1 ? "," : string.Empty;
    sb.AppendLine($"  {{ \"page\": {records[i].PageIndex}, \"value\": \"{Escape(records[i].Value)}\" }}{comma}");
}

sb.AppendLine("]");
File.WriteAllText(outputPath, sb.ToString());

Key points:

  • 出力は { page, value } オブジェクトのフラット配列です。下流サービスが簡単にデシリアライズできます。
  • Escape は外部シリアライズライブラリを使用せずに有効な JSON 文字列にします。

Helper: Escape

return s?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? string.Empty;

Comparing Methods: When to Use Each

方法 推奨シーン 主な利点 制限事項
Whole‑Document Extraction 「コメントがあるかどうか」だけを素早く確認したいとき 呼び出しが 1 回でコードが最もシンプル ページ情報が付与されない
Per‑Page Extraction フィードバックを正確なセクションに振り分けたいとき ページタグ付き結果が得られ、エクスポートが容易 ページごとに 1 回余分な呼び出しが必要
Combined Text + Annotations 1 つの可読テキストとして出力したいとき 文書を二度走査する必要がない コメントが本文テキストと分離されない
CSV Export スプレッドシートベースのレビュー管理 Excel で簡単に開け、人が読める形式 フラット構造に限られる
JSON Export 自動化パイプラインやチケットシステム 構造化され機械が読み取りやすい ペイロードがやや大きくなる

まずは Whole‑Document Extraction でコメントの有無を確認し、必要に応じてページ単位の抽出に切り替えてください。

Best Practices and Tips

  • Parser は速やかに破棄using ブロックでラップし、ネイティブリソースを解放します。
  • null と空コレクションを区別GetAnnotationsnull を返すのはフォーマット未対応、空コレクションはコメントが無いことを意味します。
  • バッチ処理では Features.Annotations を先にチェック:ループ内部で null を確認するよりも、対象外ファイルを早期にスキップできます。
  • ページタグ付きリストを再利用ExtractAnnotationsByPage で一度作成したリストを CSV と JSON の両方のエクスポートに流用すれば、出力が食い違うことはありません。
  • セキュリティ:注釈テキストはフリー形式のレビュアー入力です。UI やレポートに表示する前に、他の未信頼文字列と同様にサニタイズしてください。

Conclusion

GroupDocs.Parser を使えば、PDF からレビュアーコメントを手作業で探す必要がなく、プログラムから直接取得できます。文書全体の注釈抽出、ページ単位のタグ付け、またはテキストストリームへの統合といった手法を組み合わせることで、ドキュメントがパイプラインに入った瞬間にフィードバックを可視化できるワークフローを構築できます。結果を CSV や JSON にエクスポートすれば、チームがすでに利用しているツールへシームレスに連携できます。

次のステップ:

  • 完全なメソッドシグネチャとオーバーロードを確認するには、GetAnnotations API reference を参照してください。
  • 注釈と併せて PDF からテキストを抽出する方法は、extract text from PDF documents をご覧ください。
  • バッチ処理シナリオ向けのサンプルプロジェクトは GitHub の Examples Repo にあります。

Additional Resources