기술 문서

PDF 선형화와 Fast Web View 작동 원리

링크로 200페이지짜리 PDF를 열면 브라우저가 파일 대부분이 도착할 때까지 빈 화면을 보여 주는 일은 흔합니다. Fast Web View는 선형화된 PDF(ISO 32000-1 부속서 F)를 부르는 Adobe Acrobat 이름입니다. 첫 페이지와 작은 색인이 파일 앞쪽에 앉아서, 뷰어는 첫 페이지를 일찍 그리고 이후 페이지는 HTTP Range 요청으로 가져옵니다. HotPDF와 PDFium Component는 둘 다 Delphi에서 선형화된 파일을 작성할 수 있습니다

이 가운데 어느 것도 별개의 파일 형식이 아닙니다. 선형화된 PDF는 규격을 따르는 어떤 리더든 특별한 처리 없이 여는 평범한 PDF이며, 차이는 바이트의 순서와 두 개의 추가 구조, 곧 선형화 매개변수 사전과 힌트 스트림에 있습니다. 리더가 그 혜택을 실감할지는 두 조건이 결정합니다. 파일이 선형화되어 그 상태를 유지해야 하는데, 덧붙은 편집이 그것을 조용히 무효로 만들고, 웹 서버가 바이트 범위 요청에 206 Partial Content로 답해야 합니다. 파일 끝으로 탐색하는 데 아무 값도 들지 않는 로컬 디스크에서는 선형화된 PDF가 다른 어떤 것과 똑같이 행동하므로, Fast Web View는 URL에서 곧장 여는 큰 문서에 결실을 내는 순수한 네트워크 최적화입니다

선형화된 PDF는 무엇을 재배열하는가?

선형화된 PDF는 첫 페이지가 필요로 하는 모든 것을 파일 시작 근처의 이어진 덩어리로 모아 다른 모든 페이지보다 앞에 둡니다. 보통의 PDF에서 객체는 어떤 순서로든 놓일 수 있습니다. 파일 끝의 상호 참조 표가 객체 번호마다 바이트 오프셋을 매겨 주기 때문입니다. 리더는 꼬리로 탐색해 startxref 포인터를 읽고 표를 적재한 뒤 무엇이든 찾아냅니다(PDF 파일 구조 개요가 이 머리, 본문, xref, 트레일러 배치를 훑어 줍니다). 이 설계는 디스크에서는 훌륭하고 네트워크에서는 형편없는데, 네트워크에서 꼬리는 정확히 가장 늦게 도착하는 부분이기 때문입니다. 첫 페이지를 그리려면 전통적인 리더에게 페이지 객체, 그 내용 스트림, 그것이 쓰는 글꼴과 이미지가 필요한데, 순서 없는 파일에서는 그중 무엇이든 마지막 메가바이트에 앉아 있을 수 있습니다

선형화는 그 순서를 바로잡습니다. 머리 뒤에 선형화 사전과 작은 첫 페이지 상호 참조 구역이 오고, 이어서 카탈로그 같은 문서 수준 객체와 주 힌트 스트림, 모든 자원을 갖춘 첫 페이지가 따라옵니다. 나머지 페이지들이 하나씩 뒤따르고, 그다음 이후 페이지들이 나눠 쓰는 객체들이, 맨 마지막에 주 상호 참조 표가 옵니다. 이 최적화를 무시하는 리더는 여전히 끝에서 시작해 주 표로 모든 객체를 찾으므로, 선형화된 파일이 구형 소프트웨어와 완전히 호환되는 까닭이 여기 있습니다. 덕분에 객체 번호는 hex 편집기에서 거꾸로 보입니다. 앞쪽 덩어리가 가장 큰 번호를 실고, 그 뒤의 모든 것은 1부터 번호가 매겨집니다

Fast Web View를 가능하게 하는 선형화된 PDF와 보통의 PDF의 바이트 배치 비교
선형화된 파일은 선형화 사전과 첫 페이지 xref와 힌트 스트림을 다른 무엇보다 앞으로 옮기고, 보통의 PDF는 유일한 색인을 파일 꼬리에 둡니다

선형화 사전과 힌트 표

%PDF 머리 뒤의 첫 객체는 선형화 매개변수 사전이며, 부속서 F는 그것이 첫 1024바이트 안에 앉기를 요구합니다. 그래야 스트리밍 리더가 열리는 패킷 안에서 그것을 찾습니다. 표 F.1이 엔트리를 나열합니다. /L은 파일 전체의 바이트 길이, /H는 주 힌트 스트림의 오프셋과 길이, /O는 첫 페이지의 객체 번호, /E는 첫 페이지가 끝나는 오프셋, /N은 페이지 수, /T는 주 상호 참조 표의 첫 엔트리 오프셋입니다. /L 엔트리는 보이는 것보다 무게가 큽니다. 실제 파일 크기와 어긋나면 표 F.1은 리더에게 이 문서를 평범한 PDF로 다루고 그 안의 모든 힌트를 무시하라고 말합니다

힌트 스트림은 40페이지로 건너뛰기를 값싸게 만드는 장치입니다. 힌트 스트림은 페이지마다 그 객체가 어디서 시작하고 몇 바이트를 덮는지 기록하는 페이지 오프셋 힌트 표와, 여러 페이지에 걸쳐 쓰이는 글꼴과 로고와 기타 자원에 대해 같은 일을 하는 공유 객체 힌트 표를 담습니다. 40페이지를 원하는 리더는 그 바이트 범위를 찾아 정확히 그 조각만 요청하고 도착하자마자 그리며, 아직 갖고 있지 않은 공유 객체도 같은 방식으로 당겨 옵니다. 두 디테일이 손수 만든 선형화기를 넘어뜨립니다. 힌트 오프셋은 논리적입니다. 힌트 스트림 자체가 없는 것으로 계산하므로, 물리적 오프셋을 저장하는 라이터는 이후의 모든 범위 요청을 힌트 객체 크기만큼 어긋나게 보냅니다. 그리고 자원을 모으는 동안 페이지의 /Parent 링크를 따라간 라이터는 페이지 트리를 거슬러 올라가 문서 전체를 첫 페이지 구역으로 끌고 들어옵니다. 두 파일 모두 여전히 선형화되었다고 주장합니다. 어느 쪽도 더 빨리 열리지는 않습니다

Fast Web View는 왜 서버의 도움이 필요한가?

Fast Web View는 서버가 요청에 따라 파일의 임의 조각을 전달할 때만 동작합니다. 리더는 Range 헤더를 실은 HTTP 요청을 보내고 206 Partial Content를 기대합니다. 서버가 Accept-Ranges: bytes를 알리지 않거나, 그 앞의 프록시나 CDN이 범위 요청을 전체 전송으로 뭉개 버리면, 리더는 40페이지를 따로 가져올 방법이 없어 파일 전체를 내려받고 만답니다. 그러면 PDF 안의 구조는 옳으면서도 통째로 낭비되며, 이것이 깨진 선형화로 가장 자주 오진되는 실패입니다. 문서를 다시 짓기 전에 뷰어가 여는 정확한 URL의 첫 킬로바이트를, 가령 curl -r 0-1023으로 요청해 상태가 206인지 확인하십시오. 정적 파일 호스트는 보통 기본으로 이것을 맞게 해 두지만, 자체 핸들러로 PDF를 스트리밍하는 애플리케이션 엔드포인트는 자주 그러지 않습니다

Fast Web View PDF의 한 페이지에 206 Partial Content를 돌려주는 HTTP 범위 요청을 이끄는 선형화 힌트 표
힌트 표는 리더에게 어느 페이지든 그 바이트 범위를 주므로, 범위를 지원하는 호스트는 부분 응답 하나로 40페이지를 섬깁니다. 바이트 서빙이 없으면 파일 전체가 다시 내려받힙니다

PDF가 Fast Web View를 갖췄는지 확인하는 방법

파일을 만들어 낸 도구를 믿지 말고 바이트를 확인하십시오. 그리고 조건의 두 절반을 모두 확인하십시오. 파일이 스스로 선형화되었다고 선언하는 것과, 그 선언이 아직 디스크 위의 파일과 맞는 것입니다. 데스크톱에서의 빠른 살피기부터 출판 파이프라인의 자동화 게이트까지, 세 가지 검사가 거의 모든 사례를 커버합니다:

  • Adobe Acrobat 또는 Reader: File > Properties를 열면 Description 탭의 오른쪽 아래에 Fast Web View: Yes 또는 No가 나타납니다. Acrobat은 구조가 실제로 있고 최신일 때만 Yes를 보고하므로, 선형화 후에 편집된 파일은 No를 보여 줍니다
  • 명령줄의 qpdf: qpdf --check-linearization은 힌트 표를 실제 객체 위치와 맞춰 보고, qpdf --show-linearization은 사전과 모든 힌트 표 엔트리를 덤프합니다
  • 파일의 첫 킬로바이트: 텍스트나 hex 편집기로 열어 첫 1024바이트 안에서 /Linearized 1을 담은 객체를 찾습니다. 그것이 있으면 /L 값을 운영체제가 보고하는 파일 크기와 비교합니다
# 선형화 사전과 힌트 표를 배치와 맞춰 검증합니다
qpdf --check-linearization report-web.pdf

# /L, /H, /O, /E, /N, /T와 모든 힌트 표 엔트리를 덤프합니다
qpdf --show-linearization report-web.pdf

파이프라인에 연결할 가치가 있는 검사는 qpdf 검사입니다. 힌트 표를 실제 객체 위치와 맞춰 보기 때문입니다. 사전의 존재만 확인하는 검사는 오프셋이 엉뚱한 바이트를 가리키는 파일도 기꺼이 축복해 줍니다. Delphi에서는 HotPDF의 THotPDF.IsLoadedLinearized가 로드된 문서가 스스로 선형화되었다고 선언하는지 알려주며, 해석 전에 첫 1024바이트에서 /Linearized를 찾습니다. PDFium Component는 클래스 함수 TPdf.InspectAvailability로 같은 질문에 답하는데, 그 Linearization 필드는 문서를 전혀 적재하지 않고 plnLinearized, plnNotLinearized, plnUnknown을 돌려줍니다. 두 플래그 모두 파일이 선언하는 바를 보고합니다. 그 선언이 아직 디스크 위의 바이트와 맞는지는 /L 비교가 말해 줍니다:

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

type
  TFastWebView = (fwvNo, fwvStale, fwvYes);

// 여러분만의 헬퍼: 파일이 선형화되었는지는 PDFium이 판정하고,
// /L 비교가 뒤에 덧붙은 편집을 잡아냅니다
function FastWebViewState(const FileName: string): TFastWebView;
var
  Stream: TFileStream;
  Head: AnsiString;
  Text: string;
  P, Q: Integer;
begin
  Result := fwvNo;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if TPdf.InspectAvailability(Stream).Linearization <> plnLinearized then
      Exit;
    SetLength(Head, Min(Stream.Size, 1024));   // 부속서 F: 첫 1024바이트
    Stream.Position := 0;
    if Length(Head) > 0 then
      Stream.ReadBuffer(Head[1], Length(Head));
    Text := string(Head);
    P := Pos('/Linearized', Text);
    if P = 0 then
      Exit;
    P := PosEx('/L ', Text, P + Length('/Linearized'));
    if P = 0 then
      Exit;
    Inc(P, 3);
    Q := P;
    while (Q <= Length(Text)) and CharInSet(Text[Q], ['0'..'9']) do
      Inc(Q);
    if StrToInt64Def(Copy(Text, P, Q - P), -1) = Stream.Size then
      Result := fwvYes
    else
      Result := fwvStale;   // 한번 선형화된 뒤 수정됨
  finally
    Stream.Free;
  end;
end;

Acrobat, qpdf, Delphi에서 Fast Web View 켜기

부속서 F 순서로 파일 전체를 다시 쓰는 도구라면 무엇이든 Fast Web View를 켤 수 있고, 가장 중요한 것은 그것이 마지막에 돈다는 점입니다. Acrobat에서는 Preferences > Documents 아래의 Save As optimizes for Fast Web View를 켜고 Save 대신 File > Save As를 씁니다. 평범한 Save는 증분 갱신을 덧붙일 수 있기 때문입니다. 서버나 빌드 스크립트에서는 qpdf --linearize in.pdf out.pdf가 어떤 PDF든 선형화된 형태로 다시 쓰고 입력은 손대지 않습니다. Delphi 코드에는 문서를 직접 생성하느냐, 받아서 다루느냐에 따라 두 경로가 있습니다. HotPDF는 THotPDF.LinearizeOutput이 True일 때 선형화된 출력을 곧장 작성합니다. BeginDoc 전에 다른 출력 옵션과 함께 설정하면, 부속서 F 배치가 EndDoc 안에서 계산되고 작성됩니다:

qpdf --linearize report-final.pdf report-web.pdf
var
  PDF: THotPDF;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.AutoLaunch := False;       // EndDoc 뒤에 뷰어를 열지 않습니다
    PDF.FileName := 'catalog-web.pdf';
    PDF.LinearizeOutput := True;   // BeginDoc 전에 설정합니다
    PDF.BeginDoc;                  // BeginDoc 뒤에 페이지 1이 존재합니다
    PDF.CurrentPage.TextOut(72, 72, 0, 'Spring catalog');
    PDF.AddPage;
    PDF.CurrentPage.TextOut(72, 72, 0, 'Price list');
    PDF.EndDoc;                    // 선형화된 배치가 여기서 작성됩니다
  finally
    PDF.Free;
  end;
end;

이미 존재하는 파일에는 PDFium Component가 TPdf.SaveAsLinearized를 제공합니다. 이 메서드는 평범한 PDFium 저장을 중간 저장소로 실행하므로 페이지 편집과 양식 값은 다른 저장과 같은 경로를 거치고, 그 출력을 타깃에 쓰기 전에 부속서 F에 따라 재구성합니다. SaveAs와 같은 TSaveOption과 TPdfVersion 매개변수를 받고, saIncremental과 PDF 1.2 미만 버전은 거부하며, 파일 이름 오버로드는 저장이 성공한 뒤에만 타깃을 대체하는 임시 파일에 씁니다. 라이터가 현재 리비전에서 파일을 다시 빌드하므로, 덧붙은 편집을 떠안은 파일에 Fast Web View를 되살리는 방법이기도 합니다. 암호화된 입력에는 saRemoveSecurity가 필요합니다. 객체 번호를 다시 매기면 표준 보안 핸들러의 객체별 키(ISO 32000-1 §7.6.2)가 무효가 되므로, 웹용 사본은 암호화 없이 작성됩니다

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'manual-rev3.pdf';   // 덧붙은 편집을 실어 가고 있습니다
    Pdf.Active := True;
    if not Pdf.SaveAsLinearized('manual-web.pdf', saNoIncremental) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

증분 갱신은 Fast Web View를 조용히 끕니다

제대로 선형화된 파일이 나중에 Fast Web View를 잃는 가장 흔한 이유가 증분 갱신입니다. 도구가 증분 저장으로 서명을 더하거나 양식 필드를 채우거나 스티키 노트를 붙일 때, 원래 바이트는 손대지 않고 바뀐 객체와 새 상호 참조 구역과 새 트레일러를 끝에 덧붙입니다. 앞쪽의 선형화 사전은 여전히 낡은 배치를 묘사하고, 그 /L은 더는 파일 크기와 맞지 않으며, 규격을 따르는 리더는 선형화된 구조가 여전히 파일 앞부분에 온전히 앉아 있는데도 이 문서를 평범한 PDF로 다루는 쪽으로 물러납니다. HotPDF는 BeginIncrementalUpdate와 SaveIncrementalUpdate로 정확히 이런 종류의 개정판을 덧붙이는데, 문서가 아직 바뀌는 동안에는 옳은 도구이지만 웹 서버로 향하는 파일의 마지막 단계로는 잘못된 도구입니다:

// 개정판을 덧붙입니다: 빠르고, 앞선 바이트를 온전히 지키며,
// 결과는 Fast Web View 없이 남깁니다
PDF.BeginIncrementalUpdate('report.pdf');
PDF.AddPage;
PDF.CurrentPage.TextOut(72, 760, 0, 'Addendum');
PDF.SaveIncrementalUpdate('report-rev2.pdf');
증분 갱신이 개정판을 덧붙여 선형화 사전을 어긋나게 만들다가 마지막 전면 재작성 한 번이 Fast Web View를 되살리는 그림
증분 저장은 낡은 앞쪽 사전 너머로 개정판을 하나씩 쌓습니다. 전면 재작성으로 마무리하고 선형화를 맨 마지막에 다시 하면 Fast Web View가 온전히 남습니다

현실적인 규칙은 문서가 계속 바뀌는 동안에는 증분으로 편집하고, 맨 마지막 단계로 한 번 선형화하며, 아무도 열기 전에 메일로 보내지거나 내려받히는 파일은 앞쪽에 실린 더 큰 색인에서 얻는 것이 없으니 Fast Web View를 끄고 두는 것입니다. 서명된 문서는 진짜 선택을 강요합니다. 전면 재작성은 모든 서명의 /ByteRange가 덮는 바이트를 바꾸고, 그 뒤의 서명은 /L 어긋남을 곧바로 되돌려 오는 갱신을 덧붙이므로, 계약서라면 서명을 지키고 전체 내려받기를 받아들입니다. 부속서 F의 작성 쪽, 곧 첫 바이트가 작성되기 전에 오프셋이 어떻게 수렴하는지와 선형화된 출력이 객체 스트림을 포기하는 이유까지는 HotPDF 힌트 표로 선형화된 PDF 작성하기와 Delphi에서 PDFium Component로 Fast Web View 선형화하기에서 다룹니다. LinearizeOutput과 IsLoadedLinearized는 HotPDF Delphi Component에, SaveAsLinearized와 InspectAvailability는 Delphi, C++Builder, Lazarus용 PDFium Component에 실려 나옵니다