O HotPDF escreve ficheiros PDF linearizados, o layout que o Acrobat rotula como Fast Web View, através da propriedade LinearizeOutput em THotPDF. Defini-la antes de BeginDoc faz com que o HotPDF reordene o grafo de objetos terminado para que um leitor com conhecimento de intervalos de bytes consiga apresentar a página um depois de obter apenas a parte inicial do ficheiro, em vez de descarregar o documento inteiro primeiro. O mecanismo é o Anexo F da ISO 32000-1
A razão pela qual isto importa é pouco glamorosa. Um PDF normal coloca a sua tabela de referências cruzadas no final, pelo que um leitor tem de chegar ao último byte antes de saber onde está seja o que for. Entregue a um navegador um relatório digitalizado de 200 páginas e o utilizador fica a olhar para um indicador de carregamento durante a transferência completa, mesmo quando a única coisa que queria era a página 1. A linearização corrige isso ao pagar um custo no momento da escrita. Este artigo trata desse percurso de escrita especificamente, o particionamento, o ciclo de medição e os limites rígidos; para o contexto conceptual sobre o que o Fast Web View traz, o artigo anterior explicação sobre linearização de PDF e Fast Web View cobre esse terreno
O que garante realmente o layout linearizado
Um ficheiro linearizado é um PDF comum com uma ordenação física extremamente específica, e cada garantia que 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ências cruzadas antecipada, os objetos ao nível do documento, o stream de sugestão primário, a primeira página e os seus objetos privados, depois as páginas restantes, depois os objetos partilhados, depois tudo o resto, e finalmente a tabela de referências cruzadas principal
O particionamento é derivado, não declarado. O HotPDF percorre o grafo de referências a partir de cada objeto de página e regista, 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 torna-se privado dessa página. Um objeto alcançado por mais do que uma torna-se partilhado. O catálogo, mais o que ele referencia sob /ViewerPreferences, /OpenAction, /Threads e /AcroForm, mais o dicionário de encriptação quando a proteção está ativa, formam o grupo ao nível do documento que tem de preceder tudo. Os nós da árvore de páginas são retidos deliberadamente para não poluírem a secção da primeira página
O dicionário de parâmetros transporta os números que um leitor precisa antes de ter lido seja o que for: /L para o comprimento total do ficheiro, /H para o deslocamento e comprimento do stream de sugestão, /O para o número de objeto da primeira página, /E para o byte onde termina a secção da primeira página, /N para o número de páginas e /T para o deslocamento da entrada da tabela de referências cruzadas principal. Cada um destes é um deslocamento de byte num ficheiro que ainda não existe no momento em que precisa de os escrever
Por que têm de convergir os deslocamentos da tabela de sugestão?
Porque os números no dicionário de parâmetros descrevem o ficheiro que os contém, e alterar qualquer um deles altera o ficheiro. Essa é a dificuldade central de um escritor linearizado, e é por isso que o HotPDF mede repetidamente em vez de escrever uma só vez. Alargue /T de 6 dígitos para 7 e o dicionário de parâmetros cresce um byte; o cabeçalho cresce; cada objeto desloca-se; a tabela de referências cruzadas principal move-se; /T precisa agora de um valor diferente. O layout tem de alcançar um ponto fixo antes de um único byte de output real ser comprometido
O HotPDF trata disto com uma iteração limitada. Primeiro serializa cada objeto num stream de contagem que regista o comprimento sem manter os bytes, para que cada objeto tenha um tamanho serializado conhecido. Depois executa uma passagem de layout que atribui deslocamentos ao grupo ao nível do documento, ao stream de sugestão, ao grupo da primeira página, aos grupos das páginas posteriores, ao grupo partilhado e ao restante, e reporta onde a tabela de referências cruzadas principal ficaria. Esse resultado é reintroduzido como a entrada da passagem seguinte. O ciclo tem um limite de oito tentativas, e a não convergência lança uma exceção em vez de produzir um ficheiro com deslocamentos plausíveis mas errados
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 ciclo de patinar. O dicionário de parâmetros é escrito numa ranhura fixa de 384 bytes, preenchida com espaços, pelo que o seu próprio crescimento nunca pode desestabilizar o layout; se o texto do dicionário alguma vez excedesse essa reserva, o HotPDF lança uma exceção em vez de deslocar tudo silenciosamente. E após a convergência, o HotPDF executa mais uma passagem de layout de confirmação e volta a verificar o comprimento do stream de sugestão, porque o próprio stream de sugestão codifica deslocamentos que só eram conhecidos assim que o layout se fixou. O retorno de toda esta medição é que o HotPDF nunca armazena em buffer uma segunda cópia do documento: uma vez fixados os deslocamentos, os objetos são serializados diretamente para o stream de destino, com uma asserção em cada fronteira de secção verificando que os bytes escritos correspondem ao deslocamento que foi prometido
Ativar a partir do Delphi
A superfície da API é um único Boolean, e o seu único requisito é que o defina antes de a geração começar. LinearizeOutput tem por predefinição False, e a passagem de layout corre quando o documento é escrito, pelo que atribuí-la depois de EndDoc não faz 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 pedidos de intervalo HTTP. Sirva o mesmo ficheiro a partir de um endpoint que o transmite por inteiro, ou de uma configuração de CDN que ignora Range, e comprou apenas um percurso de escrita mais lento e um ficheiro maior sem qualquer ganho visível para o utilizador. Verifique o servidor antes de verificar o código
Por que substitui a linearização UseXRefStream e UseObjectStreams?
Porque o escritor linearizado precisa que cada objeto tenha o seu próprio deslocamento de byte diretamente endereçável, e ambas essas funcionalidades retiram isso. O HotPDF emite por isso tabelas de referências cruzadas de texto tradicionais e objetos indiretos não empacotados sempre que LinearizeOutput está ativado, mesmo que quem chama também tenha definido UseXRefStream ou UseObjectStreams. Esta é uma substituição deliberada, não um conflito que tenha de resolver por si mesmo
O raciocínio decorre das tabelas de sugestão. Uma tabela de sugestão descreve onde começa uma secção de página e quanto dura, para que um leitor possa pedir exatamente esse intervalo. Um objeto empacotado num contentor /ObjStm não tem qualquer deslocamento independente; existe apenas como uma fatia dentro de outro stream comprimido que tem de ser obtido e inflado como uma unidade. Se estava a contar com object streams para o tamanho do ficheiro, entenda que a linearização e a compressão puxam em direções opostas aqui, e leia essa contrapartida no artigo complementar sobre object streams e atualizações incrementais no HotPDF. A mesma tensão molda os ficheiros de referência híbrida, que existem precisamente para manter leitores mais antigos a funcionar ao lado de tabelas baseadas em streams, como coberto no artigo sobre cross-reference streams híbridos em PDFs gerados pelo Office
Há também um piso de versão. A linearização exige PDF 1.2 ou posterior. Se a versão selecionada for mais antiga, o HotPDF eleva-a automaticamente, a menos que StrictVersionLock esteja definido, caso em que a escrita lança uma exceção em vez de promover silenciosamente um documento que fixou de propósito
O muro dos 4 GiB, e por que se recusa o HotPDF em vez de truncar
As tabelas de sugestão de linearização armazenam deslocamentos como valores de 32 bits, pelo que um ficheiro linearizado não consegue endereçar nada em ou além de 4 GiB, e o HotPDF rejeita esse output com uma exceção explícita em vez de escrever um ficheiro com deslocamentos 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 locais, e todos os três importam. O HotPDF valida cada objeto assim que o seu comprimento serializado é conhecido, valida o comprimento de cada secção de página ao construir as entradas de sugestão, e valida o comprimento final do ficheiro depois de a tabela de referências cruzadas principal ser dimensionada. Falhar cedo é todo o objetivo: uma tabela de sugestão com um deslocamento truncado silenciosamente produz um ficheiro que abre corretamente num leitor que o descarrega por inteiro e só falha para o cliente de intervalo de bytes que a linearização existia para servir, o que é o pior modo de falha possível porque o seu leitor de teste nunca o reproduz. Se estiver a produzir output de vários 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 de PDF grandes é a direção a seguir
Detetar a linearização num ficheiro que carregou
THotPDF.IsLoadedLinearized reporta se o documento atualmente carregado já foi escrito em forma linearizada, e responde a partir de um instantâneo tirado antes da análise, não a partir do stream ativo. O HotPDF lê os primeiros 1024 bytes a partir da posição zero do stream de origem, procura-os pela primeira palavra-chave obj e depois por 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 estruturantes. A deteção não pode depender da posição do stream, porque no momento em que o código da aplicação faz a pergunta o analisador já a moveu, e não pode reler a pedido porque LoadFromFile liberta o stream de origem interno assim que o carregamento termina. Daí o desenho de capturar-antes-de-analisar-e-guardar-em-cache. A verificação é também deliberadamente literal quanto ao valor: só /Linearized 1 ou uma forma numericamente equivalente com uma fração toda a zero é aceite, porque um ficheiro cujo dicionário de parâmetros diz outra coisa não está a fazer a promessa do Anexo F
Uma armadilha de registos do Delphi que vale a pena roubar
Os registos locais que contêm arrays dinâmicos inicializam os seus campos geridos e mais nada, e se mantiver um campo Count simples ao lado do array tem de o limpar você mesmo. Isto afetou 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 é referenciado por contagem, pelo que o compilador o zera. O Count ao lado é um inteiro comum sem tal garantia, e um Count não inicializado envia o primeiríssimo acréscimo para um índice arbitrário. No Win32, a ranhura da pilha calhou de conter zero, o acréscimo caiu no índice 0, e todos os testes passaram. No Win64, o mesmo código escreveu além do final do array. A lição generaliza-se muito além da linearização: quando um registo mistura campos geridos e não geridos, atribua Default(TRecord) e deixe de raciocinar sobre que campos o compilador cobre, e nunca trate uma execução Win32 sem erros como prova de que a inicialização está correta
Os membros LinearizeOutput e IsLoadedLinearized aqui descritos são disponibilizados com o HotPDF Component standard para Delphi e C++Builder; a página do produto contém a referência completa de propriedades, incluindo as regras de interação com cross-reference streams, object streams e bloqueio de versão