💡 Full working example available on GitHub:
extract-annotations-from-pdf-using-groupdocs-parser-dotnet

Introduction

Một tệp PDF đã được xem xét thường chứa nhiều hơn chỉ văn bản hiển thị – các ghi chú dính, các đoạn được tô sáng và các bình luận nội dòng do người đánh giá để lại. Cuộn qua từng trang để tìm chúng không khả thi khi tài liệu đã trải qua nhiều vòng phản hồi. GroupDocs.Parser là một thư viện .NET cho phép đọc các chú thích nhúng trong tài liệu một cách lập trình, biến các bình luận rải rác của người đánh giá thành dữ liệu có cấu trúc mà mã của bạn có thể xử lý. Hướng dẫn này sẽ chỉ cách trích xuất chú thích từ toàn bộ PDF, phân tách chúng theo trang, lấy chúng cùng với văn bản của tài liệu, và xuất kết quả ra CSV hoặc JSON.

Tôi gặp vấn đề này khi xây dựng một công cụ theo dõi đánh giá cho đội ngũ tài liệu: một bản ghi chú phát hành 40 trang đã được ba người đánh giá, và việc mở tệp để tìm mọi bình luận mất nhiều thời gian hơn việc thực sự sửa các vấn đề họ đã chỉ ra. Việc trích xuất chú thích chỉ trong vài dòng mã đã biến công việc thành một nhiệm vụ kéo dài hai phút.

Trong các phần sau, bạn sẽ học cách:

  • Trích xuất mọi chú thích từ một PDF trong một lần gọi.
  • Gắn nhãn mỗi chú thích với trang mà nó thuộc về.
  • Lấy văn bản tài liệu và văn bản chú thích cùng nhau trong một lần đọc.
  • Chuẩn hóa kết quả ra CSV hoặc JSON để các công cụ downstream sử dụng.

Why Extracting PDF Annotations Matters

Đọc chú thích PDF một cách lập trình hữu ích cho:

  • Quy trình đánh giá: Thu thập mọi bình luận của người đánh giá mà không cần mở tệp trong trình xem PDF.
  • Hợp tác: Hiển thị các đoạn được tô sáng hoặc có ghi chú trực tiếp trong công cụ của bạn.
  • Kiểm toán: Giữ hồ sơ các đánh dấu trên tài liệu theo thời gian, ngay cả khi tài liệu đã được làm phẳng hoặc hoàn thiện.

GroupDocs.Parser đã thêm khả năng trích xuất chú thích gốc cho tài liệu PDF từ phiên bản 26.7 thông qua phương thức GetAnnotations, cùng với tùy chọn mới IncludeAnnotations trên TextOptions để kéo văn bản chú thích vào kết quả đọc văn bản thông thường.

Prerequisites

  • .NET 6.0 hoặc mới hơn
  • GroupDocs.Parser for .NET 26.7+ (temporary license)
  • Một tệp PDF có sẵn các chú thích (ví dụ: document-with-annotations.pdf)

Cài đặt qua NuGet:

dotnet add package GroupDocs.Parser

How do I extract annotations from a PDF document?

Answer: Load the file with Parser, then call GetAnnotations() for the whole document or GetAnnotations(pageIndex) for a single page. Each result is a collection of AnnotationItem objects whose Value property holds the comment text. If you’d rather see comments inline with the document’s regular content, set IncludeAnnotations on TextOptions and call GetText instead.

Whole‑Document Extraction

The following snippet pulls every annotation out of the file in a single call, which is the fastest way to check whether a document has any open comments at all.

// 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() returns null when annotation extraction isn’t supported for the document, and an empty collection when the document simply has none.
  • Each AnnotationItem exposes its text through the Value property – that’s the only data point the SDK currently reports.
  • No page attribution is included here; use the per‑page overload below if you need it.

Per‑Page Extraction

When a comment’s location matters, loop over the document’s pages and call GetAnnotations(pageIndex) for each one.

// 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 drives the loop; there’s no separate “annotation page count”.
  • GetAnnotations(pageIndex) uses a zero‑based index, matching every other page‑level method in the API.
  • The resulting AnnotationRecord list is exactly the shape a CSV or JSON export needs.

Extracting Text Together with Annotations

Instead of two passes over the document, you can fold annotation text directly into the regular text extraction output.

// 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:

  • IncludeAnnotations is a property on TextOptions, so this works with the same GetText call you’d already use for plain text extraction.
  • Useful when you want a single transcript‑style output rather than a separate comment list.
  • Combine it with GetText(pageIndex, options) if you only need this for one page.

Checking Annotation Support First

Not every format supports annotations, so it’s worth checking before you build logic around GetAnnotations.

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

Key points:

  • Features.Annotations is a simple boolean flag on the Parser instance.
  • Checking it upfront makes intent explicit, even though GetAnnotations already fails gracefully by returning null.

Exporting the Annotations to CSV

A CSV export lets reviewers open the comment list directly in Excel. The method below writes a two‑column file (page,value) from the page‑tagged records built earlier.

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 safely quotes fields containing commas, quotes, or line breaks.
  • The resulting file opens directly in Excel or can be piped into a ticketing tool.

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

For pipelines that consume comments programmatically, a JSON array is usually a better fit than a flat CSV.

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:

  • The output is a flat array of { page, value } objects – easy for any downstream service to deserialize.
  • Escape keeps the payload valid JSON without pulling in a serialization library.

Helper: Escape

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

Comparing Methods: When to Use Each

Phương pháp Thích hợp cho Ưu điểm chính Hạn chế
Whole‑Document Extraction Kiểm tra nhanh “có bình luận nào không?” Gọi một lần, mã đơn giản nhất Không có thông tin trang
Per‑Page Extraction Gửi phản hồi tới đúng phần Kết quả có nhãn trang, sẵn sàng xuất Gọi thêm một lần cho mỗi trang
Combined Text + Annotations Tạo một bản ghi chép duy nhất Không cần lần đọc thứ hai Bình luận không tách riêng khỏi nội dung
CSV Export Theo dõi đánh giá bằng bảng tính Dễ mở trong Excel, người đọc được Cấu trúc phẳng, hạn chế
JSON Export Pipeline tự động, hệ thống ticket Cấu trúc, máy đọc được Kích thước payload hơi lớn hơn

Bắt đầu với việc trích xuất toàn tài liệu để xác nhận tệp có bình luận đáng chú ý, sau đó chuyển sang trích xuất theo trang khi cần định vị phản hồi vào phần cụ thể.

Best Practices and Tips

  • Giải phóng Parser kịp thời: bao bọc trong khối using để giải phóng tài nguyên gốc.
  • Phân biệt null và rỗng: GetAnnotations trả về null nghĩa là định dạng không hỗ trợ; một collection rỗng nghĩa là tài liệu không có bình luận.
  • Kiểm tra Features.Annotations trong các job batch: bỏ qua các tệp không hỗ trợ ngay từ đầu thay vì dựa vào kiểm tra null sâu trong vòng lặp.
  • Tái sử dụng danh sách đã gắn nhãn trang: tạo một lần bằng ExtractAnnotationsByPage và dùng chung cho cả exporter CSV và JSON, để hai đầu ra không bị lệch nhau.
  • Bảo mật: văn bản chú thích là đầu vào tự do của người đánh giá – hãy xử lý chúng như bất kỳ chuỗi không tin cậy nào trước khi hiển thị trong UI hoặc báo cáo.

Conclusion

GroupDocs.Parser cung cấp cách trực tiếp, lập trình để lấy các bình luận của người đánh giá ra khỏi PDF thay vì phải tìm kiếm thủ công. Bằng cách trích xuất chú thích cho toàn bộ tài liệu, gắn nhãn chúng theo trang, hoặc gộp chúng vào luồng văn bản thông thường, bạn có thể xây dựng quy trình đánh giá mà ngay khi tài liệu vào pipeline đã hiện ra phản hồi. Xuất kết quả ra CSV hoặc JSON và tích hợp ngay vào các công cụ mà đội ngũ của bạn đã sử dụng.

Next steps:

Additional Resources