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

Introduction

리뷰를 거친 PDF는 보이는 텍스트 외에도 스티키 노트, 강조된 메모, 인라인 코멘트 등 다양한 주석을 포함하고 있습니다. 페이지를 일일이 스크롤하면서 주석을 찾는 방식은 문서가 여러 차례 피드백을 거칠 때는 비효율적입니다. GroupDocs.Parser는 문서에 내장된 주석을 프로그래밍 방식으로 읽어, 흩어져 있는 리뷰어 코멘트를 구조화된 데이터로 변환해 줍니다. 이 튜토리얼에서는 전체 PDF에서 주석을 추출하고, 페이지별로 나누어 가져오며, 문서 텍스트와 함께 추출하고, 결과를 CSV 또는 JSON으로 내보내는 방법을 보여줍니다.

문서 팀을 위한 리뷰 트래커를 만들던 중 이 문제를 마주했습니다. 40페이지 분량의 릴리즈 노트가 세 명의 리뷰어를 거쳤는데, 파일을 열어 모든 코멘트를 찾는 데 걸리는 시간이 실제 문제를 해결하는 시간보다 더 길었습니다. 몇 줄의 코드만으로 주석을 추출하면 두 분 안에 작업을 마칠 수 있었습니다.

다음 섹션에서는 다음을 배울 수 있습니다.

  • 한 번의 호출로 PDF의 모든 주석을 추출하기
  • 각 주석에 해당 페이지 번호를 태깅하기
  • 문서 텍스트와 주석 텍스트를 한 번에 읽어오기
  • 결과를 CSV 또는 JSON으로 직렬화하여 다운스트림 도구에 전달하기

Why Extracting PDF Annotations Matters

PDF 주석을 프로그래밍 방식으로 읽는 것이 유용한 경우:

  • 리뷰 워크플로: PDF 뷰어를 열지 않고도 모든 리뷰어 코멘트를 수집합니다.
  • 협업: 강조되거나 메모된 섹션을 자체 도구 안에서 바로 확인합니다.
  • 감사: 문서가 플래튼되거나 최종화된 후에도 시간 경과에 따른 마크업 기록을 보관합니다.

GroupDocs.Parser는 버전 26.7부터 PDF 문서에 대한 네이티브 주석 추출을 GetAnnotations 메서드와 TextOptions의 새로운 IncludeAnnotations 옵션을 통해 지원합니다.

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 속성에 코멘트 텍스트가 들어 있습니다. 문서 본문과 함께 코멘트를 보고 싶다면 TextOptionsIncludeAnnotations를 설정하고 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가 루프를 제어합니다. 별도의 “annotation page count”는 없습니다.
  • 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 호출에 그대로 적용됩니다.
  • 별도의 코멘트 리스트가 아니라 하나의 전사 형태 출력이 필요할 때 유용합니다.
  • 특정 페이지에만 적용하려면 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 인스턴스에 있는 간단한 불리언 플래그입니다.
  • 사전에 확인하면 의도를 명확히 할 수 있으며, GetAnnotationsnull을 반환해도 안전하게 처리됩니다.

Exporting the Annotations to CSV

CSV 내보내기를 사용하면 리뷰어가 Excel에서 바로 코멘트 목록을 확인할 수 있습니다. 아래 메서드는 앞서 만든 페이지 태깅 레코드에서 두 열(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 배열이 일반적으로 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:

  • 출력은 { page, value } 객체의 평탄한 배열이며, 어떤 다운스트림 서비스에서도 쉽게 역직렬화할 수 있습니다.
  • Escape는 별도의 직렬화 라이브러리를 사용하지 않고도 JSON 유효성을 유지합니다.

Helper: Escape

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

Comparing Methods: When to Use Each

Method Best For Key Advantages Limitations
Whole‑Document Extraction 전체 문서에 코멘트가 있는지 빠르게 확인 한 번 호출, 가장 간단한 코드 페이지 정보가 없음
Per‑Page Extraction 피드백을 정확한 섹션에 라우팅 페이지 태깅 결과, 바로 내보내기 가능 페이지당 추가 호출 필요
Combined Text + Annotations 하나의 읽기 가능한 전사본 필요 문서를 두 번 읽을 필요 없음 코멘트가 본문 텍스트와 섞여 있음
CSV Export 스프레드시트 기반 리뷰 트래킹 Excel에서 바로 열 수 있음, 인간 친화적 평탄 구조에 한정
JSON Export 자동화 파이프라인, 티켓팅 시스템 구조화되고 기계 친화적 약간 더 큰 페이로드

먼저 전체 문서 추출로 파일에 코멘트가 있는지 확인하고, 섹션별 라우팅이 필요하면 페이지별 추출로 전환하세요.

Best Practices and Tips

  • Parser를 즉시 Dispose: using 블록을 사용해 네이티브 리소스를 해제합니다.
  • null과 empty 구분: GetAnnotationsnull을 반환하면 형식이 지원되지 않는 것이고, 빈 컬렉션은 주석이 없다는 의미입니다.
  • 배치 작업에서 Features.Annotations 확인: 루프 내부에서 null 체크에 의존하기보다 파일을 일찍 건너뛰세요.
  • 페이지 태깅 리스트 재사용: ExtractAnnotationsByPage로 한 번 만들고 CSV와 JSON 내보내기에 동일 데이터를 사용해 두 출력이 일치하도록 합니다.
  • 보안: 주석 텍스트는 자유 형식 입력이므로 UI나 보고서에 렌더링하기 전에 반드시 신뢰할 수 없는 문자열로 처리하세요.

Conclusion

GroupDocs.Parser를 사용하면 PDF에서 리뷰어 코멘트를 수동으로 찾는 대신 프로그래밍 방식으로 직접 추출할 수 있습니다. 전체 문서, 페이지별, 혹은 텍스트와 결합된 형태로 주석을 추출함으로써 문서가 파이프라인에 들어오는 순간 피드백을 바로 활용할 수 있습니다. 결과를 CSV 또는 JSON으로 내보내어 팀이 이미 사용 중인 도구와 바로 연동하세요.

Next steps:

Additional Resources