기술 문서

PDFium으로 Delphi에서 PDF 페이지의 표 추출하기

PDFium Component는 PDF 페이지 위의 표를 감지하고, 한 페이지에는 ExtractTables를, 전체 문서에는 ExtractDocumentTables를 사용해 행과 열 스팬, 머리글 행, 신뢰도 값을 가진 셀 격자로 반환합니다. 각 표는 호출 한 번으로 CSV나 JSON으로 변환되며, 페이지 나누기를 넘어 이어지는 표는 연속 체인으로 연결될 수 있습니다

PDF에는 표 객체가 없습니다. PDF 안의 표란 사람이 격자로 읽도록 배치된 텍스트 실행의 집합일 뿐이며, 때로는 그 주위에 선이 그려져 있고 때로는 그렇지 않습니다. 격자를 복원한다는 것은 파일이 한 번도 기록한 적 없는 의도를 재구성하는 것이므로, 모든 추출 도구가 조금씩 다른 결과를 내며, 자신의 신뢰도를 알려주는 도구가 그렇지 않은 도구보다 더 쓸모 있는 이유이기도 합니다

두 종류의 표를 위한 두 가지 감지 모드

선 기반 감지는 그려진 선을 사용합니다. 각 획이 그어진 경로 세그먼트는 페이지 객체의 행렬을 통해 페이지 좌표로 변환되고, 수평선과 수직선이 교차되며, 그 교차점들이 연결된 성분을 형성합니다. 각 성분은 자신만의 정렬된 X, Y 위치 격자가 되는데, 이것이 같은 페이지 위의 서로 다른 두 표가 하나의 말도 안 되는 격자로 합쳐지는 것을 막아 줍니다

공백 기반 감지는 선 대신 정렬로 그려진 표를 처리합니다. 단어 상자들은 시각적인 행으로 그룹화되고, 행 내부의 간격이 그 행을 후보 열로 나누며, 표는 최소 MinRows개의 행이 AlignmentTolerance 이내에서 최소 MinColumns개의 왼쪽 정렬 앵커를 반복할 때만 받아들여집니다. 행 간격 계수는 기본값이 3이며, 이는 12포인트 텍스트에서 일반적인 약 30포인트의 베이스라인 간격을 커버하면서도, 여러 텍스트 실행을 담은 한 줄이 표인 척하지 못하게 막습니다

Delphi에서의 PDFium Component 표 감지 파이프라인: 선의 교차점과 공백으로 정렬된 단어 행이 CSV와 JSON 내보내기를 갖춘 점수화된 하나의 표 레코드로 흘러들어갑니다
선 기반 감지는 그려진 획을 교차시키고 공백 기반 감지는 정렬된 단어 상자 행을 세며, MinRows와 MinColumns를 통과한 후보는 신뢰도 점수와 DetectionMode를 달고 나타납니다
uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'annual-report.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 12;                    // 1부터 시작

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // 기본값은 0.5
    Options.HeaderRowCount := 1;

    Tables := Pdf.ExtractTables(Options);
    for I := 0 to High(Tables) do
      Writeln(Format('table %d: %dx%d cells, confidence %.2f, mode %d',
        [I, Tables[I].RowCount, Tables[I].ColumnCount,
         Tables[I].Confidence, Ord(Tables[I].DetectionMode)]));

    if Length(Tables) > 0 then
      SaveText('page12-table0.csv', Tables[0].ToCsv);
  finally
    Pdf.Free;
  end;
end;

병합된 셀은 어떻게 복원되는가?

이 부분이 순진한 추출기가 틀리는 지점입니다. 병합된 셀은 전역 격자만으로는 식별할 수 없는데, 그 격자는 페이지의 모든 선으로부터 파생되며 병합된 영역에는 그 영역을 갈랐을 내부 선이 애초에 없기 때문입니다

여기서 사용하는 규칙은 지역적입니다: 인접한 두 기본 셀 사이의 구간을 덮는 경계선이 없으면 그 둘은 병합된 것입니다. 유니온-파인드가 이들을 결합하고, 그 결과로 만들어진 사각형 성분이 RowSpanColumnSpan 값이 되며, 텍스트는 그 중심점을 기준으로 기본 셀에 할당된 다음 그 셀을 따라 병합 루트로 이동합니다. 이렇게 하면 모든 단어를 모든 셀과 대조하는 이차 시간 스캔 대신, 단어 수와 셀 수에 선형적인 비용만 듭니다

Delphi를 위한 PDFium 표 추출에서의 병합 셀 복원 다이어그램: 인접한 두 기본 셀 사이의 구간을 덮는 경계선이 없을 때마다 유니온-파인드가 이들을 결합하여 RowSpan과 ColumnSpan을 만들어냅니다
유니온-파인드는 공유 구간에 그려진 경계가 없는 인접 기본 셀들을 병합하므로, 병합된 머리글은 채워진 셀 하나와 그 옆의 알 수 없는 빈 셀들이 아니라 ColumnSpan이 설정된 셀 하나로 반환됩니다

실무적인 효과는, 세 개의 열에 걸쳐 병합된 'Total' 머리글이 있는 재무 표가 값이 채워진 셀 하나와 알 수 없는 빈 셀 두 개가 아니라, 스팬이 3인 셀 하나로 나온다는 것입니다

페이지를 넘나드는 연속

긴 표는 페이지를 넘어 끊기며, 각 페이지의 조각을 독립된 표로 취급하면 호출자가 이를 직접 이어 붙여야 합니다. ExtractDocumentTables는 이를 대신 연결할 수 있지만, 엄격한 조건 아래에서만 가능합니다: 조각은 이전 페이지에서 가장 아래에 있는 표여야 하고, 다음 조각은 이어지는 페이지에서 가장 위에 있는 표여야 하며, 페이지 번호는 인접해야 하고, 열 경계가 일치해야 합니다

이 네 가지 조건이 모두 함께 있어야만 명백한 오류를 막을 수 있습니다. 그 오류란, 우연히 같은 열 개수를 공유한다는 이유만으로 문서 안의 모든 4열짜리 표를 하나의 가상 초대형 표로 이어붙이는 것입니다. 조건이 모두 성립하면 표들은 연속 그룹 식별자를 공유하고 연속 메타데이터를 갖게 되며, 성립하지 않으면 별도의 표로 남아 사용자가 직접 판단할 수 있습니다

Delphi에서 PDF 페이지를 넘나드는 표 연속 다이어그램: 네 가지 엄격한 게이트가 한 페이지의 가장 아래 조각이 다음 페이지의 가장 위 조각과 합쳐질지를 결정합니다
문서 수준 추출은 네 가지 게이트가 모두 성립할 때만 조각을 연결하므로, 서로 무관한 4열짜리 표들이 하나의 가상 초대형 표로 합쳐지지 않습니다

문서 수준 추출은 MaxCellsMaxTables 예산을 페이지마다 다시 설정하는 대신 페이지 전반에 걸쳐 공유하며, finally 블록에서 활성 페이지를 복원하므로, 뷰어에서 실행된 추출은 사용자가 원래 보고 있던 페이지에 그대로 머물게 합니다

데이터를 손상시키지 않고 내보내기

두 익스포터 모두 이스케이프에 신중합니다. CSV는 항상 필드를 따옴표로 감싸고 내부의 따옴표를 이중으로 처리하는데, 이는 쉼표를 담은 셀이 조용히 두 열로 갈라지는 전형적인 실패를 막아 줍니다. 병합된 셀의 경우 내용은 왼쪽 위 앵커에서만 출력되므로, CSV 왕복이 걸쳐 있는 머리글을 그 머리글이 덮는 여러 열에 걸쳐 중복시키지 않습니다

JSON은 유니코드를 ASCII로 이스케이프하는 대신 그대로 보존하고, 제어 문자는 이스케이프하며, 소비자가 품질을 판단하는 데 필요한 메타데이터 — 감지 모드, 신뢰도, 경계, 스팬 값, 머리글 플래그, 연속 정보 — 를 포함합니다. 추출된 표를 다운스트림 시스템에 공급한다면 JSON을 선호하세요. CSV 행은 자신이 나온 표가 0.51의 신뢰도를 받았다는 것을 알려줄 수 없기 때문입니다:

// 문서 전체 추출, 신뢰할 만한 표만 남김
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
begin
  if Tables[I].Confidence < 0.75 then
  begin
    Log(Format('page %d table needs review (%.2f)',
      [Tables[I].PageNumber, Tables[I].Confidence]));
    Continue;
  end;
  if Tables[I].ContinuationGroup > 0 then
    AppendToGroup(Tables[I].ContinuationGroup, Tables[I].ToJson)
  else
    EmitStandalone(Tables[I].ToJson);
end;

튜닝, 그리고 언제 멈춰야 하는지 알기

세 가지 설정이 나머지보다 중요합니다. MinConfidence는 품질 게이트이며, 0.5는 의도적으로 관대한 값입니다. 자동화된 수집에는 이를 높이고, 사람이 각 결과를 확인하는 검토 UI에는 낮추세요. MinColumnGap은 공백 모드에서 무엇을 열 경계로 볼지 결정하며, 밀도 높은 보고서 안에 촘촘하게 배치된 표라면 기본값인 12포인트에서 줄여야 할 수도 있습니다. MaxRowGapFactor는 수직 거리가 언제 표를 끝내는지를 결정하며, 가끔 빈 행이 있는 표에서 중요합니다

한계에 대해서는 솔직해야 합니다. 선이 그어진 표는 안정적으로 추출됩니다. 깔끔하게 정렬된 공백 표도 잘 추출됩니다. 회전된 텍스트, 중첩된 표, 또는 내용이 줄바꿈되어 마치 다른 행처럼 보이는 셀이 있는 표는 매개변수를 어떻게 설정하든 검토가 필요합니다. 그런 경우를 위해, 구조화된 텍스트 모델은 도메인에 특화된 판독기를 만들 수 있는 원재료를 제공하며, 이는 구조화된 텍스트 블록과 읽기 순서에서 설명합니다

유용한 조합 하나: 스캔된 문서에 텍스트가 전혀 없다면, 텍스트 레이어가 존재하기 전까지는 표 감지가 다룰 재료가 없습니다. 스캔된 PDF에 검색 가능한 텍스트 레이어 추가하기에서 설명하는 대로 먼저 하나를 추가한 다음 추출하세요. OCR 제공자가 반환하는 단어 상자는 정확히 공백 기반 감지가 필요로 하는 입력입니다

표 추출, 구조화된 텍스트, 리플로우는 모두 Delphi, C++Builder, Lazarus에서 동일한 페이지 모델로부터 읽어옵니다. 전체 API는 Delphi용 PDFium Component 페이지에서 설명합니다