O HotPDF escreve arquivos PDF linearizados, o layout que o Acrobat chama de Fast Web View, através da propriedade LinearizeOutput em THotPDF. Defini-la antes de BeginDoc faz o HotPDF reordenar o grafo de objetos finalizado para que um leitor ciente de intervalos de bytes consiga exibir a página um depois de buscar apenas a parte inicial do arquivo, em vez de baixar o documento inteiro primeiro. O mecanismo é o Anexo F da ISO 32000-1
O motivo pelo qual isso importa é pouco glamoroso. Um PDF normal coloca sua tabela de referência cruzada no final, então um visualizador precisa alcançar o último byte antes de saber onde qualquer coisa está. Entregue a um navegador um relatório digitalizado de 200 páginas e o usuário fica olhando para um spinner durante a transferência inteira, mesmo que a única coisa que queria fosse a página 1. A linearização corrige isso pagando um custo no momento da escrita. Este artigo trata especificamente desse caminho de escrita, o particionamento, o laço de medição e os limites rígidos; para o contexto conceitual sobre o que o Fast Web View proporciona, a explicação anterior sobre linearização de PDF e Fast Web View cobre esse terreno
O que o layout linearizado realmente garante
Um arquivo linearizado é um PDF comum com uma ordenação física extremamente específica, e toda garantia que ele oferece vem dessa ordenação em vez de qualquer novo tipo de objeto. O HotPDF emite as partes na sequência que o Anexo F prescreve: o dicionário de parâmetros de linearização dentro dos primeiros 1024 bytes, uma tabela de referência cruzada antecipada, os objetos de nível de documento, o hint stream primário, a primeira página e seus objetos privados, depois as páginas restantes, depois os objetos compartilhados, depois tudo o mais, e finalmente a tabela de referência cruzada principal
O particionamento é derivado, não declarado. O HotPDF percorre o grafo de referências a partir de cada objeto de página e registra, para cada objeto indireto, quantas páginas o alcançam e qual página o alcançou primeiro. Um objeto usado por exatamente uma página se torna privado dessa página. Um objeto alcançado por mais de uma se torna compartilhado. O catálogo, mais o que ele referencia sob /ViewerPreferences, /OpenAction, /Threads e /AcroForm, mais o dicionário de criptografia quando a proteção está ativa, formam o grupo de nível de documento que deve preceder tudo. Os nós da árvore de páginas são deliberadamente retidos para não poluir a seção da primeira página
O dicionário de parâmetros carrega os números que um leitor precisa antes de ter lido qualquer outra coisa: /L para o comprimento total do arquivo, /H para o offset e comprimento do hint stream, /O para o número de objeto da primeira página, /E para o byte onde a seção da primeira página termina, /N para a contagem de páginas e /T para o offset da entrada da tabela de referência cruzada principal. Cada um desses é um offset de byte em um arquivo que ainda não existe no momento em que você precisa escrevê-los
Por que os offsets da tabela de hint precisam convergir?
Porque os números no dicionário de parâmetros descrevem o arquivo que os contém, e alterar qualquer um deles altera o arquivo. Essa é a dificuldade central de um escritor linearizado, e é por isso que o HotPDF mede repetidamente em vez de escrever uma única vez. Amplie /T de 6 dígitos para 7 e o dicionário de parâmetros cresce um byte; o cabeçalho cresce; cada objeto se desloca; a tabela de referência cruzada principal se move; /T agora precisa de um valor diferente. O layout precisa alcançar um ponto fixo antes de um único byte de saída real ser gravado
O HotPDF trata isso com uma iteração limitada. Primeiro ele serializa cada objeto em um stream de contagem que registra o comprimento sem manter os bytes, então cada objeto tem um tamanho serializado conhecido. Depois ele executa uma passagem de layout que atribui offsets ao grupo de nível de documento, ao hint stream, ao grupo da primeira página, aos grupos de páginas posteriores, ao grupo compartilhado e ao restante, e reporta onde a tabela de referência cruzada principal cairia. Esse resultado é realimentado como entrada para a próxima passagem. O laço é limitado a oito tentativas, e a não convergência dispara uma exceção em vez de produzir um arquivo com offsets errados mas plausíveis
CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
CalculateLayout(CandidateMainOffset, FirstXRefData,
HintOffset, EndFirstPage, NewMainOffset);
if NewMainOffset = CandidateMainOffset then
Break;
CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
raise Exception.Create('Linearization layout did not converge');
Dois detalhes impedem o laço de oscilar. O dicionário de parâmetros é escrito em um slot fixo de 384 bytes, preenchido com espaços, então seu próprio crescimento nunca pode desestabilizar o layout; se o texto do dicionário algum dia excedesse essa reserva, o HotPDF dispara exceção em vez de silenciosamente deslocar tudo. E após a convergência, o HotPDF executa mais uma passagem de layout de confirmação e reverifica o comprimento do hint stream, porque o próprio hint stream codifica offsets que só eram conhecidos assim que o layout se estabilizava. O ganho de toda essa medição é que o HotPDF nunca armazena uma segunda cópia do documento: uma vez que os offsets estão fixados, os objetos são serializados diretamente no stream de destino, com uma asserção em cada limite de seção conferindo se os bytes escritos correspondem ao offset que foi prometido
Ativando a partir do Delphi
A superfície de API é um único Boolean, e sua única exigência é que você o defina antes de a geração começar. LinearizeOutput tem padrão False, e a passagem de layout roda quando o documento é escrito, então atribuí-lo depois de EndDoc não realiza nada
var
PDF: THotPDF;
begin
PDF := THotPDF.Create(nil);
try
PDF.FileName := 'fast-view.pdf';
PDF.Version := pdf17;
PDF.LinearizeOutput := True; // must precede BeginDoc
PDF.BeginDoc;
PDF.Canvas.TextOut(72, 72, 'First page');
PDF.EndDoc;
finally
PDF.Free;
end;
end;
Uma ressalva de implantação supera tudo do lado do código. A linearização só compensa quando o transporte suporta requisições HTTP de intervalo (range requests). Sirva o mesmo arquivo a partir de um endpoint que o transmite inteiro, ou de uma configuração de CDN que ignora Range, e você comprou um caminho de escrita mais lento e um arquivo maior sem ganho visível para o usuário. Verifique o servidor antes de verificar o código
Por que a linearização sobrepõe UseXRefStream e UseObjectStreams?
Porque o escritor linearizado precisa que cada objeto tenha seu próprio offset de byte diretamente endereçável, e ambos esses recursos tiram isso. O HotPDF portanto emite tabelas de referência cruzada em texto tradicionais e objetos indiretos desempacotados sempre que LinearizeOutput está habilitado, mesmo que o chamador também tenha definido UseXRefStream ou UseObjectStreams. Essa é uma sobreposição deliberada, não um conflito que você precisa resolver sozinho
O raciocínio decorre das tabelas de hint. Uma tabela de hint descreve onde uma seção de página começa e quanto ela dura, para que um leitor possa requisitar exatamente esse intervalo. Um objeto empacotado em um contêiner /ObjStm não tem nenhum offset independente; ele existe apenas como uma fatia dentro de outro stream comprimido que precisa ser buscado e inflado como uma unidade. Se você contava com object streams para o tamanho do arquivo, entenda que linearização e compressão estão puxando em direções opostas aqui, e leia a troca no texto complementar sobre object streams e atualizações incrementais no HotPDF. A mesma tensão molda os arquivos de referência híbrida, que existem precisamente para manter leitores mais antigos funcionando ao lado de tabelas baseadas em stream, como coberto no artigo sobre cross-reference streams híbridos em PDFs gerados pelo Office
Também há um piso de versão. A linearização exige PDF 1.2 ou posterior. Se a versão selecionada é mais antiga, o HotPDF a eleva automaticamente, a menos que StrictVersionLock esteja definido, caso em que a escrita dispara uma exceção em vez de silenciosamente promover um documento que você fixou de propósito
O muro de 4 GiB, e por que o HotPDF se recusa em vez de truncar
As tabelas de hint de linearização armazenam offsets como valores de 32 bits, então um arquivo linearizado não consegue endereçar nada em ou além de 4 GiB, e o HotPDF rejeita tal saída com uma exceção explícita em vez de escrever um arquivo com offsets que deram a volta. O limite não é uma escolha de implementação do HotPDF; é a largura dos campos que o Anexo F define
A verificação é aplicada em três lugares, e todos os três importam. O HotPDF valida cada objeto assim que seu comprimento serializado é conhecido, valida cada comprimento de seção de página ao construir as entradas de hint, e valida o comprimento final do arquivo depois que a tabela de referência cruzada principal é dimensionada. Falhar cedo é o ponto inteiro: uma tabela de hint com um offset silenciosamente truncado produz um arquivo que abre corretamente em um visualizador baixando-o inteiro e falha apenas para o cliente de intervalo de bytes que a linearização existia para servir, que é o pior modo de falha possível porque seu visualizador de teste nunca o reproduz. Se você está produzindo saída de múltiplos gigabytes, a linearização não é a ferramenta, e a abordagem de streaming descrita nas notas sobre a Direct File API para fluxos de trabalho com PDFs grandes é a direção a seguir
Detectando linearização em um arquivo carregado
THotPDF.IsLoadedLinearized reporta se o documento atualmente carregado já foi escrito em forma linearizada, e responde a partir de um instantâneo capturado antes da análise, não do stream ativo. O HotPDF lê os primeiros 1024 bytes a partir da posição zero do stream de origem, os varre em busca da primeira palavra-chave obj e depois de uma entrada /Linearized com o valor 1, e armazena o resultado booleano em cache
var
PDF: THotPDF;
PageCount: Integer;
begin
PDF := THotPDF.Create(nil);
try
PageCount := PDF.LoadFromFile('incoming.pdf');
if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
Writeln('Source is not Fast Web View ready');
finally
PDF.Free;
end;
end;
Duas restrições nessa descrição são estruturais. A detecção não pode depender da posição do stream, porque quando o código de aplicação faz a pergunta o analisador já a moveu, e não pode reler sob demanda porque LoadFromFile libera o stream de origem interno assim que o carregamento termina. Daí o design de captura-antes-de-analisar-e-cachear. A varredura também é deliberadamente literal quanto ao valor: apenas /Linearized 1 ou uma forma numericamente equivalente com fração totalmente zero é aceita, porque um arquivo cujo dicionário de parâmetros diz outra coisa não está fazendo a promessa do Anexo F
Uma armadilha de record do Delphi que vale a pena roubar
Records locais contendo arrays dinâmicos inicializam seus campos gerenciados e nada mais, e se você mantiver um campo Count simples ao lado do array precisa limpá-lo você mesmo. Isso mordeu o particionamento de linearização durante o desenvolvimento, e é o tipo de bug que custa um dia precisamente porque uma plataforma o esconde
type
THPDFLinearIndexList = record
Values: THPDFIntegerArray; // managed field: cleared for you
Count: Integer; // plain field: whatever was on the stack
end;
// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);
O campo de array dinâmico tem contagem de referência, então o compilador o zera. O Count ao lado dele é um inteiro comum sem essa garantia, e um Count não inicializado envia o primeiríssimo append para um índice arbitrário. No Win32 o slot da pilha por acaso continha zero, o append caiu no índice 0, e todo teste passou. No Win64 o mesmo código escreveu além do fim do array. A lição generaliza bem além da linearização: quando um record mistura campos gerenciados e não gerenciados, atribua Default(TRecord) e pare de raciocinar sobre quais campos o compilador cobre, e nunca trate uma execução verde no Win32 como evidência de que a inicialização está correta
Os membros LinearizeOutput e IsLoadedLinearized descritos aqui são fornecidos com o HotPDF Component padrão para Delphi e C++Builder; a página do produto traz a referência completa de propriedades, incluindo as regras de interação com cross-reference streams, object streams e travamento de versão