기술 문서

PDF 선형화 및 빠른 웹 보기: 작동 원리

80MB 스캔 보고서를 링크 뒤에 두고 브라우저에서 열면 어떤 일이 일어나는지 지켜보십시오. 뷰어는 많은 바이트가 도착할 때까지 빈 창으로 대기하다가, 한 번에 첫 페이지를 그립니다. 40페이지로 이동하면 파일 구조가 잘못된 경우 전체 다운로드가 다시 시작될 수 있습니다. 답답한 점은 독자가 오직 첫 페이지만 보고 싶어 했다는 것입니다. 선형화(Linearization)는 이 문제에 대한 구조적인 해결책입니다. 선형화는 뷰어가 파일의 작은 앞부분에서 첫 페이지를 렌더링하고 나머지는 요청 시 가져올 수 있도록 PDF를 재구성하며, 이것이 Adobe가 이 기능을 "빠른 웹 보기(Fast Web View)"로 마케팅하는 이유입니다

이 기능은 다른 파일 형식이 아닙니다. 선형화된 PDF는 규격을 준수하는 리더가 특별한 처리 없이 열 수 있는 일반적인 PDF입니다. 비결은 바이트의 순서와 파일이 가지는 두 가지 추가 구조에 있습니다. ISO 32000-1 부록 F에 전체 구성이 명시되어 있으며, 레이아웃을 확인하고 나면 그 동작이 마법이 아니라 파일 순서를 첫 화면 렌더링 지연 시간(first-paint latency)과 맞바꾼 의도적인 거래라는 것을 알게 될 것입니다

선형화가 실제로 재배열하는 것

일반 PDF는 객체를 거의 어떤 순서로든 분산시킬 수 있습니다. 파일 끝에 있는 상호 참조 표(cross-reference table) 덕분에 이 방식이 작동합니다. 리더는 끝으로 이동하여 startxref 포인터를 읽고 xref를 로드한 다음 거기서 오프셋을 통해 모든 객체를 찾을 수 있습니다. 끝부분으로의 탐색(seek) 비용이 없는 로컬 파일에서는 아주 훌륭한 설계지만, 끝부분이 가장 늦게 도착하는 네트워크를 통해 스트리밍되는 파일의 경우 형편없는 구조입니다. 첫 페이지를 렌더링하려면 일반적인 리더는 페이지 객체, 콘텐츠 스트림, 참조하는 폰트, 그리고 그리는 이미지 등이 필요한데, 순서가 없는 파일에서는 이것들이 마지막 메가바이트 영역을 포함하여 어디에든 위치할 수 있습니다

선형화는 순서를 고정합니다. 첫 페이지를 표시하는 데 필요한 객체들은 작은 헤더 섹션 바로 뒤 앞부분에 연속된 블록으로 모여 바이트 스트림 초반에 도착합니다. 나머지 페이지들과 그것들이 공유하는 리소스는 예측 가능한 순서로 그 뒤를 따릅니다. 이 최적화를 무시하는 리더를 위해 두 번째 전체 상호 참조 표가 여전히 끝에 남아 있지만, 선형화된 파일에는 첫 페이지 상호 참조와 스트리밍 리더에 필요한 파라미터가 파일 앞쪽에 위치합니다. 리더는 더 이상 무언가를 그리기 위해 파일의 끝부분에 도달할 필요가 없습니다

첫 페이지 객체 세트 및 선형화 파라미터 딕셔너리

선형화된 파일에서 %PDF 헤더 다음으로 가장 처음 나오는 객체는 선형화 파라미터 딕셔너리입니다. 스트리밍 리더는 이것을 찾아 최적화 존재 여부와 사용 방법을 결정합니다. 딕셔너리에는 전체 파일 길이, 메인 상호 참조 섹션이 시작되는 바이트 오프셋, 첫 페이지의 객체 번호, 그리고 뒤따르는 힌트 스트림의 위치와 길이가 기록됩니다. 리더는 열리는 처음 몇 킬로바이트의 이 숫자들만으로 첫 페이지를 보여주기 위해 얼마나 가져와야 하는지, 다른 곳으로 이동할 수 있게 해주는 인덱스를 어디서 찾아야 하는지 알 수 있습니다

부록 F는 여기서 "첫 페이지"의 의미를 엄격하게 정의합니다. 첫 페이지 섹션에는 페이지 객체 자체, 콘텐츠 스트림, 스트림이 참조하는 리소스가 포함되어야 파일 앞부분 다운로드 완료 시 해당 페이지가 자급자족할 수 있습니다. 공유 리소스(모든 페이지에 사용되는 폰트, 헤더에 반복되는 로고 등)는 특별하게 처리됩니다. 첫 페이지에 제공될 수 있도록 일찍 나타나되, 나중에 리더가 30페이지를 렌더링할 때 다시 가져오지 않도록 공유(shared)로 플래그가 지정됩니다. 페이지 비공개(private) 객체와 공유 객체 간의 이러한 구분이 자체 제작 "옵티마이저"들이 가장 많이 실수하는 부분이며, 이를 잘못 구성하면 선형화되었다고 주장하면서도 여전히 지연이 발생하는 파일을 만들게 됩니다

힌트 스트림: 페이지 이동 비용을 낮추는 인덱스

첫 페이지를 빠르게 보여주는 것은 절반의 가치에 불과합니다. 나머지 절반은 중간 내용을 모두 다운로드하지 않고도 임의의 페이지로 넘어가는 것이며, 이를 제공하는 것이 힌트 스트림(hint stream)입니다. 선형화된 파일은 페이지 오프셋 힌트 테이블(page offset hint table)과 공유 객체 힌트 테이블(shared object hint table)을 가지고 있으며, 이들은 파라미터 딕셔너리에서 참조된 스트림으로 저장됩니다. 페이지 오프셋 테이블은 모든 페이지에 대해 해당 객체가 파일 내에서 시작하는 위치와 길이를 기록합니다. 공유 객체 테이블은 여러 페이지에 걸쳐 사용되는 리소스에 대해 같은 역할을 합니다

이 테이블들을 통해 40페이지를 원하는 리더는 파일을 순차적으로 구문 분석(파싱)하지 않습니다. 힌트 테이블을 참조해 40페이지가 차지하는 바이트 범위를 알아내어, 서버에 정확히 그 범위만 요청하고 바이트가 도착하면 페이지를 렌더링하며, 아직 보유하지 않은 모든 공유 리소스도 같은 메커니즘을 통해 가져옵니다. 힌트 스트림은 사실상 문서 위에 놓인 무작위 액세스 맵(random-access map)이며, 선형화가 잘 된 500페이지짜리 파일이 느린 연결에서도 반응 속도가 좋게 느껴지는 반면 최적화되지 않은 동일한 크기의 파일은 그렇지 않은 이유입니다

서버가 협력해야 하는 이유

선형화는 전송 시스템이 파일의 임의 조각(slice)을 제공할 수 있다고 가정하며, 빈약한 결과에 대해 형식을 탓하기 전에 이 가정을 확인해 볼 가치가 있습니다. 그 메커니즘은 HTTP 바이트 서빙(byte-serving)입니다. 리더는 범위(range) 요청을 보내고, 서버는 206 Partial Content 응답으로 응답합니다. 서버가 Accept-Ranges: bytes를 알리지 않거나 전면의 프록시 또는 CDN이 범위 요청을 전체 전송으로 축소하는 경우, 리더는 40페이지만 개별적으로 가져올 방법이 없어서 전체 파일 다운로드로 대체(fallback)합니다. 그러면 PDF 내부 구조가 완벽하게 올바르더라도 완전히 낭비됩니다

이것이 가장 흔히 "선형화가 작동하지 않는다"고 오진되는 실패입니다. 파일은 정상이지만 전달 경로가 정상이 아닙니다. 문서를 재구축하기 전에, 리더가 접근하는 URL에 대해 호스트가 실제로 부분 콘텐츠(partial content)를 반환하는지 조건부 요청(conditional request)을 통해 확인하십시오. 많은 정적 호스트는 기본적으로 이를 수행하지만 구성이 잘못된 애플리케이션 서버와 캐싱 계층은 그렇지 않습니다

점진적 업데이트가 선형화를 조용히 깨뜨리는 방법

선형화된 파일을 올바르게 생성하고도 왜 최적화가 사라지는지 궁금해하는 사람들이 놀라는 제약 조건이 여기에 있습니다. 선형화는 파일 앞에 인덱스가 있는 신중하게 배열된 단일 레이아웃에 의존합니다. 점진적 업데이트(incremental update)는 설계상 이를 위반합니다. 도구가 증분 저장(incremental save)을 통해 서명을 추가하거나 양식 필드를 채우거나 주석을 추가할 때, 파일을 다시 쓰지 않습니다. 원본 바이트는 건드리지 않고 변경된 객체, 새로운 상호 참조 섹션, 새로운 트레일러(trailer)를 끝에 추가합니다. 이 추가(append) 방식이 증분 업데이트의 핵심입니다. 속도가 빠르고, 감사(audit) 또는 서명 검증을 위해 이전 개정판을 보존하기 때문입니다

부작용은 파일에서 세심하게 배치된 첫 페이지 블록 다음에 이제 가장 최신 상호 참조 데이터가 꼬리(끝) 부분에 존재하게 되며, 맨 앞에 있는 선형화 파라미터 딕셔너리가 더 이상 파일과 일치하지 않는 레이아웃을 설명하게 된다는 점입니다. 규격을 준수하는 리더는 불일치를 감지하고 문서를 정상적인 비선형화 PDF로 취급합니다. 원본의 선형화 구조가 여전히 파일 앞부분 절반에 그대로 있더라도 빠른 웹 보기는 사라집니다. 여러 번의 업데이트를 추가하면 각각 새로운 개정판이 끝에 쌓이고 오래된 앞부분의 인덱스와 실제 상태 간의 격차는 더 커집니다

워크플로에 편집과 빠른 웹 보기가 모두 필요한 경우, 규칙은 구조에서 직접 비롯됩니다. 문서가 유동적인 상태(in flux)일 때는 점진적으로 편집하고 마지막에 한 번 전체를 다시 선형화합니다. 전체를 다시 쓰는 것(full rewrite)이 레이아웃을 복원하는 방법입니다. HotPDF 관점에서 이는 진행 중인 편집이 델타(delta)를 추가하는 BeginIncrementalUpdateSaveIncrementalUpdate를 거치지만, 마무리 단계에서는 누적된 이전 개정판들을 버리고 하나의 깔끔한 레이아웃을 내보내는 LoadFromFile과 이은 SaveLoadedDocument를 통해 전체 문서를 로드하고 새롭게 직렬화(serialize)함을 의미합니다. 동일한 거래가 객체 스트림(object streams)에도 나타납니다. UseObjectStreamsUseXRefStream을 함께 활성화하면 상호 참조를 압축하고 객체를 촘촘하게 채워 파일 크기를 줄이는 데 도움이 되지만 다른 구조적 선택과 마찬가지로 추가되는(appended) 개정판 위에 결합되기보다는 마지막 다시 쓰기 중에 적용되어야 합니다

// 진행 중인 편집: 델타 추가, 이전 개정판 유지.
// 이 작업은 파일을 선형화되지 않은 상태로 둡니다.
Pdf.BeginIncrementalUpdate('report.pdf');
Pdf.AddPage;
Pdf.CurrentPage.TextOut(72, 760, 0, 'Addendum');
Pdf.SaveIncrementalUpdate('report.pdf');

// 마무리 단계: 전체 재직렬화로 깔끔한 단일 레이아웃 생성,
// 누적된 개정판 제거. 출력 결과에 선형화 도구를 다시 실행하세요.
Pdf.LoadFromFile('report.pdf');
Pdf.SaveLoadedDocument('report-final.pdf');

HotPDF는 한 번의 호출로 끝나는 "선형화" 루틴을 노출하지 않으므로, 실용적인 패턴은 깔끔하고 완전히 다시 쓰인 파일을 생성하고 그 위에 전용 최적화 도구를 실행하는 것입니다. 명령줄 도구가 직접 재배열을 처리합니다. qpdf는 단일 플래그를 사용하여 파일을 선형화된 형태로 다시 씁니다

qpdf --linearize report-final.pdf report-web.pdf

파일이 선형화되었는지 구분하는 방법

파일 이름이나 해당 파일을 생성했다고 주장하는 도구를 신뢰하지 말고 바이트를 직접 확인하십시오. 가장 직접적인 확인 방법은 파일의 앞부분(head)입니다. 파일을 열고 헤더 이후의 첫 번째 객체로 /Linearized 키가 포함된 선형화 파라미터 딕셔너리를 찾으십시오. 리더 관점에서의 지름길은 Acrobat의 문서 속성(Document Properties) 대화 상자이며, 구조가 진정으로 존재하고 최신 상태일 때만 "빠른 웹 보기: 예(Fast Web View: Yes)"로 보고합니다

스크립트 점검 시에는 qpdf가 구조의 존재 여부와 무결성(integrity)을 모두 보고하며, 점진적 업데이트가 남기는 바로 그 상태처럼 더 이상 실제 레이아웃을 반영하지 않는 선형화 딕셔너리를 파일이 가질 수 있기 때문에 이것이 중요합니다

# "파일이 선형화되었습니다"를 보고하고 레이아웃에 대해 힌트 테이블 유효성 검사
qpdf --check report-web.pdf

# 선형화 매개변수와 힌트 데이터를 자세히 덤프
qpdf --show-linearization report-web.pdf

검증 단계가 그 가치를 발휘하는 부분입니다. 딕셔너리 존재 여부만 확인하는 검사는 인덱스가 잘못된 오프셋을 가리키는 파일도 기꺼이 승인할 것이지만 힌트 테이블을 실제 객체 위치에 맞춰 조정하는 점검(check)은 실제 리더의 범위(range) 요청 하에서 최적화가 잘 유지될 것인지 말해줍니다

선형화는 웹을 통해 제공되는 대형 문서, 특히 불안정한 연결 상태에 있는 모바일 독자들에게 여전히 적용할 가치가 있으며 앞부분에 로드되는 인덱스에 파일 크기의 몇 퍼센트가 소요됩니다. 명확히 해야 할 두 가지는 PDF 내부 구조와 외부의 바이트 서빙이 모두 올바라야 한다는 점과 사후 편집은 파일을 다시 쓰기 전까지 최적화를 무효화한다는 점입니다. 다시 선형화하는 작업은 파이프라인의 모든 변경이 마무리된 후 마지막 단계로 다루십시오. 여기에 설명된 상호 참조(cross-reference), 객체 스트림(object-stream) 및 점진적 업데이트(incremental-update) 동작은 Delphi 및 C++Builder용 HotPDF Component가 구현하는 구조 모델의 일부입니다. 더 넓은 파일 레이아웃 배경은 PDF 구조 이해를 참고하시고 Delphi에서 증분 업데이트 및 대용량 파일을 코드로 처리하는 워크플로는 Delphi에서 대용량 PDF 처리하기를 참조하십시오