간단한 PDF 검증 도구(validator)를 구현한다고 가정해 봅시다. PDF를 열고 파일 끝으로 이동하여 startxref 태그를 찾고 오프셋 주소를 읽어 들인 후, xref 키워드 위치로 점프해 그 하단의 고정 너비 크로스 레퍼런스(cross-reference) 테이블에 도착하기를 기대합니다. 이 테이블을 스캔해 개별 객체들의 물리 오프셋 위치를 추출하고, 다시 역방향 스캔을 통해 trailer 딕셔너리에서 /Root 및 /Size 정보를 획득하는 단순한 구조입니다. 이 검증기는 로컬에서 직접 테스트 생성한 문서들에 대해서는 완벽하게 작동할 것입니다. 하지만 최신 MS Word에서 내보내거나 PDF 1.5 사양을 지원하는 컴포넌트가 생성한 상용 문서가 유입되는 순간 오류를 감지했다고 판단하게 됩니다. 오프셋이 가리키는 위치에는 xref 키워드가 전혀 보이지 않고 trailer 딕셔너리 역시 문서 구조 어디에서도 조회되지 않으며, 빌드된 오브젝트 매핑 테이블은 거의 텅 비어 있습니다. 파일 자체는 완벽하게 규격에 맞는 정상 문서이지만, 검증기가 15년 전의 구형 설계 필터로 파일을 읽고 있는 것입니다.
이는 기존의 파일 구조를 전제로 문자열 바이트 수준 검색을 수행하는 구형 검증기가 최신 PDF 문서에서 오동작하는 가장 빈번한 원인입니다. 검증기가 의존하고 있었던 플레인텍스트 형태의 크로스 레퍼런스 테이블과 trailer 키워드는 PDF 1.5 규격부터 선택 사항(optional)으로 전환되었으며, 최신 문서에서는 대부분 제외되어 저장됩니다. 이 두 요소를 대신해 크로스 레퍼런스 스트림(cross-reference stream)과 압축된 오브젝트 스트림(compressed object stream)이 유입되었습니다. 두 구조 모두 ISO 32000-1에 정의되어 있으며, 이 사양을 식별하지 못하는 검증기는 정상 파일을 손상되어 객체 정보가 유실된 불량 파일로 오판하게 됩니다.
PDF 1.5에서 파일 테일(tail)과 관련해 변경된 점
ISO 32000-1 §7.5.8은 크로스 레퍼런스 스트림을 규정하며, §7.5.7은 /ObjStm 유형의 오브젝트 스트림 구조를 규정합니다. 두 신형 구조가 통합되면서 라이터 모듈은 기존 분석기가 의존했던 문자열 표식들을 모두 생략할 수 있게 되었습니다. PDF 1.5 이후 파일들에는 xref 키워드와 연계된 테이블 표식이 아예 존재하지 않을 수 있습니다. 대신 startxref 주소가 일반 스트림 객체를 가리키며 해당 스트림 헤더 딕셔너리에 /Type /XRef가 명시되고 크로스 레퍼런스 주소 데이터는 이진 바이너리 스트림으로 압축되어 보관됩니다. 또한 trailer 문자열 역시 생략되는데, 스트림 자체의 헤더 딕셔너리가 트레일러의 역할을 직접 수행하기 때문입니다. 즉, 구형 파서가 찾던 /Root, /Size, /ID 키 값들은 이제 해당 스트림 딕셔너리 내부에 보관됩니다.
두 번째 변화는 개별 객체 데이터들의 물리 저장소 구조와 연관됩니다. 각 간접 참조 객체(indirect object)들을 개별 바이트 오프셋 위치에 매번 기록하는 비효율을 방지하기 위해, 라이터는 다수의 소형 오브젝트(페이지 딕셔너리, 주석 정보, 구조 트리 레이아웃 등)를 단일 오브젝트 스트림 용기에 일괄 패키징하고 전체를 Flate로 압축해 저장합니다. 즉, 개별 객체들은 파일 전체 좌표계 기준으로 독자적인 바이트 오프셋 주소를 갖지 않고, 압축된 블롭(blob) 내부의 상대 오프셋만을 갖게 됩니다. 바이트 수준의 파일 검사기가 1 0 obj 같은 텍스트 표식을 검색하더라도 압축을 해제(inflation)하기 전에는 텍스트가 노출되지 않으므로 탐색에 실패합니다. 구형 검증 필터 관점에서는 문서 정보의 절반이 통째로 유실된 것처럼 보이게 됩니다.
압축 파일에서도 평문 텍스트인 트레일러 키
다행스러운 점은 크로스 레퍼런스 스트림의 트레일러 속성을 읽기 위해 스트림 전체의 압축을 해제할 필요는 없다는 점입니다. 스트림 객체는 헤더 딕셔너리가 먼저 출력된 후 stream 키워드가 뒤따르고 이어서 압축 바이너리 데이터가 배치되는 순서를 따릅니다. 이 헤더 딕셔너리 부분은 일반 평문 텍스트 상태로 파일에 노출됩니다. 따라서 startxref 주소가 크로스 레퍼런스 스트림을 가리키는 경우, 오브젝트 인덱스 정보 바로 뒤에 배치되는 딕셔너리 평문을 분석하여 stream 키워드와 압축 바이트 영역이 시작되기 전에 /Root, /Size, /ID 속성 값을 손쉽게 해독해낼 수 있습니다.
즉 검증 모듈은 단지 스트림 헤더 딕셔너리 영역을 파싱함으로써 가장 핵심이 되는 세 가지 메타 정보: 카탈로그 루트 오프셋, 총 선언 오브젝트 수, 파일 고유 식별자(ID) 정보를 가볍게 획득할 수 있습니다. 크로스 레퍼런스 내부 바이너리 정보를 복잡하게 디코딩하거나 압축을 해제하지 않아도 작동합니다. 구형 파서가 막히는 진짜 지점은 트레일러 정보를 읽는 과정이 아니라 개별 오브젝트를 물리적으로 추적하는 영역입니다. 이들은 별개의 가벼운 과제이며 첫 번째 기초 정보 해독은 매우 저비용으로 해결됩니다.
객체 스트림: 헤더와 Flate 블롭
오브젝트 스트림은 데이터를 묶는 용기입니다. 딕셔너리에 /Type /ObjStm 지시어가 명시되며, 내포된 객체의 수를 명시하는 /N 속성과 압축 해제 데이터 내에서 실제 첫 번째 객체 바디의 시작 위치를 알리는 가로 오프셋 /First 정보가 들어갑니다. 압축을 해제하면, 데이터 최상단에 /N개의 정수 쌍(integer pair)으로 구성된 소형 헤더가 배치됩니다. 각 정수 쌍은 객체 번호와 /First 지점 기준의 해당 객체 상대 오프셋으로 매핑됩니다. 헤더 뒤에는 개별 객체 바디 데이터들이 연속해서 연결됩니다.
압축을 해제(inflate)하고 나면 이들을 추출해 구조화하는 작업은 단순하고 명확합니다. 헤더 딕셔너리에서 /N 및 /First 속성을 파싱하고 Flate 디코더를 사용해 스트림 데이터의 압축을 푼 뒤, 최상단의 /N개 쌍을 스캔해 각 오브젝트 인덱스의 오프셋 주소를 확인하고 각 객체의 바디를 개별 간접 객체처럼 추출합니다. 유일한 종속 라이브러리는 Flate 디코더인데, 이미 개발 환경에 포함되어 있습니다: Delphi는 System.ZLib 유닛을 지원하고 Free Pascal은 zstream 유닛을 제공하므로 추가 타사 모듈 없이도 원시 Flate 압축 데이터를 풀 수 있습니다. 추출된 객체들을 검증기의 객체 테이블에 연동해 두면, /Root를 분석하고 페이지 트리를 검사하는 나머지 검증 단계들은 기존의 파일 구조를 처리할 때와 동일하게 물 흐르듯 실행됩니다.
직접 구현할 필요가 없는 부분
구현 복잡도를 필요 이상으로 높여 생각하기 쉽습니다. 압축 파일 내의 트레일러 정보를 읽는 과정에서 크로스 레퍼런스 스트림의 복잡한 이진 바이너리 데이터까지 완벽하게 디코딩할 필요는 없습니다. §7.5.8 크로스 레퍼런스 스트림은 세 가지 엔트리 유형을 적용하며, '해당 오브젝트가 오브젝트 스트림 N의 인덱스 i 내부에 존재한다'는 유형 2(type 2) 엔트리는 전체 물리 좌표 맵을 설계할 때 해독을 필요로 합니다. 특정 번호의 무작위 객체를 개별 조회하려면 이 물리 맵이 필요하지만, 일반 텍스트 상태인 딕셔너리 헤더에서 /Root, /Size, /ID 속성만 조회하는 경우에는 이를 디코딩하지 않아도 무방합니다. 또한 /ObjStm 객체들은 헤더 딕셔너리의 /N 및 /First 속성을 통해 저장 내용을 스스로 알려주므로, 이를 위해 복잡한 분석식을 구현할 필요도 없습니다.
단순히 트레일러 정보만을 얻고자 하는 경우, 크로스 레퍼런스 스트림에 적용되어 있을 수 있는 PNG/TIFF 프리딕터(predictor) 연산도 처리할 필요가 없습니다. 프리딕터 지시어는 이진 바이너리 크로스 레퍼런스 배열의 압축 정밀도를 높이기 위한 필터이며, 스트림 헤더 딕셔너리 해독과는 무관합니다. 따라서 기존 검증기를 최신 PDF 사양과 호환되도록 상향 조정하는 최소 변경 범위는 매우 명확합니다: 즉 startxref가 xref 키워드 대신 스트림 객체를 가리킬 때 헤더 딕셔너리를 파싱해 트레일러 키 정보를 추출하고, 검사 경로에 있는 /ObjStm 오브젝트들의 압축을 해제하여 객체 테이블에 연동해 주는 것입니다. 유형 2 엔트리 해독 및 프리딕터 처리는 더 고차원적인 작업이므로 실제 무작위 조회 모듈을 설계해야 할 때 구현을 미룰 수 있습니다.
표준 준수 여부 검사 시 스트림을 먼저 해제해야 하는 이유
표준 프로파일 검사를 실행하는 즉시 이러한 구조적 요구 사항이 체감됩니다. PDF/A 또는 PDF/X 검증 도구는 다음과 같은 고유 객체 정보를 순회 검사합니다: /OutputIntents 배열을 포함하는 문서 카탈로그 정보, 특정 식별자가 있는 XMP 메타데이터 스트림, 폰트 파일 임베딩을 선언한 각 글꼴 디스크립터(font descriptor), 그리고 트레일러의 /ID 속성 정보입니다. 압축된 최신 파일에서 이러한 핵심 객체들의 대부분은 오브젝트 스트림 묶음 내에 압축되어 보관됩니다. 검증기가 오브젝트 스트림을 해제하지 않고 스캔하면 카탈로그 속성, 메타데이터 구조, 폰트 구성 목록 등을 조회해낼 수 없습니다. 결국 정상적으로 사양을 준수해 제작된 파일인데도 출력 인텐트 누락, XMP 누락, 문서 구조 파손 등의 치명적인 오판 경고를 발생시키게 됩니다. 필요한 검증 근거들이 아직 Flate 압축 데이터 블롭 내에 잠겨 있기 때문입니다.
순서가 결정적인 차이를 만듭니다. 압축 해제는 본문 순회 검사가 진행되기 전에 선행되어야 합니다. 모든 검증 로직은 식별 번호를 기준으로 오브젝트 데이터에 즉각 접근할 수 있음을 전제로 작동하기 때문입니다. 압축 해제 없이 파일 바이트 스트림을 직접 검사하면 크로스 레퍼런스 스트림을 정상적으로 출력해내는 최신 검증 툴킷이 만든 양질의 문서임에도 분석 실패 오류를 내는 악순환이 생깁니다.
Letting PDFium do the parsing for you
PDFium 컴포넌트는 문서를 로드하는 과정에서 크로스 레퍼런스 스트림 및 오브젝트 스트림의 압축 해제 작업을 직접 전담하므로, 수동으로 압축 해제 및 디코딩 필터를 구현할 필요가 없습니다. TPdf 컴포넌트로 문서를 로드하면 /ObjStm 묶음 내의 모든 개별 객체들이 이미 메모리상에 압축 해제되어 배치되므로, 검증 모듈은 평문 파일처럼 정상적으로 데이터를 스캔할 수 있습니다. ValidatePdfA는 pac1b 또는 pacNone 등의 준수 등급을 명시하는 Conformance 속성과 발견된 에러 사항들을 보관하는 Issues 집합, 그리고 에러 리스트가 비어 있고 준수 등급이 식별되었을 때 True를 반환하는 IsCompliant 메서드를 포함하는 TPdfAValidationResult 레코드를 반환합니다. 로드 시점에 모든 스트림 압축이 해제되므로 스트림 내부에 압축 보관되어 있었던 /OutputIntents 배열이나 폰트 임베딩 정보가 유실 오류 없이 정상 탐지됩니다.
uses
PDFium, FPdfPdfa;
function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
이와 동일한 형태의 처리가 ValidatePdfX 메서드에도 적용되어 동일한 형태의 TPdfXValidationResult 레코드를 반환합니다. PDFium 컴포넌트를 연동해 얻는 구조적 강점은, 압축 해제 및 디코딩 연산이 문서 로더(loader) 단계에서 단 한 번 안전하게 완결되므로 개발자 검증 코드 수준에서는 구형 문서와 신형 압축 문서의 구조 차이를 전혀 신경 쓸 필요가 없다는 점입니다. 두 경우 모두 완전히 디코딩된 객체 맵 상태로 검증 모듈에 전달됩니다.
var
Pdf: TPdf;
R : TPdfXValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Press_Ready.pdf';
Pdf.Active := True;
R := Pdf.ValidatePdfX;
if R.IsCompliant then
Writeln('PDF/X conformance: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
문서 데이터가 파일이 아닌 메모리 바이트 스트림 형태로 로드되어 있는 경우에도 LoadDocument(const Data: TBytes) 오버로드 메서드를 통해 동일하게 크로스 레퍼런스 및 오브젝트 스트림 파싱을 구동할 수 있습니다. 자체 검증 모듈을 직접 구현할 때 명심해야 할 점은 특정 API 사용법보다 파일의 구조적 규칙을 이해하는 것입니다: 즉 스트림 딕셔너리 헤더 평문에서 트레일러 정보를 파싱하고, 본문 조사를 실행하기 전에 Flate 디코더를 이용해 /ObjStm 묶음 압축을 선행 해제하며, 이진 크로스 레퍼런스 데이터의 완벽한 디코딩 작업은 필요에 따라 수동 구현하거나 유예하는 것입니다.
구조가 메모리상에 해제되면 검증기는 이를 연계해 다양한 워크플로를 구동할 수 있습니다. 특정 폴더 내의 무수한 입력 문서를 한 번에 검증하여 CLI 리포트를 출력하는 자동 프리플라이트 배치 빌드 방식은 배치 프리플라이트 리포트 CLI 빌드 연습 가이드에서 상세 다루며, 대용량 파일을 개별 사본 문서로 쪼개기 전 유효성 사전 진단 게이트를 구현하는 방식은 PDF 문서를 복수 파일로 해체 분할하는 가이드를 통해 연계 설계를 확인할 수 있습니다. 두 기능 모두 Delphi 및 C++Builder용 PDFium 컴포넌트에서 지원하는 로드 및 검증 API를 기반으로 구현됩니다.