Artigo Técnico

Linearizar PDFs para Fast Web View com PDFium em Delphi

O PDFium Component produz PDFs Fast Web View (linearizados) a partir de Delphi através do TPdf.SaveAsLinearized. O método grava o documento atual com o PDFium, e depois reescreve o resultado de acordo com o Anexo F da ISO 32000-1: os objetos da primeira página movem-se para a frente, escrevem-se duas secções de cross-reference, e as hint tables de offset de página e de objetos partilhados dizem a um leitor que intervalos de bytes ir buscar para qualquer página

A razão pela qual alguém se importa é a rede. Um catálogo de 40 MB servido de object storage ou de um portal de documentos devia mostrar a página um depois dos primeiros quilobytes, e saltar para a página 380 devia custar uns pedidos HTTP Range, não um download completo. Os leitores só o conseguem fazer quando o ficheiro está linearizado. A maioria dos PDFs que saem de um pipeline de edição não está, porque a forma natural de escrever um PDF põe a tabela de cross-reference no fim e espalha os objetos de página onde o escritor calhassem emitir

Porque é que o PDFium lê ficheiros linearizados mas não os escreve?

O PDFium tem um leitor completo de linearização e nenhum escritor para ela. A API de disponibilidade FPDFAvail_* deteta o dicionário de linearização, descodifica hint tables, e pede ao anfitrião exatamente os intervalos de bytes de que precisa, que é sobre o que se constrói o workflow de download progressivo e cancelamento no PDFium Component. A ABI pública de gravação, por outro lado, só oferece uma gravação de cópia simples e uma gravação com versão PDF alvo. Nada nela reordena objetos ou emite hint streams

O PDFium Component fecha essa lacuna sem reimplementar o que o PDFium já faz bem. O TPdf.SaveAsLinearized primeiro chama o SaveAs existente, por isso o PDFium continua a ser dono da gravação semântica: páginas editadas, estado de campos de formulário e tratamento de versão passam todos pelo caminho de código exercitado durante anos. O componente depois corre uma passagem de reestruturação do Anexo F sobre esse PDF intermédio estável e escreve o resultado linearizado no seu alvo. Dividir o trabalho desta forma significa que a linearização nunca tem de compreender uma anotação ou um formulário, apenas objetos, referências e offsets de bytes

Chamar TPdf.SaveAsLinearized a partir de Delphi

O TPdf.SaveAsLinearized tem dois overloads, um para um TStream e um para um nome de ficheiro, e ambos levam os mesmos parâmetros opcionais Option: TSaveOption e PdfVersion: TPdfVersion que o SaveAs. Ambos devolvem True em sucesso. O overload de ficheiro escreve para um ficheiro temporário no diretório alvo e substitui o destino atomicamente, por isso uma gravação falhada nunca deixa um PDF meio escrito onde estava um bom

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.Active := True;
    // ... editar páginas, preencher formulários, carimbar, o que o pipeline fizer ...
    if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

As regras dos parâmetros são estritas de propósito. O saIncremental levanta um erro, porque uma atualização incremental acrescenta uma nova revisão ao fim do ficheiro e destruiria imediatamente o layout carregado para a frente. Uma versão alvo abaixo do PDF 1.2 também é recusada; o pvUnknown mantém a versão da origem. Entrada encriptada precisa de saRemoveSecurity, já que o escritor renumera objetos e as chaves de encriptação ao nível de objeto derivam do número do objeto. E porque a linearização é uma reescrita completa, qualquer assinatura digital existente é invalidada. Se tem de entregar assinado e linearizado, linearize primeiro e assine depois

Como o escritor agrupa objetos para Fast Web View

Linearização não é só pôr a página um no topo do ficheiro. O escritor analisa a revisão atual, quer os seus dados de cross-reference sejam uma tabela clássica, um ficheiro híbrido ou um cross-reference stream, e expande cada membro de um object stream ativo num objeto autónomo. Constrói depois um grafo de referências e ordena os objetos em cinco grupos: objetos ao nível do documento necessários para abrir o ficheiro, objetos da primeira página, objetos privados de uma página posterior, objetos partilhados por várias páginas posteriores, e todo o resto

A armadilha nessa travessia do grafo é o /Parent. Cada dicionário de página aponta para cima na árvore de páginas, e uma travessia ingénua segue esse ponteiro até ao nó pai, e depois desce pelo /Kids para dentro de cada página irmã e dos seus content streams. Um passo depois o grupo da primeira página contém o documento inteiro, e o ficheiro está tecnicamente linearizado mas é inútil para acesso range-first. O PDFium Component ignora a ligação /Parent da página e os nós da árvore de páginas fora da página a recolher, que é o que mantém a secção da primeira página pequena. Numa amostra de regressão de 51 páginas com uma árvore de páginas profunda, a secção da primeira página são 5289 bytes, 0.48% do output de 1105354 bytes

Object streams e cross-reference streams na entrada são achatados em objetos indiretos ordinários de geração 0, cada referência é reescrita para os novos números, e o output usa secções de cross-reference clássicas. Isso custa algum tamanho de ficheiro em comparação com object streams comprimidos, mas simplifica a cobertura das hint tables e é o layout que todo o leitor consciente de linearização trata. Se a compactação importa mais do que Fast Web View para um dado output, o PDFium Component tem um escritor de object streams separado para esse trabalho

O que entra no dicionário de linearização e nas duas secções xref?

Um ficheiro linearizado escrito pelo PDFium Component traz duas secções de cross-reference clássicas e um dicionário de linearização no próprio primeiro objeto. A primeira secção xref segue o dicionário de linearização e cobre os objetos ao nível do documento e da primeira página, por isso um leitor consegue renderizar a página um a partir da frente do ficheiro sozinha. A secção xref principal assenta no fim e cobre todo o resto

O dicionário de linearização em si guarda os números de que um leitor precisa antes de ler qualquer outra coisa: /L o comprimento do ficheiro, /H o offset e o comprimento do hint stream, /O o número do objeto da primeira página, /E o fim da secção da primeira página, /N a contagem de páginas, e /T o offset da primeira entrada na tabela xref principal. Todos dependem do layout final, e o dicionário assenta antes do layout. Escrevê-lo com números de largura natural cria um loop: um comprimento que cresce de 999999 para 1000000 acrescenta um byte, desloca todos os offsets seguintes, o que pode mudar a largura de outro número, e por aí fora

O PDFium Component parte o loop reservando 320 bytes fixos para o dicionário de linearização. Todos os objetos, o hint stream, ambas as secções xref e os trailers são escritos primeiro; uma vez todos os offsets finais, o dicionário é preenchido no lugar e acolchoado até à largura reservada. Um detalhe subtil é o /T: tem de apontar para a entrada do objeto 0 da tabela xref principal, não para a palavra-chave xref que a precede. Validadores estritos assinalam o offset da palavra-chave embora leitores indulgentes abram o ficheiro na mesma

Porque é que os offsets das hint tables ignoram o hint stream?

Offsets de hint tables num PDF linearizado interpretam-se como se o hint stream não existisse no ficheiro. O Anexo F define-o assim para que o hint stream se possa sentar entre a secção da primeira página e o resto sem o seu próprio tamanho a realimentar os números que contém. A consequência prática para um escritor é fácil de errar: qualquer offset real que fique depois do hint stream tem de ser reduzido antes de entrar na hint table de offset de página ou de objetos partilhados

A questão é reduzido por quanto. A resposta tentadora é o comprimento dos dados do stream, porque é a parte que parece o conteúdo da hint. A resposta correta é o comprimento do objeto indireto da hint inteiro, incluindo N 0 obj, o dicionário do stream, as palavras-chave stream e endstream, e o endobj. Subtraia só o comprimento dos dados e todos os offsets posteriores ficam errados em algumas dezenas de bytes. A página um continua a renderizar porque vive antes do hint stream, por isso o bug sobrevive a uma verificação visual rápida e só aparece quando um leitor salta para uma página posterior e pede um intervalo que começa a meio do objeto errado

Ambas as hint tables são bit-packed com o bit mais significativo primeiro, de novo conforme o Anexo F. O componente verifica o resultado da forma como um cliente real o faria: um armazenamento de intervalos esparso fornece só o primeiro 1 KiB da amostra de 51 páginas, e o tratamento nativo de hints do PDFium pede o resto. A última página abre sem o ficheiro completo nunca estar presente

Verificar um PDF linearizado em Delphi

A verificação mais barata é perguntar ao próprio PDFium. O TPdf.InspectAvailability corre a API de disponibilidade contra um stream sem carregar o documento, e o seu campo Linearization reporta plnLinearized, plnNotLinearized ou 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;

Isso confirma que o PDFium reconhece o layout; não prova que as hints estão corretas. Para um teste real de ponta a ponta, sirva o ficheiro através de um transporte limitado a intervalos e abra uma página tardia com TPdfProgressiveDocument, da mesma forma que o guia de streaming de PDFs grandes a pedido com PDFium VCL faz. Se a página renderiza depois de ir buscar só uma pequena fração do ficheiro, as hint tables estão a fazer o seu trabalho. Acrescentar um segundo verificador de linearização independente ao pipeline de lançamento também vale a pena, porque os leitores diferem na indulgência com um /T fora do sítio ou um offset ligeiramente errado

Dois limites a ter em mente. O materializador de objetos ainda recusa arrays de 2 GiB ou mais, por isso documentos extremamente grandes precisam por agora de uma estratégia diferente. E a linearização só compensa quando o servidor honra pedidos HTTP Range; atrás de um proxy que retire Accept-Ranges, um ficheiro linearizado faz download exatamente como qualquer outro

FAQ

O TPdf.SaveAsLinearized mantém as assinaturas digitais válidas?

Não. O TPdf.SaveAsLinearized renumera e reescreve todos os objetos, por isso as assinaturas existentes são invalidadas. Linearize primeiro, depois aplique a assinatura

O PDFium Component lineariza um PDF encriptado?

Só quando passa saRemoveSecurity. As chaves de encriptação ao nível de objeto dependem dos números de objeto, que o escritor muda, por isso o output é escrito sem encriptação

Que versões PDF o output linearizado suporta?

O TPdf.SaveAsLinearized aceita pvUnknown para manter a versão da origem ou qualquer alvo a partir do PDF 1.2 para cima, e recusa o saIncremental porque uma revisão acrescentada partiria o layout

O output Fast Web View, o carregamento progressivo e a API de disponibilidade fazem todos parte do PDFium Component for Delphi and C++Builder, que embrulha o PDFium para projetos Delphi, C++Builder e Lazarus