💡 전체 작업 예제는 GitHub에서 확인할 수 있습니다:
extract-annotations-from-pdf-using-groupdocs-parser-dotnet
소개
검토를 거친 PDF는 보이는 텍스트 외에도 스티키 노트, 강조된 메모, 검토자가 남긴 인라인 댓글 등을 포함하고 있습니다. 페이지를 일일이 스크롤하면서 이러한 주석을 찾는 일은 문서가 여러 차례 피드백을 받은 후에는 비효율적입니다. GroupDocs.Parser는 문서에 내장된 주석을 프로그래밍 방식으로 읽어, 흩어져 있는 검토자 댓글을 코드에서 활용할 수 있는 구조화된 데이터로 변환해 주는 .NET 라이브러리입니다. 이 튜토리얼에서는 전체 PDF에서 주석을 추출하고, 페이지별로 나누어 가져오며, 문서 텍스트와 함께 추출하고, 결과를 CSV 또는 JSON으로 내보내는 방법을 보여줍니다.
문서 팀을 위한 리뷰 트래커를 만들던 중 이 문제에 직면했습니다. 40페이지 분량의 릴리스 노트가 세 명의 검토자를 거쳤는데, 파일을 열어 모든 댓글을 찾는 데 걸리는 시간이 실제 문제를 해결하는 시간보다 더 길었습니다. 몇 줄의 코드만으로 주석을 추출하면 두 분 안에 작업을 마칠 수 있었습니다.
다음 섹션에서는 다음을 배웁니다:
- 한 번에 PDF 전체에서 모든 주석을 추출하는 방법
- 각 주석에 해당 페이지 번호를 태깅하는 방법
- 문서 텍스트와 주석 텍스트를 한 번에 읽어오는 방법
- 결과를 CSV 또는 JSON으로 직렬화하여 다운스트림 도구에 전달하는 방법
PDF 주석 추출이 중요한 이유
프로그래밍 방식으로 PDF 주석을 읽는 것이 유용한 경우:
- 리뷰 워크플로: PDF 뷰어를 열지 않고도 모든 검토자 댓글을 수집합니다.
- 협업: 강조된 부분이나 메모를 자체 도구에 바로 표시합니다.
- 감사: 문서가 플래튼되거나 최종화된 후에도 시간 경과에 따른 마크업 기록을 보관합니다.
GroupDocs.Parser는 버전 26.7부터 PDF 문서에 대한 네이티브 주석 추출을 GetAnnotations 메서드(GetAnnotations)와 함께 제공하며, TextOptions의 새로운 IncludeAnnotations 옵션을 통해 주석 텍스트를 일반 텍스트 읽기에 포함시킬 수 있습니다.
전제 조건
- .NET 6.0 이상
- GroupDocs.Parser for .NET 26.7+ (temporary license)
- 기존 주석이 포함된 PDF 파일 (예:
document-with-annotations.pdf)
NuGet을 통해 설치:
dotnet add package GroupDocs.Parser
PDF 문서에서 주석을 추출하려면 어떻게 해야 하나요?
답변: Parser로 파일을 로드한 뒤 전체 문서에 대해 GetAnnotations()를 호출하거나, 단일 페이지에 대해 GetAnnotations(pageIndex)를 호출합니다. 각 결과는 AnnotationItem 객체 컬렉션이며, Value 속성에 댓글 텍스트가 들어 있습니다. 주석을 문서의 일반 콘텐츠와 함께 인라인으로 보고 싶다면 TextOptions에 IncludeAnnotations를 설정하고 GetText를 호출하면 됩니다.
전체 문서 추출
다음 스니펫은 파일에서 모든 주석을 한 번에 추출합니다. 이는 문서에 댓글이 있는지 빠르게 확인할 수 있는 가장 빠른 방법입니다.
// 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;
핵심 포인트:
GetAnnotations()는 해당 형식이 주석 추출을 지원하지 않을 경우null을 반환하고, 주석이 전혀 없을 경우 빈 컬렉션을 반환합니다.- 각
AnnotationItem은Value속성을 통해 텍스트를 제공하며, 현재 SDK가 보고하는 유일한 데이터 포인트입니다. - 여기서는 페이지 정보가 포함되지 않으므로, 페이지별 정보가 필요하면 아래의 페이지별 오버로드를 사용하세요.
페이지별 추출
댓글 위치가 중요한 경우, 문서 페이지를 순회하면서 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;
핵심 포인트:
GetDocumentInfo().PageCount가 루프를 제어합니다. 별도의 “주석 페이지 수”는 없습니다.GetAnnotations(pageIndex)는 0부터 시작하는 인덱스를 사용하며, API의 다른 페이지 레벨 메서드와 일치합니다.- 결과
AnnotationRecord리스트는 CSV 또는 JSON 내보내기에 바로 사용할 수 있는 형태입니다.
주석과 함께 텍스트 추출하기
문서를 두 번 읽는 대신, 주석 텍스트를 일반 텍스트 추출 결과에 바로 포함시킬 수 있습니다.
// 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;
}
}
핵심 포인트:
IncludeAnnotations는TextOptions의 속성이므로, 일반 텍스트 추출에 사용하던GetText호출에 그대로 적용할 수 있습니다.- 별도의 댓글 목록이 아니라 하나의 전사(transcript) 형태 출력이 필요할 때 유용합니다.
- 특정 페이지에만 적용하려면
GetText(pageIndex, options)와 결합하면 됩니다.
먼저 주석 지원 여부 확인하기
모든 형식이 주석을 지원하는 것은 아니므로, GetAnnotations 로직을 구현하기 전에 확인하는 것이 좋습니다.
// Returns true if the loaded document format supports annotation extraction
using (var parser = new Parser(path))
{
return parser.Features.Annotations;
}
핵심 포인트:
Features.Annotations는Parser인스턴스에 있는 간단한 불리언 플래그입니다.- 사전에 확인하면 의도를 명확히 할 수 있으며,
GetAnnotations가null을 반환하면서도 정상적으로 실패합니다.
주석을 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());
핵심 포인트:
CsvEscape는 쉼표, 따옴표, 줄 바꿈이 포함된 필드를 안전하게 인용합니다.- 결과 파일은 바로 Excel에서 열 수 있으며 티켓팅 도구에 파이프라인으로 전달할 수도 있습니다.
Helper: CsvEscape
if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;
주석을 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());
핵심 포인트:
- 출력은
{ page, value }객체의 평면 배열이며, 어떤 다운스트림 서비스에서도 쉽게 역직렬화할 수 있습니다. Escape는 별도의 직렬화 라이브러리를 사용하지 않고도 유효한 JSON을 유지합니다.
Helper: Escape
return s?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? string.Empty;
방법 비교: 언제 어떤 방법을 사용해야 할까
| 방법 | 가장 적합한 상황 | 주요 장점 | 제한 사항 |
|---|---|---|---|
| 전체 문서 추출 | “댓글이 있는지 빠르게 확인” | 한 번 호출, 가장 간단한 코드 | 페이지 정보가 없음 |
| 페이지별 추출 | 피드백을 정확한 섹션에 라우팅 | 페이지 태깅 결과, 바로 내보내기 가능 | 페이지당 추가 호출 필요 |
| 텍스트 + 주석 결합 | 하나의 읽기 가능한 전사 필요 | 문서를 두 번 읽을 필요 없음 | 댓글이 본문 텍스트와 구분되지 않음 |
| CSV 내보내기 | 스프레드시트 기반 리뷰 트래킹 | Excel에서 바로 열 수 있음, 인간 친화적 | 평면 구조에 제한 |
| JSON 내보내기 | 자동화 파이프라인, 티켓 시스템 | 구조화되고 기계 친화적 | 약간 더 큰 페이로드 |
먼저 전체 문서 추출로 파일에 댓글이 있는지 확인하고, 특정 섹션에 피드백을 연결해야 할 때는 페이지별 추출로 전환하세요.
모범 사례 및 팁
Parser를 즉시 해제:using블록으로 감싸 네이티브 리소스를 해제합니다.null과 빈 컬렉션 구분:GetAnnotations가null을 반환하면 형식이 지원되지 않는 것이고, 빈 컬렉션은 주석이 없다는 의미입니다.- 배치 작업에서
Features.Annotations확인: 루프 내부에서null을 검사하기보다 파일을 일찍 건너뛰는 것이 효율적입니다. - 페이지 태깅 리스트 재사용:
ExtractAnnotationsByPage로 한 번 만들고 CSV와 JSON 내보내기에 동일 데이터를 사용하면 두 출력이 일치합니다. - 보안: 주석 텍스트는 자유 형식 입력이므로 UI나 보고서에 렌더링하기 전에 반드시 신뢰할 수 없는 문자열로 처리하세요.
결론
GroupDocs.Parser를 사용하면 PDF에서 검토자 댓글을 수동으로 찾는 대신 프로그래밍 방식으로 직접 추출할 수 있습니다. 전체 문서, 페이지별, 혹은 텍스트와 결합된 형태로 주석을 추출함으로써, 문서가 파이프라인에 들어오는 순간 바로 피드백을 표시하는 리뷰 워크플로를 구축할 수 있습니다. 결과를 CSV 또는 JSON으로 내보내어 팀이 이미 사용하고 있는 도구와 바로 연동하세요.
다음 단계:
- 전체 메서드 시그니처와 오버로드를 확인하려면 GetAnnotations API reference를 살펴보세요.
- 주석과 함께 PDF 문서에서 텍스트를 추출하는 방법은 extract text from PDF documents를 참고하세요.
- 배치 처리 시나리오용 샘플 프로젝트는 GitHub의 (Examples Repo)에서 확인할 수 있습니다.