기술 문서

Delphi의 Office 애플리케이션에서 하이브리드 참조 PDF 처리하기

Microsoft Word나 Excel에서 'PDF로 저장'을 사용하여 문서를 내보내면 디스크에 저장되는 파일은 대부분 하이브리드 참조 파일입니다. 이 파일은 상호 참조 정보를 두 번 포함합니다: 버전 1.4까지의 모든 PDF 끝에 있는 기존의 고정 너비 테이블과 문서의 대부분이 의존하는 압축된 상호 참조 스트림입니다. 단일 트레일러 키인 /XRefStm이 이 두 가지 뷰를 함께 연결하며, 도구가 전체 문서를 볼 수 있는지 여부는 이 키를 따르느냐에 달려 있습니다

이 문서는 파일을 소비하는 측면에서 하이브리드 파일을 살펴봅니다: 파일 끝의 바이트 형태, 편집 시 두 뷰가 어떻게 분리되는지, 그리고 Delphi 파이프라인에서 하이브리드 입력을 감지하고 라우팅하는 방법. 로더가 뷰를 병합하는 방법과 순서가 왜 협상 불가능한지에 대해서는 HotPDF의 하이브리드 참조 파일 로드에 관한 문서를 참조하세요; 이 글은 우선 레이아웃을 인식하는 데 중점을 둡니다

Office 내보내기가 인덱스를 두 번 쓰는 이유

PDF 1.5는 파일의 구조를 변경한 두 가지 기능을 도입했습니다: 객체 인덱스를 일반 텍스트 테이블이 아닌 압축된 이진 데이터로 저장하는 상호 참조 스트림과 많은 작은 객체들을 하나의 Flate 압축 컨테이너로 묶는 객체 스트림입니다. 이를 사용하는 라이터는 더 작은 파일을 생성하지만, PDF 1.4 리더는 결과를 열 수 없습니다. 왜냐하면 xref 키워드와 trailer 딕셔너리 같은 주요 구조가 사라지기 때문입니다

ISO 32000-1 §7.5.8.4는 이에 대한 타협안을 정의합니다. 하이브리드 참조 파일은 두 가지를 모두 기록합니다: 카탈로그와 페이지 트리를 포함하여 구형 리더가 접근해야 하는 객체를 주소 지정하는 클래식 상호 참조 테이블과 다른 모든 것을 인덱싱하는 상호 참조 스트림입니다. 객체 스트림에 포함된 객체들은 클래식 테이블에서 'free'로 표시되므로 1.4 리더는 불만 없이 이들을 건너뜁니다; 이들의 실제 위치는 오직 스트림 뷰에만 존재합니다. 이후 클래식 트레일러는 해당 스트림의 바이트 오프셋을 포함하는 /XRefStm 키를 전달합니다. 구형 뷰어는 이 키를 읽지 않고 테이블 뷰에서 파일을 렌더링합니다. 최신 뷰어는 이를 따르고 완전한 문서를 보게 됩니다. Word와 Excel은 수년간 정확히 이 레이아웃을 배출해 왔으며, 이것이 하이브리드 파일이 이색적인 예외 사례가 아니라 비즈니스 파이프라인이 받는 큰 비중을 차지하는 이유입니다

하이브리드 파일의 끝부분 형태

레이아웃은 바이트에서 가장 이해하기 쉽습니다. 다음은 작은 하이브리드 파일의 끝부분으로, 오프셋이 줄어들어 있습니다; 실제 Office 내보내기에서는 /XRefStm 값이 보통 파일 끝부분 근처의 큰 오프셋입니다. 읽기 순서는 PDF 파일 구조에 대한 당사의 개요에서 설명한 끝에서부터의 역추적입니다: %%EOF를 찾고, startxref를 읽은 다음 테이블로 이동합니다

% ... 본문 객체, 객체 스트림 포함, 그리고 116 바이트 위치의
% 상호 참조 스트림 (/Type /XRef를 가진 스트림 객체) ...

xref                    % 클래식 섹션: startxref가 가리키는 곳
0 4
0000000000 65535 f      % 슬롯 0: 빈 목록의 머리, 항상 존재함
0000000017 00000 n      % 객체 1: 카탈로그, 어떤 리더에게나 보임
0000000000 65535 f      % 객체 2: free로 표시됨 -- 객체 스트림 안에 존재
0000000000 65535 f      % 객체 3: 동일함; 스트림 뷰만 이를 위치 지정함
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % 상호 참조 스트림의 바이트 오프셋
>>
startxref
7164                    % 위의 'xref' 키워드의 바이트 오프셋
%%EOF

이 덤프의 두 가지 세부 사항이 전체 메커니즘을 이끕니다. 첫째, startxref는 클래식 섹션을 가리키며, 이는 의도적인 것입니다: 이것이 구형 리더가 도달해야 하는 주소입니다. 상호 참조 스트림은 트레일러 딕셔너리 내부의 /XRefStm 키를 통해서만 접근 가능하므로, 이 키를 찾지 않는 파서는 스트림의 존재를 결코 알지 못합니다. 둘째, 객체 2와 3은 유익한 종류의 거짓말입니다. 클래식 테이블은 이들을 비어 있다고 선언하지만, 이들은 실제로는 압축된 컨테이너 안에 있는 실제 객체입니다; 이 'free' 표시는 1.4 리더가 사용할 수 없는 항목에 걸려 넘어지지 않게 합니다. 클래식 뷰만 신뢰하는 소비자는 이 문서의 대부분이 존재하지 않는다고 결론을 내립니다

두 가지 뷰가 분리되는 방식

Word에서 갓 나온 하이브리드 파일은 내부적으로 일관성이 있습니다: 두 뷰 모두 선언된 범위 내에서 동일한 문서를 설명합니다. 문제는 뷰 중 하나만 이해하는 도구로 파일을 편집할 때 시작됩니다. 클래식 스타일의 증분 업데이트(새로운 객체, 새로운 xref 섹션, 이전 섹션에 대한 /Prev 체인 및 새 트레일러)를 추가하는 스탬핑 유틸리티를 생각해 보십시오. 만약 해당 트레일러가 /XRefStm 키를 누락한다면 스트림 뷰는 고아가 됩니다; 만약 예전 값을 복사하여 전달한다면, 스트림 뷰는 편집 이전의 문서를 여전히 설명합니다. 어느 쪽이든, 두 인덱스는 이제 파일이 무엇을 포함하는지에 대해 일치하지 않게 됩니다

그 결과로 생성된 파일은 뚜렷한 실패 징후를 보입니다: 한 뷰에서는 보이는 객체가 다른 뷰에서는 없거나 오래된 상태입니다. 스트림 뷰를 통해 확인하는 리더는 업데이트된 객체의 편집 전 버전을 찾거나 추가된 객체에 대한 항목을 전혀 찾지 못합니다. 테이블 뷰를 사용하는 리더는 편집 내용을 보지만 스트림만 위치시킬 수 있는 압축된 객체를 놓칩니다. 실제 상황에서는 한 뷰어에서는 살아남고 다른 뷰어에서는 사라지는 양식 필드, 스탬핑 작업이 삭제한 것으로 보이는 주석, 혹은 완전히 잘못된 객체에 도착하는 검색 등으로 나타납니다

이러한 파일들의 디버깅에 비용이 많이 드는 이유는 Adobe Acrobat이 보통 불만 없이 이를 열기 때문입니다: 인덱스가 바이트와 불일치할 때, 이는 객체 헤더를 검색하여 상호 참조 데이터를 조용히 다시 빌드하므로, 손상된 파일을 생성한 사용자는 문제를 파악하지 못합니다. 문제는 파일이 엄격한 소비자, 프리플라이트 검증기, 서명 서비스, 또는 아카이브 수집 작업에 도달하여 선언된 구조를 신뢰하고 누락된 객체나 상호 참조 불일치를 보고할 때 나중에 드러납니다. "Acrobat에서는 잘 열립니다"는 거의 모든 하이브리드 비동기화 티켓의 시작입니다

순수 Delphi에서 하이브리드 파일 감지

입력을 분류하는 데에는 PDF 라이브러리가 필요하지 않습니다. /XRefStm 키는 오직 클래식 트레일러 딕셔너리 내부에서만 나타날 수 있으며, 활성 트레일러는 파일의 마지막 2KB 안에 위치합니다. 이는 사양이 %%EOF가 물리적인 끝 근처에 나타날 것을 요구하기 때문입니다. 제한된 파일 꼬리 창(tail window)을 읽고 검색하는 것으로 분류하기에 충분합니다:

uses
  System.SysUtils, System.Classes, System.StrUtils, System.Math;

function IsHybridReferencePdf(const FileName: string): Boolean;
const
  TailWindow = 2048;
var
  Stream: TFileStream;
  Buf: TBytes;
  Tail: string;
  Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
  Result := False;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if Stream.Size < 48 then
      Exit;
    Len := Min(TailWindow, Integer(Stream.Size));
    SetLength(Buf, Len);
    Stream.Position := Stream.Size - Len;
    Stream.ReadBuffer(Buf[0], Len);
  finally
    Stream.Free;
  end;

  // 관련된 모든 키워드는 7비트 ASCII이므로 바이트별 디코딩은 안전합니다.
  Tail := TEncoding.ANSI.GetString(Buf);

  // '마지막' trailer 키워드 찾기: 증분 업데이트 시
  // 파일을 제어하는 트레일러는 가장 최신 것입니다.
  TrailerPos := 0;
  NextPos := Pos('trailer', Tail);
  while NextPos > 0 do
  begin
    TrailerPos := NextPos;
    NextPos := PosEx('trailer', Tail, NextPos + 1);
  end;
  if TrailerPos = 0 then
    Exit;  // 클래식 트레일러가 없음: 하이브리드가 아닌 순수 xref-stream 파일

  // 하이브리드 트레일러는 'trailer'와 'startxref' 사이에 /XRefStm을 가집니다.
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

세 가지 결과는 세 가지 레이아웃과 일치합니다. 클래식 단독 파일은 트레일러가 있지만 /XRefStm이 없으므로 False입니다. 상호 참조 스트림을 완전히 사용하는 파일은 trailer 키워드가 전혀 없고 트레일러 키들이 스트림 딕셔너리 내에 존재하므로, 압축되었지만 하이브리드는 아니므로 역시 False입니다. 이중 인덱스 레이아웃만이 True를 반환합니다

프로덕션 사용을 위해, 두 가지 강화는 코드 라인을 더 추가할 가치가 있습니다. /XRefStm 뒤의 정수를 파싱하고, 해당 오프셋으로 찾기(seek)를 실행하며, /Type /XRef인 스트림 객체가 실제로 그 위치에 존재하는지 확인하십시오. 파일이 잘린 경우 스트림이 사라진 상태에서 키가 있을 수 있으며, 이는 정상적인 하이브리드와 다른 문제 카테고리에 속합니다. 또한 창 크기를 매개변수로 취급하십시오. 2KB는 일반적인 Office 출력에는 충분하지만 유난히 큰 트레일러 딕셔너리가 키워드를 범위를 벗어나게 밀어낼 수 있으며, 창 크기를 넓히는 것이 실수로 파일을 클래식으로 선언하는 것보다 낫습니다

Delphi 파이프라인을 통한 하이브리드 파일 라우팅

감지를 통해 라우팅 결정을 내릴 수 있습니다. 읽기, 렌더링 또는 검증만 되는 파일의 경우, 두 뷰를 해결하는 로더를 사용한 다음 바이트가 아닌 동작을 확인하십시오. PDFium Component는 로드 중에 /XRefStm 체인을 파싱하므로, 코드가 보는 객체 테이블은 병합된 것이고 객체 및 상호 참조 스트림 검증에 대한 당사 기사에서 설명된 검사는 변경 없이 적용됩니다. 만약 동기화가 풀린 하이브리드 파일이 심각하게 손상되어 로드를 거부할 경우, 엔진은 그 오류 집합을 통해 FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITYFPDF_ERR_PAGE로 보고하며, FPDF_ERR_FORMAT은 구조적 손상이 생성하는 결과입니다. 그러나 이 신호에 전적으로 의지하지 마십시오: PDFium은 설계상 관대하게 작동하여 대다수의 불일치 파일을 말없이 재구성하므로, 성공적인 로드는 파일이 복구 가능했음을 증명할 뿐 두 뷰가 일치한다는 것을 의미하지는 않습니다. 의미 있는 일관성 확인은 완전한 객체 탐색에서 찾은 것과 트레일러의 /Size가 선언하는 것을 비교하는 것입니다

파이프라인이 수정하는 파일의 경우, 가장 안전한 정책은 파일이 하이브리드인 것을 중지시키는 것입니다. 로드 후에 HotPDF를 통한 전체 저장을 실행하면, /XRefStm이 없고 동기화가 풀릴 두 번째 뷰가 없으며 모든 객체가 정확히 하나의 인덱스 항목에 속하게 되는 단일하고 일관된 상호 참조 형태로 문서를 다시 씁니다. 그러한 정규화(normalization)는 아카이브 수집 전, 엄격한 하위 RIP 또는 서명 서비스 전, 하이브리드 입력에 편집이 적용된 후 필요합니다. 이것이 작동하는 이유는 HotPDF 하이브리드 참조 기사에서 자세히 설명한 메커니즘대로 로더가 들어오는 도중에 뷰를 올바르게 병합했기 때문입니다

그대로 두어야 하는 파일의 한 종류는 디지털 서명된 문서입니다. 완전히 다시 쓰면 모든 바이트가 이동하여 원본 범위에 대해 계산된 서명이 무효화됩니다. 서명된 하이브리드 파일에 대한 변경은 두 뷰를 모두 유지하는 올바른 증분 업데이트로 이루어져야 하며, 읽기만 필요한 파일은 손대지 않고 통과해야 합니다. 정규화는 여러분이 소유한 파일을 위한 것이며, 서명된 파일에는 추가만 해야 합니다

하이브리드 참조 PDF는 형식이 잘못된 것이 아닙니다; 그것들은 포맷 자체의 호환성 브리지이며, Office 애플리케이션들은 PDF 1.4 리더들이 설치 기반에 살아남아 있는 한 계속해서 그것들을 생산할 것입니다. /XRefStm 키를 발견하고, PDFium Component로 병합된 문서를 검증하며, HotPDF Component로 깨끗한 단일 인덱스 출력을 재생성할 수 있는 파이프라인은 이들을 단지 트레일러에 하나의 추가 표지판이 있는 평범한 입력으로 취급합니다