기술 문서

Delphi에서 PDFium으로 Fast Web View PDF 직선화하기

PDFium Component는 TPdf.SaveAsLinearized를 통해 Delphi에서 Fast Web View(직선화) PDF를 만들어 냅니다. 이 메서드는 현재 문서를 PDFium으로 저장한 다음, ISO 32000-1 Annex F에 따라 결과를 다시 씁니다: 첫 페이지 오브젝트가 앞으로 이동하고, 크로스 레퍼런스 섹션 두 개가 기록되며, 페이지 오프셋과 공유 오브젝트 힌트 테이블이 뷰어에게 어떤 페이지든 가져올 바이트 범위를 알려 줍니다

아무도 신경 쓰는 이유는 네트워크입니다. 오브젝트 스토리지나 문서 포털에서 제공되는 40 MB 카탈로그는 처음 몇 킬로바이트만으로 1페이지를 보여 줘야 하고, 380페이지로 건너뛰는 건 전체 다운로드가 아니라 HTTP Range 요청 한두 번의 비용이어야 합니다. 뷰어가 그럴 수 있는 경우는 파일이 직선화되어 있을 때뿐입니다. 편집 파이프라인을 떠나는 대부분의 PDF는 그렇지 않습니다. PDF를 쓰는 자연스러운 방식은 크로스 레퍼런스 테이블을 끝에 놓고 페이지 오브젝트를 작성기가 마침 내보낸 자리에 뿌려 버리니까요

PDFium은 직선화 파일을 읽을 수 있는데 쓸 수 없는 이유

PDFium에는 직선화용 완전한 리더가 있고 라이터는 없습니다. FPDFAvail_* 가용성 API는 직선화 딕셔너리를 감지하고, 힌트 테이블을 디코드하고, 필요한 바이트 범위를 정확히 호스트에 요구합니다. PDFium Component의 프로그레시브 다운로드와 취소 워크플로가 그 위에 세워져 있습니다. 반면 공개 저장 ABI는 평범한 복사 저장과 대상 PDF 버전을 지정한 저장만 제공합니다. 오브젝트를 재배치하거나 힌트 스트림을 내보내는 것은 하나도 없습니다

PDFium Component는 PDFium이 이미 잘하는 것을 재구현하지 않고 그 간극을 메웁니다. TPdf.SaveAsLinearized는 먼저 기존 SaveAs를 호출하므로, 의미적 저장은 여전히 PDFium 소유입니다: 편집된 페이지, 폼 필드 상태, 버전 처리가 모두 수년간 단련된 코드 경로로 갑니다. 그다음 컴포넌트는 그 안정적인 중간 PDF에 대해 Annex F 재구성 패스를 돌리고 직선화된 결과를 대상에 기록합니다. 작업을 이렇게 쪼개면 직선화는 주석이나 폼을 이해할 필요가 없고 오브젝트, 참조, 바이트 오프셋만 다루면 됩니다

Delphi에서 TPdf.SaveAsLinearized 호출하기

TPdf.SaveAsLinearized에는 오버로드가 둘 있습니다. TStream용과 파일명용이며, 둘 다 SaveAs와 같은 선택적 Option: TSaveOption, PdfVersion: TPdfVersion 파라미터를 받습니다. 둘 다 성공 시 True를 반환합니다. 파일 오버로드는 대상 디렉터리의 임시 파일에 쓰고 목적지를 원자적으로 치환하므로, 실패한 저장이 좋던 PDF 자리에 반쯤 쓰인 PDF를 남기는 일은 없습니다

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.Active := True;
    // ... 페이지 편집, 폼 채우기, 도장 등 파이프라인이 하는 일 ...
    if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

파라미터 규칙은 의도적으로 엄격합니다. saIncremental은 에러를 냅니다. 증분 업데이트는 파일 끝에 새 리비전을 덧붙여 앞으로 몰아 넣은 레이아웃을 즉시 파괴할 테니까요. PDF 1.2 미만의 대상 버전도 거부됩니다; pvUnknown은 소스 버전을 유지합니다. 암호화된 입력에는 saRemoveSecurity가 필요합니다. 라이터가 오브젝트를 다시 번호 매기고 PDF 오브젝트 수준 암호화 키는 오브젝트 번호에서 유도되기 때문입니다. 그리고 직선화는 전면 재작성이므로 기존 디지털 서명은 무효화됩니다. 서명된 채 직선화된 상태로 전달해야 한다면, 먼저 직선화하고 나중에 서명하세요

라이터가 Fast Web View를 위해 오브젝트를 묶는 방식

직선화는 1페이지를 파일 꼭대기로 옮기는 것 이상입니다. 라이터는 현재 리비전을 파싱합니다. 크로스 레퍼런스 데이터가 클래식 테이블이든, 하이브리드 파일이든, 크로스 레퍼런스 스트림이든요. 그리고 활성 오브젝트 스트림의 모든 멤버를 독립 오브젝트로 펼칩니다. 이어서 참조 그래프를 만들어 오브젝트를 다섯 그룹으로 정렬합니다: 파일을 열기 위해 필요한 문서 수준 오브젝트, 첫 페이지 오브젝트, 이후 한 페이지 전용 오브젝트, 이후 여러 페이지가 공유하는 오브젝트, 그리고 나머지 전부입니다

그 그래프 순회의 함정은 /Parent입니다. 모든 페이지 딕셔너리는 페이지 트리 위쪽을 가리키고, 순진한 순회는 그 포인터를 따라 부모 노드로 갔다가 /Kids를 통해 모든 형제 페이지와 그 콘텐츠 스트림으로 내려갑니다. 한 걸음 뒤에는 첫 페이지 그룹이 문서 전체를 담고, 파일은 기술적으로 직선화되어 있지만 범위 우선 접근에는 쓸모가 없습니다. PDFium Component는 페이지 /Parent 링크와 수집 중인 페이지 밖의 페이지 트리 노드를 무시하는데, 첫 페이지 섹션을 작게 유지하는 것이 바로 그것입니다. 깊은 페이지 트리를 가진 51페이지 회귀 샘플에서 첫 페이지 섹션은 5289바이트, 1105354바이트 출력의 0.48%입니다

입력의 오브젝트 스트림과 크로스 레퍼런스 스트림은 평범한 세대 0 간접 오브젝트로 평탄화되고, 모든 참조는 새 번호로 다시 쓰이며, 출력은 클래식 크로스 레퍼런스 섹션을 씁니다. 압축 오브젝트 스트림에 비해 파일 크기가 좀 늘지만, 힌트 테이블 커버리지가 단순해지고 직선화를 아는 모든 뷰어가 다루는 레이아웃이기도 합니다. 특정 출력에서 Fast Web View보다 압축이 중요하다면, PDFium Component에는 그 일을 위한 별도의 오브젝트 스트림 라이터가 있습니다

직선화 딕셔너리와 두 xref 섹션에는 무엇이 들어가나요?

PDFium Component가 쓴 직선화 파일은 두 개의 클래식 크로스 레퍼런스 섹션과 맨 첫 오브젝트 안의 직선화 딕셔너리를 운반합니다. 첫 xref 섹션은 직선화 딕셔너리 뒤에 오고 문서 수준 및 첫 페이지 오브젝트를 커버하므로, 뷰어는 파일 앞부분만으로 1페이지를 렌더링할 수 있습니다. 메인 xref 섹션은 끝에 놓이고 나머지 전부를 커버합니다

직선화 딕셔너리 자체는 뷰어가 다른 것을 읽기 전에 필요로 하는 숫자들을 담습니다: 파일 길이 /L, 힌트 스트림 오프셋과 길이 /H, 첫 페이지 오브젝트 번호 /O, 첫 페이지 섹션의 끝 /E, 페이지 수 /N, 그리고 메인 xref 테이블의 첫 엔트리 오프셋 /T입니다. 이들 전부 최종 레이아웃에 의존하는데, 딕셔너리는 레이아웃 앞에 앉아 있습니다. 자연 폭 숫자로 쓰면 루프가 생깁니다: 999999에서 1000000으로 늘어난 길이는 바이트 하나를 더하고 그 뒤의 모든 오프셋을 밀고, 그것이 다른 숫자의 폭을 바꿀 수 있고, 또 그렇습니다

PDFium Component는 직선화 딕셔너리에 고정 320바이트를 예약해서 루프를 끊습니다. 모든 오브젝트, 힌트 스트림, 두 xref 섹션, 트레일러를 먼저 쓰고; 모든 오프셋이 확정되면 딕셔너리를 그 자리에 채우고 예약된 폭으로 패딩합니다. 미묘한 디테일 하나는 /T입니다: 앞의 xref 키워드가 아니라 메인 xref 테이블의 오브젝트 0 엔트리를 가리켜야 합니다. 관대한 뷰어는 어차피 열어 주지만 엄격한 검증기는 키워드 오프셋을 지적합니다

힌트 테이블 오프셋은 힌트 스트림을 무시하는 이유

직선화 PDF의 힌트 테이블 오프셋은 힌트 스트림이 파일에 존재하지 않는 것처럼 해석됩니다. Annex F가 그렇게 정의한 이유는 힌트 스트림이 자기 크기를 자기가 담는 숫자들에 되먹이지 않고 첫 페이지 섹션과 나머지 사이에 앉을 수 있게 하기 위함입니다. 라이터에 대한 실용적 결과는 틀리기 쉽습니다: 힌트 스트림 뒤에 놓인 실제 오프셋은 페이지 오프셋이나 공유 오브젝트 힌트 테이블에 들어가기 전에 줄여야 합니다

문제는 얼마나 줄이느냐입니다. 유혹적인 답은 스트림 데이터의 길이입니다. 힌트 콘텐츠 같아 보이는 부분이니까요. 올바른 답은 힌트 간접 오브젝트 전체의 길이입니다. N 0 obj, 스트림 딕셔너리, stream과 endstream 키워드, endobj를 포함한요. 데이터 길이만 빼면 이후의 모든 오프셋은 수십 바이트 어긋납니다. 1페이지는 힌트 스트림 앞에 살아 있으므로 여전히 렌더링되고, 그래서 이 버그는 빠른 육안 검사를 살아남고, 뷰어가 이후 페이지로 건너뛰어 엉뚱한 오브젝트 한가운데서 시작하는 범위를 요청할 때만 나타납니다

두 힌트 테이블 모두 최상위 비트 우선으로 비트 패킹됩니다. 이것도 Annex F대로입니다. 컴포넌트는 진짜 클라이언트처럼 결과를 검증합니다: 스파스 범위 스토어가 51페이지 샘플의 처음 1 KiB만 공급하고, PDFium의 네이티브 힌트 처리가 나머지를 요구합니다. 전체 파일이 한 번도 도착하지 않은 채 마지막 페이지가 열립니다

Delphi에서 직선화 PDF 검증하기

가장 싼 검사는 PDFium 자신에게 묻는 것입니다. TPdf.InspectAvailability는 문서를 로드하지 않고 스트림에 대해 가용성 API를 돌리고, 그 Linearization 필드는 plnLinearized, plnNotLinearized, plnUnknown을 보고합니다

function IsFastWebView(const FileName: string): Boolean;
var
  Stream: TFileStream;
  Info: TPdfAvailability;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Info := TPdf.InspectAvailability(Stream);
    Result := Info.Linearization = plnLinearized;
  finally
    Stream.Free;
  end;
end;

그것은 PDFium이 레이아웃을 인식한다는 확인일 뿐입니다; 힌트가 올바르다는 증명은 아닙니다. 진짜 엔드투엔드 테스트는 파일을 범위 제한 전송으로 서빙하고 TPdfProgressiveDocument로 늦은 페이지를 여는 것입니다. PDFium VCL로 대용량 PDF 온디맨드 스트리밍 가이드가 하는 것과 같은 방식이죠. 파일의 작은 일부만 가져온 뒤 페이지가 렌더링되면 힌트 테이블이 제 역할을 하는 겁니다. 릴리스 파이프라인에 두 번째 독립 직선화 검사기를 더하는 것도 값어치가 있습니다. 엉뚱한 자리의 /T나 살짝 틀린 오프셋에 대한 관용이 뷰어마다 다르니까요

기억해 둘 한계 둘입니다. 오브젝트 머티리얼라이저는 여전히 2 GiB 이상의 배열을 거부하므로, 극단적으로 큰 문서는 당분간 다른 전략이 필요합니다. 그리고 직선화는 서버가 HTTP Range 요청을 존중할 때만 값을 냅니다; Accept-Ranges를 벗겨 내는 프록시 뒤에서는 직선화된 파일도 다른 파일과 똑같이 다운로드됩니다

FAQ

TPdf.SaveAsLinearized는 디지털 서명을 유효하게 유지하나요?

아니요. TPdf.SaveAsLinearized는 모든 오브젝트를 다시 번호 매기고 다시 쓰므로 기존 서명은 무효화됩니다. 먼저 직선화하고, 그다음 서명하세요

PDFium Component는 암호화된 PDF를 직선화할 수 있나요?

saRemoveSecurity를 넘길 때만 그렇습니다. 오브젝트 수준 암호화 키는 오브젝트 번호에 의존하는데 라이터가 그 번호를 바꾸므로, 출력은 암호화 없이 기록됩니다

직선화 출력은 어느 PDF 버전을 지원하나요?

TPdf.SaveAsLinearized는 소스 버전을 유지하는 pvUnknown과 PDF 1.2 이상의 어떤 대상이든 받아들이고, 덧붙여진 리비전이 레이아웃을 깨뜨리므로 saIncremental은 거부합니다

Fast Web View 출력, 프로그레시브 로딩, 가용성 API는 모두 Delphi 및 C++Builder용 PDFium Component의 일부입니다. Delphi, C++Builder, Lazarus 프로젝트를 위해 PDFium을 감싼 컴포넌트죠