Artigo Técnico

Linearização de PDF e Fast Web View: Como Funciona

Abra um PDF de 200 páginas a partir de um link e o navegador muitas vezes fica em branco até que a maior parte do arquivo chegue. Fast Web View é o nome que o Adobe Acrobat dá a um PDF linearizado (Anexo F da ISO 32000-1): a página um e um pequeno índice ficam na frente, então um visualizador pinta a página um cedo e busca as páginas seguintes por requisição HTTP Range. HotPDF e PDFium Component podem ambos escrever arquivos linearizados a partir do Delphi

Nada disso é um formato de arquivo separado. Um PDF linearizado é um PDF comum que qualquer leitor em conformidade abre sem tratamento especial; a diferença está na ordem dos bytes e em duas estruturas extras, o dicionário de parâmetros de linearização e o hint stream. Duas condições decidem se um leitor sente o benefício algum dia. O arquivo precisa estar linearizado e continuar assim, o que uma edição anexada desfaz silenciosamente, e o servidor web precisa responder a requisições de byte-range com 206 Partial Content. Em um disco local, onde seek até o fim de um arquivo não custa nada, um PDF linearizado se comporta exatamente como qualquer outro, então o Fast Web View é puramente uma otimização de rede que compensa para documentos grandes abertos direto de uma URL

O que um PDF linearizado reorganiza?

Um PDF linearizado reúne tudo o que a página um precisa em um bloco contíguo perto do início do arquivo, à frente de todas as outras páginas. Em um PDF comum os objetos podem ficar em qualquer ordem, porque a tabela de referência cruzada no fim mapeia cada número de objeto ao seu offset de byte: um leitor faz seek até a cauda, lê o ponteiro startxref, carrega a tabela e então pode encontrar qualquer coisa (a visão geral da estrutura de arquivos PDF percorre esse layout de header, body, xref e trailer). O design é excelente em disco e ruim em rede, onde a cauda é exatamente a parte que chega por último. Para desenhar a página um, um leitor convencional precisa do objeto de página, do seu content stream e das fontes e imagens que ele usa, e em um arquivo sem ordem qualquer um deles pode estar no megabyte final

A linearização corrige a ordem. Depois do header vêm o dicionário de linearização e uma pequena seção de referência cruzada da primeira página, então os objetos de nível de documento como o catálogo, o hint stream primário e a primeira página com todos os seus recursos. As páginas restantes seguem uma a uma, depois os objetos compartilhados entre páginas posteriores, e por último a tabela de referência cruzada principal. Um leitor que ignora a otimização ainda começa pelo fim e encontra cada objeto pela tabela principal, razão pela qual um arquivo linearizado continua totalmente compatível com software mais antigo. A numeração de objetos parece invertida em um editor hexadecimal como resultado: o bloco da frente carrega os números mais altos, enquanto tudo atrás dele é numerado a partir de 1

Layout de bytes de um PDF comum comparado com um PDF linearizado que habilita o Fast Web View
Um arquivo linearizado move o dicionário de linearização, o xref da primeira página e o stream de hints para a frente de todo o resto, enquanto um PDF comum mantém seu único índice na cauda do arquivo

O dicionário de linearização e as tabelas de hints

O primeiro objeto depois do header %PDF é o dicionário de parâmetros de linearização, e o Anexo F exige que ele esteja dentro dos primeiros 1024 bytes para que um leitor em streaming o encontre no pacote inicial. A Tabela F.1 lista suas entradas: /L é o comprimento do arquivo inteiro em bytes, /H o offset e o comprimento do hint stream primário, /O o número de objeto da primeira página, /E o offset onde a primeira página termina, /N a contagem de páginas, e /T o offset da primeira entrada na tabela de referência cruzada principal. A entrada /L carrega mais peso do que aparenta. Quando ela diverge do tamanho real do arquivo, a Tabela F.1 manda o leitor tratar o documento como um PDF comum e ignorar toda hint dentro dele

O hint stream é o que torna um salto para a página 40 barato. Ele guarda uma tabela de hints de offset de página, que registra para cada página onde seus objetos começam e quantos bytes eles cobrem, e uma tabela de hints de objeto compartilhado que faz o mesmo para fontes, logos e outros recursos usados em várias páginas. Um leitor que quer a página 40 consulta o byte range dela, pede exatamente essa fatia e renderiza assim que ela chega, puxando do mesmo jeito qualquer objeto compartilhado que ainda não tenha. Dois detalhes derrubam linearizadores caseiros. Os offsets de hints são lógicos, calculados como se o próprio hint stream estivesse ausente, então um writer que armazena offsets físicos manda toda requisição de range posterior errada pelo tamanho do objeto de hints. E um writer que segue o link /Parent de uma página ao coletar os recursos dela desce pela árvore de páginas e arrasta o documento inteiro para a seção da primeira página. Os dois arquivos continuam alegando estar linearizados. Nenhum carrega mais rápido

Por que o Fast Web View precisa da ajuda do servidor?

O Fast Web View só funciona quando o servidor entrega fatias arbitrárias do arquivo sob demanda. O leitor envia requisições HTTP com um header Range e espera receber 206 Partial Content; se o servidor nunca anuncia Accept-Ranges: bytes, ou um proxy ou CDN na frente dele colapsa requisições de range em transferências completas, o leitor não consegue buscar a página 40 por conta própria e baixa o arquivo inteiro em vez disso. A estrutura dentro do PDF então está correta e totalmente desperdiçada, e essa é a falha mais frequentemente diagnosticada errado como linearização quebrada. Antes de reconstruir um documento, requisite o primeiro kilobyte da URL exata que o visualizador abre, por exemplo com curl -r 0-1023, e confirme que o status é 206. Hosts de arquivos estáticos normalmente acertam isso por padrão, enquanto endpoints de aplicação que servem PDFs pelo próprio handler frequentemente não

Tabelas de hints de linearização dirigindo uma requisição HTTP de range que retorna 206 Partial Content para uma página de um PDF com Fast Web View
As tabelas de hints dão ao leitor o intervalo de bytes de qualquer página, então um host capaz de ranges atende a página 40 com uma única resposta parcial; sem byte-serving, o arquivo inteiro é baixado de novo

Como verificar se um PDF tem Fast Web View?

Verifique os bytes em vez de confiar na ferramenta que produziu o arquivo, e verifique as duas metades da condição: o arquivo se declara linearizado, e essa declaração ainda bate com o arquivo no disco. Três verificações cobrem quase todo caso, de uma olhada rápida no desktop a um gate automatizado em um pipeline de publicação:

  • Adobe Acrobat ou Reader: abra File > Properties; a aba Description mostra Fast Web View: Yes ou No no canto inferior direito. O Acrobat reporta Yes apenas quando a estrutura está presente e atual, então um arquivo editado depois da linearização mostra No
  • qpdf na linha de comando: qpdf --check-linearization valida as tabelas de hints contra as posições reais dos objetos, e qpdf --show-linearization despeja o dicionário e cada entrada de tabela de hints
  • O primeiro kilobyte do arquivo: abra-o em um editor de texto ou hexadecimal e procure um objeto contendo /Linearized 1 dentro dos primeiros 1024 bytes; quando ele estiver lá, compare o valor /L dele com o tamanho de arquivo reportado pelo sistema operacional
# Valida o dicionário de linearização e as tabelas de hints contra o layout
qpdf --check-linearization report-web.pdf

# Despeja /L, /H, /O, /E, /N, /T e cada entrada de tabela de hints
qpdf --show-linearization report-web.pdf

A verificação do qpdf é a que vale a pena ligar a um pipeline, porque ela reconcilia as tabelas de hints com as posições reais dos objetos; uma verificação que só confirma que o dicionário existe vai abençoar um arquivo cujos offsets apontam para os bytes errados. A partir do Delphi, o THotPDF.IsLoadedLinearized do HotPDF reporta se um documento carregado se declara linearizado, escaneando os primeiros 1024 bytes por /Linearized antes de fazer o parse. O PDFium Component responde à mesma pergunta pela class function TPdf.InspectAvailability, cujo campo Linearization retorna plnLinearized, plnNotLinearized ou plnUnknown sem carregar o documento de forma alguma. Ambas as flags reportam o que o arquivo declara; a comparação do /L diz se essa declaração ainda bate com os bytes no disco:

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

type
  TFastWebView = (fwvNo, fwvStale, fwvYes);

// Seu próprio helper: o PDFium decide se o arquivo está linearizado,
// e a comparação do /L captura edições anexadas depois
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));   // Anexo F: primeiros 1024 bytes
    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;   // linearizado uma vez, depois modificado
  finally
    Stream.Free;
  end;
end;

Ativando o Fast Web View no Acrobat, no qpdf e no Delphi

Qualquer ferramenta que reescreve o arquivo inteiro na ordem do Anexo F pode ligar o Fast Web View, e o que mais importa é que ela rode por último. No Acrobat, habilite Save As optimizes for Fast Web View em Preferences > Documents e então use File > Save As em vez de Save, porque um Save simples pode anexar uma atualização incremental. Em um servidor ou em um script de build, qpdf --linearize in.pdf out.pdf reescreve qualquer PDF em forma linearizada e deixa a entrada intocada. Código Delphi tem duas rotas, dependendo de você gerar o documento ou recebê-lo. O HotPDF escreve saída linearizada diretamente quando o THotPDF.LinearizeOutput é True; defina-o com as outras opções de saída antes do BeginDoc, e o layout do Anexo F é calculado e escrito dentro do EndDoc:

qpdf --linearize report-final.pdf report-web.pdf
var
  PDF: THotPDF;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.AutoLaunch := False;       // não abre um visualizador depois do EndDoc
    PDF.FileName := 'catalog-web.pdf';
    PDF.LinearizeOutput := True;   // defina antes do BeginDoc
    PDF.BeginDoc;                  // a página 1 existe depois do BeginDoc
    PDF.CurrentPage.TextOut(72, 72, 0, 'Spring catalog');
    PDF.AddPage;
    PDF.CurrentPage.TextOut(72, 72, 0, 'Price list');
    PDF.EndDoc;                    // o layout linearizado é escrito aqui
  finally
    PDF.Free;
  end;
end;

Para arquivos que já existem, o PDFium Component oferece o TPdf.SaveAsLinearized. Ele roda o salvamento PDFium comum para um armazenamento intermediário, então edições de página e valores de formulário passam pelo mesmo caminho de qualquer outro salvamento, e depois reestrutura essa saída de acordo com o Anexo F antes de escrever o seu destino. O método aceita os mesmos parâmetros TSaveOption e TPdfVersion do SaveAs, rejeita saIncremental e qualquer versão abaixo de PDF 1.2, e sua sobrecarga com nome de arquivo escreve para um arquivo temporário que substitui o destino só depois de o salvamento ter sucesso. Como o writer reconstrói o arquivo a partir da revisão atual, ele também é o jeito de restaurar o Fast Web View em um arquivo que acumulou edições anexadas. Entrada criptografada precisa de saRemoveSecurity: renumerar objetos invalida as chaves por objeto do security handler padrão (ISO 32000-1 §7.6.2), então a cópia web é escrita sem criptografia

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'manual-rev3.pdf';   // carrega edições anexadas
    Pdf.Active := True;
    if not Pdf.SaveAsLinearized('manual-web.pdf', saNoIncremental) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

Atualizações incrementais desligam o Fast Web View silenciosamente

Uma atualização incremental é o motivo mais comum de um arquivo corretamente linearizado perder o Fast Web View depois. Quando uma ferramenta adiciona uma assinatura, preenche um campo de formulário ou anexa uma nota adesiva por um salvamento incremental, ela deixa os bytes originais intocados e anexa ao fim os objetos alterados, uma nova seção de referência cruzada e um novo trailer. O dicionário de linearização na frente ainda descreve o layout antigo, o /L dele não bate mais com o tamanho do arquivo, e um leitor em conformidade volta a tratar o documento como um PDF comum, mesmo que a estrutura linearizada continue intacta na primeira parte do arquivo. O HotPDF anexa exatamente esse tipo de revisão por meio do BeginIncrementalUpdate e do SaveIncrementalUpdate, que é a ferramenta certa enquanto o documento ainda muda e o passo final errado para um arquivo destinado a um servidor web:

// Anexa uma revisão: rápido, mantém os bytes anteriores intactos,
// e deixa o resultado sem Fast Web View
PDF.BeginIncrementalUpdate('report.pdf');
PDF.AddPage;
PDF.CurrentPage.TextOut(72, 760, 0, 'Addendum');
PDF.SaveIncrementalUpdate('report-rev2.pdf');
Atualizações incrementais anexam revisões que dessincronizam o dicionário de linearização até uma reescrita final restaurar o Fast Web View
Cada salvamento incremental empilha mais uma revisão além do dicionário frontal desatualizado; terminar com uma reescrita completa e relinearizar por último mantém o Fast Web View intacto

A regra viável é editar incrementalmente enquanto o documento está em mudança, linearizar uma única vez como o último passo, e deixar o Fast Web View desligado para arquivos que são enviados por e-mail ou baixados antes de alguém abri-los, já que eles não ganham nada com o índice maior carregado na frente. Documentos assinados forçam uma escolha real: uma reescrita completa muda os bytes que o /ByteRange de cada assinatura cobre, e assinar depois anexa uma atualização que traz a divergência do /L de volta direto, então para contratos mantenha a assinatura e aceite downloads completos. O lado de escrita do Anexo F, incluindo como os offsets convergem antes de o primeiro byte ser escrito e por que a saída linearizada abre mão de object streams, está coberto em escrever PDFs linearizados com tabelas de hints do HotPDF e linearização Fast Web View com PDFium Component em Delphi. O LinearizeOutput e o IsLoadedLinearized vêm com o HotPDF Delphi Component, enquanto o SaveAsLinearized e o InspectAvailability fazem parte do PDFium Component para Delphi, C++Builder e Lazarus