기술 문서

Delphi에서의 진정한 PDF 리댁션(Redaction): 삭제 및 위생화(Sanitize)

Delphi에서 실제 PDF 리댁션(Redaction, 민감 정보 마스킹 및 영구 삭제)을 수행한다는 것은 그 위에 그림을 덧칠하는 것이 아니라 하부의 바이트를 완전히 삭제하는 것을 의미합니다. losLab PDF Library는 두 가지 API로 이 경계를 나눕니다: RedactRegion은 페이지 콘텐츠 스트림을 편집하여 제거된 텍스트, 벡터 및 이미지를 물리적으로 완전히 지우고, SanitizeDocument는 리댁션된 정보가 여전히 숨어있을 수 있는 메타데이터, 스크립트 및 작업을 정리(위생화)합니다. 이 중 하나라도 잘못 구현하면 화면에서는 가려진 것처럼 보이지만, 텍스트를 복사하거나 문서 속성을 열어보는 사람에게 기밀 정보를 고스란히 유출하게 됩니다

이러한 유출 사고 사례는 언론 보도를 통해 널리 알려져 있습니다. 신문사, 법원, 정부 기관 등이 이름이나 숫자 위에 검은색 사각형을 올려놓은 PDF를 배포했으나, 독자가 그 아래의 텍스트를 선택하여 복사한 뒤 일반 텍스트 편집기에 붙여넣기하여 리댁션된 줄 아았던 문자열이 그대로 노출되는 사고가 발생하곤 했습니다. 사각형은 그저 그림에 불과했습니다. 텍스트는 페인트 레이어 바로 아래의 콘텐츠 스트림에 완전히 손상되지 않은 채 선택 가능한 상태로 여전히 존재하고 있었습니다. TPDFlib 클래스를 통해 제공되는 Delphi 및 C++Builder용 PDF 엔진인 losLab PDF Library는 이러한 구분을 사후 대책이 아닌 리댁션의 가장 핵심적인 설계 목표로 다룹니다

PDF 텍스트 위에 검은색 상자를 그리는 것이 왜 진정한 리댁션이 아닌가요?

losLab PDF Library는 워터마크, 시각적 블랙아웃, 증명 스탬프처럼 단순한 덮어쓰기가 실제로 필요한 경우를 위해 MaskRect를 제공합니다. MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue)는 페이지 콘텐츠 스트림에 사각형을 추가하고 re 경로 구성 연산자와 f 채우기 연산자(ISO 32000-1 §8.5)를 사용하여 그립니다. 사용자가 가린 이름의 글리프, 벡터 스트로크, 임베디드 이미지 등 기존에 페이지에 존재하던 모든 바이트는 동일한 스트림 내에서 단지 한 단계 이전 명령어로 그대로 남아 있습니다. 호환되는 뷰어는 검은색 상자를 가장 마지막에 그리므로 시각적으로는 가려진 것처럼 보이지만, 그 아래의 데이터는 아무것도 변하지 않습니다

누구든지 세 가지 방법으로 원래 콘텐츠를 검색할 수 있습니다: 불투명한 상자 너머로 텍스트를 드래그하여 복사하거나, 페이지에서 텍스트 추출 도구를 실행하거나, 콘텐츠 스트림에서 덮고 있는 사각형을 삭제하는 것입니다. MaskRect는 그 이름 그대로 단지 마스킹할 뿐 제거하지 않습니다. 이는 단어 위에 스티커를 붙여놓은 것과 같아서, 스티커를 떼어내기만 하면 원래 단어를 그대로 읽을 수 있습니다

RedactRegion이 명령어 수준에서 콘텐츠를 삭제하는 방법

RedactRegion(Page, Left, Top, Width, Height, Options)은 페인트 레이어 한 단계 아래에서 작동합니다. losLab PDF Library는 페이지의 각 콘텐츠 레이어를 TPDFContentProgram으로 파싱하고, CTM 추적 분석기(콘텐츠 모델의 FindInstructionsInRect)를 사용하여 명령어 목록을 순회하며, 지정한 사각형과 교차하는 영역을 그리는 모든 텍스트 표시, 경로 채우기 및 이미지 Do 연산자를 마킹합니다. 이 명령어들은 TPDFContentProgram.Delete를 통해 삭제되고, 편집된 레이어는 WritePageContentLayer를 통해 다시 기록됩니다. 바이트는 커버 뒤로 숨는 것이 아니라 스트림에서 영구히 삭제됩니다. 이후에 텍스트 추출기를 실행해도 추출할 수 있는 데이터가 전혀 남지 않습니다

기억해 두어야 할 두 가지 구현 세부 사항이 있습니다. RedactRegion은 현재 원점과 단위를 기준으로 좌측 상단 사각형 영역을 입력받아 콘텐츠 스트림이 사용하는 우측 하단(실제로는 좌측 하단) 사용자 공간 좌표로 내부적으로 변환하므로, 사용자는 페이지에 보이는 그대로 영역을 지정할 수 있습니다. 사각형 영역이 여러 명령어를 덮는 경우, RedactRegion은 인덱스의 내림차순으로 이를 삭제합니다: 7번 명령어보다 4번 명령어를 먼저 제거하면 이후의 모든 인덱스가 어긋나서 범위가 손상되기 때문입니다. Options = 0을 전달하면 losLab PDF Library는 지워진 영역 위에 시각적 표시로 단색 검은색 상자를 인쇄하지만, 이 표시는 화장용에 불과하며 실제 내부 콘텐츠는 이미 제거된 상태에서 적용됩니다

실제 환경에서 사각형 좌표를 하드코딩하는 경우는 드뭅니다. 대신 페이지에서 민감한 문자열을 검색하고 그 바운딩 박스를 읽어온 뒤 — Delphi에서 텍스트 및 페이지 요소 위치 찾기에서 다루는 페이지 검색 및 요소 열거 방식 사용 — 해당 사각형을 RedactRegion에 전달합니다:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', '') = 1 then
    begin
      // 1페이지의 220 x 14 pt 상자 안에 그려진 모든 것을 삭제합니다
      // 좌표는 현재 원점 및 단위를 기준으로 한 좌측 상단입니다
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);   // Options=0은 검은색 마커를 인쇄합니다
      PDF.SaveToFile('contract-redacted.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

PDF의 어느 곳에 리댁션된 정보가 여전히 숨어있을 수 있나요?

시각적인 레이어에서만 멈추는 리댁션은 흔적을 남깁니다. PDF는 독자가 보지 않는 위치에 정보를 저장하기 때문입니다. losLab PDF Library는 정보가 유출될 수 있는 채널들을 체계적으로 분류합니다. 문서 정보 딕셔너리(Document Information Dictionary)와 XMP 메타데이터 스트림은 작성자(Author), 생산기기(Producer) 또는 키워드(Keywords) 문자열을 정기적으로 전송하며, 여기에 본문에서 제거한 인물이나 사건명이 그대로 포함되어 있을 수 있습니다. 문서 레벨의 JavaScript와 카탈로그 OpenAction은 문서를 열 때 실행되어 이미 삭제한 줄 알았던 데이터에 도달할 수 있습니다. 모든 주석은 선택적인 /A 작업을 포함하며, 주석 자체(스티키 노트, 오래된 양식 필드, 링크 등)가 리댁션된 영역 바로 위에 존재하며 자체 텍스트 복사본을 유지할 수 있습니다. 암호화는 이 부분에 영향을 미치지 않으므로, 일반 텍스트 XMP 패킷을 포함하는 암호화된 파일은 여전히 검색엔진에 제목을 노출하게 되는데, 이는 암호화 및 권한 감사에 관한 설명에서 보안 진단이 필요한 취약점입니다. 방어적인 위생화(Sanitization) 작업은 단지 보고 있는 페이지만이 아니라 이러한 숨겨진 위치를 모두 찾아서 정리해야 합니다

SanitizeDocument로 문서 청소하기

losLab PDF Library는 이러한 광범위한 정보 유출 통로를 막기 위해 비트마스크로 선택된 독립적인 청소 작업을 순차적으로 실행하고 적용된 작업 개수를 반환하는 오케스트레이션 호출인 SanitizeDocument(Flags)를 제공합니다. 플래그들은 결합할 수 있습니다. SANITIZE_JAVASCRIPT는 모든 문서 수준 JavaScript 패킷과 OpenAction 스크립트를 제거합니다. SANITIZE_ACTIONS는 새로운 TPDFAnnots.RemoveAnnotAction을 통해 모든 주석에서 /A 작업 딕셔너리를 제거하고 카탈로그 OpenAction을 정리합니다. SANITIZE_METADATA는 문서 정보 딕셔너리(ISO 32000-1 §14.3.3)와 XMP 메타데이터 스트림(§14.3.2) 양쪽에서 작성자, 주제, 키워드, 생성 프로그램(Creator) 및 생산기기를 지웁니다. SANITIZE_ANNOTATIONS는 문서 전체에서 모든 주석을 제거하고, SANITIZE_OPEN_ACTION은 카탈로그 /OpenAction 엔트리를 삭제합니다. SANITIZE_ALL은 이 다섯 가지 플래그의 비트 연산 합집합입니다

이러한 청소 루틴 내부의 인덱스 규칙들은 개발자가 수작업으로 루프를 구현할 때 실수하기 쉬운 부분이며, 이는 SanitizeDocument가 단일 호출로 제공되는 주된 이유이기도 합니다. GlobalJavaScriptPackageName은 0부터 시작하고 RemoveGlobalJavaScript를 실행하면 리스트 크기가 동적으로 작아지므로, JavaScript 청소 루틴은 항상 패키지 인덱스 0을 읽어서 개수가 0이 될 때까지 계속 지워나가야 합니다. 반면 DeleteAnnotation은 1부터 시작합니다. AnnotationCount는 현재 선택된 페이지를 기준으로 값을 보고하므로, 주석 청소 루틴은 카운팅 전에 각 페이지를 차례로 먼저 선택해야 합니다. losLab PDF Library는 이러한 내부 인덱스 경계 처리를 모두 정확하게 수행하므로, 사용자는 다섯 가지의 서로 다른 반복 규칙을 복제해 넣는 대신 하나의 플래그 세트를 전달하고 실행 횟수만 보고받으면 됩니다

var
  Applied: Integer;
begin
  // 메타데이터, 스크립트 및 작업은 지우고 주석은 유지합니다:
  Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
    SANITIZE_ACTIONS or SANITIZE_OPEN_ACTION);

  // 또는 주석을 포함하여 모든 숨겨진 채널을 지웁니다:
  Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;

전체 리댁션 및 위생화 워크플로우

이 두 가지를 함께 배치하면 전체 절차는 매우 간단해집니다. losLab PDF Library는 페이지별로 RedactRegion을 사용해 시각적인 콘텐츠를 삭제하고, 파일이 디스크에 작성되기 전에 SanitizeDocument(SANITIZE_ALL)을 호출하여 모든 숨겨진 채널을 지웁니다. 리댁션은 대개 외부에서 전달되어 제어할 수 없는 임의의 문서를 대상으로 실행되므로, 손상된 입력 파일이 리댁션 중간에 크래시를 유발하지 않고 안전하게 예외 처리를 수행할 수 있도록 신뢰할 수 없는 PDF를 안전하게 파싱하기에 소개된 강화된 검증 경로를 통해 파일을 로드하는 것이 좋습니다

var
  PDF: TPDFlib;
  Applied: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('incoming.pdf', '') = 1 then
    begin
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);       // 민감한 라인을 제거합니다
      Applied := PDF.SanitizeDocument(SANITIZE_ALL);   // 메타데이터, JS, 작업, 주석을 제거합니다
      PDF.SaveToFile('published.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

콘텐츠 스트림 리댁션의 한계

RedactRegion은 콘텐츠 스트림에서 직접 동작하는데, 이는 원래 디지털로 생성된 텍스트가 위치하는 곳이며 스캔된 텍스트는 존재하지 않는 곳입니다. 페이지가 스캔된 이미지인 경우 글자는 이미지 XObject 내부의 픽셀일 뿐 텍스트 연산자가 아니므로, losLab PDF Library는 글리프를 삭제하듯이 래스터 데이터 중간에서 이름만 파내어 지울 수 없습니다. 라이브러리의 이미지 처리는 매우 보수적으로 설계되었습니다: 지정한 사각형 영역과 교차하는 모든 이미지는 전체가 삭제되므로, 스캔된 페이지에서 리댁션 박스를 적용하면 이미지 일부분이 아닌 전체 스캔 이미지가 지워집니다. 이미지 내부의 특정 영역을 리댁션하는 것은 픽셀 레벨에서 다시 래스터화하고 칠한 뒤 다시 임베딩하는 복잡한 작업을 의미하며, 이는 콘텐츠 스트림 수준의 데이터 삭제와는 완전히 다른 작업입니다. 스캔된 페이지 위에 얹어진 OCR 텍스트 레이어를 처리할 때도 주의해야 합니다: RedactRegion은 눈에 보이는 텍스트 연산자는 지울 수 있지만, 이미지 XObject 자체가 영역과 교차하여 완전히 지워지지 않는 한 그 아래의 원본 그림은 손상되지 않고 그대로 유지됩니다

정확한 처리를 돕는 두 가지 미세한 지점이 있습니다. 교차 영역 검사는 글리프의 정밀한 치수를 측정하는 대신 문자 수와 글꼴 크기에서 각 텍스트 런(Run)의 너비를 근사치로 도출합니다. 따라서 가변 폭 글꼴로 작성된 매우 짧은 단어는 타이트하게 그려진 사각형 영역을 벗어날 수 있으므로, 영역 박스에 약간의 마진을 부여하는 것이 좋습니다. 또한 삭제는 명령어 단위로 실행됩니다: RedactRegion은 텍스트 표시 연산자 전체를 제거하므로, 텍스트 런의 일부분만 사각형 영역에 걸쳐 있어도 동일한 연산자를 공유하는 인접 문자들까지 함께 지워질 수 있습니다. 이러한 한계가 메커니즘 자체를 신뢰하지 못할 이유는 아니며, 사각형 영역을 세심하게 획정하고 최종 결과를 꼼꼼히 확인해야 하는 근거가 됩니다

모든 리댁션 워크플로우를 완벽하게 마무리하는 원칙은 맹신이 아닌 교차 검증입니다. 영역을 리댁션하고, 문서를 위생화하고, 새 파일로 저장한 뒤 다시 열어서 제거한 텍스트와 비운 메타데이터가 실제로 조회되지 않는지 직접 추출하여 테스트해 보십시오: 양쪽 모두에서 아무 정보도 나오지 않는다면 리댁션이 완벽하게 완료된 것입니다. 여기에 언급된 RedactRegionSanitizeDocument API는 전체 콘텐츠 모델 및 위생화에 대한 상세 레퍼런스와 함께 Delphi 및 C++Builder용 losLab PDF Library와 함께 제공됩니다