Technical Article

Fluxos de Objetos e Atualizações Incrementais em Delphi com HotPDF

O PDF 1.5 introduziu duas estruturas de armazenamento que o formato de ficheiro anterior não conseguia expressar: o fluxo de objetos (object stream) e o fluxo de referência cruzada (cross-reference stream). Um fluxo de objetos é um contentor comprimido com Flate, identificado como /Type /ObjStm, que contém vários pequenos objetos indiretos empacotados lado a lado, em vez de os dispersar pelo corpo do ficheiro. Um fluxo de referência cruzada é a tabela de pesquisa do ficheiro reescrita em formato binário comprimido com campos de largura variável, em substituição da tabela ASCII de largura fixa que encerrava todos os PDFs até à versão 1.4. Estas estruturas caminham juntas. Assim que os objetos são integrados num fluxo, a antiga tabela de texto deixa de os conseguir endereçar, pelo que o xref binário tem de a acompanhar.

Se compararmos isto com o esquema clássico, o custo que elimina é fácil de constatar. Num ficheiro PDF 1.4, cada objeto indireto reside descompromido atrás do seu próprio cabeçalho obj, e a tabela no final consome exatamente 20 bytes de ASCII por entrada, sendo a compressão proibida. Um documento com 200.000 objetos transporta cerca de 4 MB de dados de referência cruzada antes de um único glifo ser desenhado, com todos os corpos de dicionário não comprimidos empilhados por cima. O PDF 1.5 ataca ambos os números em simultâneo: os dicionários são integrados em contentores Flate e a tabela de 4 MB encolhe para algumas centenas de kilobytes de dados binários. A norma ISO 32000-1 define estas duas estruturas nas secções §7.5.7 e §7.5.8.

Onde reside realmente a poupança

Os fluxos de objetos afetam apenas objetos que não são fluxos (non-stream objects), pelo que comprimem a estrutura e não os píxeis. O conteúdo da página já era comprimido com Flate antes da versão 1.5, e os dados das imagens possuem os seus próprios codecs, razão pela qual um folheto rico em imagens quase não sofre alteração de tamanho. Os ficheiros que encolhem significativamente são os que possuem uma estrutura densa: AcroForms com milhares de dicionários de campos, árvores de marcadores profundas, elementos de estrutura de PDF estruturado (tagged PDF). Estes objetos são minúsculos, numerosos e quase idênticos entre si, sendo essa repetição exatamente o que o Flate aproveita assim que passam a residir num único buffer, em vez de estarem espalhados pelo corpo do documento com cabeçalhos pelo meio.

É fácil subestimar o peso do overhead num ficheiro antigo. Um arquivo de formulários que acumulou anos de edições pode gastar bem mais de metade dos seus bytes em cabeçalhos de dicionário, preenchimento de xref e revisões que nenhum leitor irá consultar. As duas funcionalidades aqui descritas recuperam os dois primeiros elementos. O terceiro, as revisões acumuladas, só cede perante a compactação, quando o ficheiro já não necessita de guardar o histórico do seu percurso.

No HotPDF, ambas as propriedades são ativadas através de um par de propriedades, e a forma como dependem uma da outra importa mais do que a ordem em que as define no código:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.UseXRefStream := True;      // binary xref, prerequisite for ObjStm
    Pdf.UseObjectStreams := True;   // pack objects into /Type /ObjStm
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
    Pdf.EndDoc;                     // emits XRefStm + ObjStm containers
  finally
    Pdf.Free;
  end;
end;

UseObjectStreams necessita que o UseXRefStream esteja definido como True. Um objeto comprimido é acedido através de uma entrada xref de tipo 2, que regista um número de fluxo de objetos e um índice, e uma linha de texto clássica de 20 bytes não dispõe de espaço para guardar esse par. Assim, o UseObjectStreams por si só nada faz de visível; a configuração correta exige a definição de ambas as flags antes de BeginDoc. Se as definir após o BeginDoc, o HotPDF já se terá comprometido com o layout antigo.

Por que razão ambas estão desativadas por defeito

O HotPDF deixa ambas as propriedades como False por defeito, e o motivo reside na integração com sistemas de leitura legados. Um visualizador que apenas compreenda o PDF 1.4 não avisa que é incapaz de gerir objetos comprimidos. Ao encontrar um fluxo xref, não deteta as palavras-chave de trailer que aguarda e reporta uma tabela de referência cruzada danificada ou simplesmente recusa-se a abrir o ficheiro. Se o seu output se destina a um gateway de fax antigo, a uma impressora física com um interpretador integrado ou a um analisador escrito com base na especificação 1.4 há uma década, mantenha ambas as flags desativadas para esse canal e aceite o tamanho maior do ficheiro. Para armazenamento de arquivo e distribuição web, onde qualquer visualizador moderno lê o PDF 1.5 há vinte anos, ativá-las representa uma compressão obtida praticamente sem custos.

Há um efeito secundário que vale a pena partilhar com a sua equipa de suporte. Assim que os dicionários são agrupados em fluxos de objetos, a comparação byte a byte de dois ficheiros gerados deixa de fazer sentido, uma vez que a alteração de um único campo pode voltar a comprimir com Flate todo o contentor e reorganizar tudo o que se segue. Compare estes ficheiros pelo conteúdo dos seus objetos e não através de uma comparação binária.

Atualizações incrementais e os offsets de bytes que protegem

Uma assinatura digital cobre um /ByteRange explícito: dois intervalos do ficheiro físico, indicados como offsets de bytes absolutos, sobre os quais o resumo (digest) CMS foi calculado. Se reescrever o ficheiro, mesmo para algo que pareça idêntico no ecrã, todos esses offsets mudam. O resumo deixa de coincidir e a assinatura passa a ser considerada inválida. Esse é o problema exato que a norma ISO 32000-1 §7.5.6 resolve com as atualizações incrementais. Os objetos novos e modificados são anexados após o %%EOF existente e, em seguida, é gravada uma nova secção de referência cruzada cuja entrada /Prev aponta de volta para a anterior. Os bytes originais nunca são alterados, pelo que a revisão assinada permanece verificável e o Acrobat consegue apresentar cada revisão assinada individualmente no painel de assinaturas.

O HotPDF expõe esta funcionalidade através do seu próprio ponto de entrada:

Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf');  // appends the delta only

Dois aspetos costumam causar dificuldades. O BeginIncrementalUpdate tem de receber o nome do ficheiro original, uma vez que a secção xref anexada regista offsets que apenas fazem sentido em relação a esses exatos bytes originais; se apontar para uma cópia renomeada ou gravada noutro local, os offsets descreverão um ficheiro que já não existe. E a gravação é, por construção, do tipo apenas anexo, pelo que o output será sempre maior do que o input. Este crescimento não é desperdício que deva ser eliminado. É a propriedade que garante que as revisões assinadas anteriores permanecem intactas.

Modificar um ficheiro carregado passa por LoadFromFile

Os programadores que conheceram o HotPDF através da sua API de geração tendem a deparar-se com uma barreira específica. O BeginDoc cria um documento totalmente novo, o que é a ferramenta incorreta quando o objetivo é alterar um documento já existente. A edição de um ficheiro existente faz-se, em vez disso, através das chamadas de documento carregado:

PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5);  // pages 1-3 after page 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');

Se misturar as duas abordagens, o sintoma será um ficheiro de saída contendo apenas o novo conteúdo e nada do original, uma vez que o BeginDoc criou alegremente um documento novo ao lado daquele que pensava estar a editar. Encare LoadFromFile com SaveLoadedDocument como um vocabulário próprio e BeginDoc com EndDoc como outro. Uma rotina que recorra a ambos para o mesmo ficheiro está quase sempre errada.

Quando compactar um ficheiro anexado

A gravação de apenas anexo acarreta um custo a longo prazo. Um processo diário que aplique um carimbo com uma linha de estado no mesmo PDF gera 365 reuniões ao fim de um ano, e cada revisão arrasta uma nova secção xref consigo. Quando esse histórico perde utilidade e não há assinaturas no ficheiro que precisem de sobreviver, pode simplificar todo o documento resserializando-o através do caminho de documento carregado:

Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');

Esta nova gravação constitui uma reescrita completa. Elimina intencionalmente as revisões anteriores e invalida qualquer assinatura presente no ficheiro, pelo que deve sujeitá-la aos mesmos controlos que aplica a qualquer outro passo destrutivo. Uma regra prática de produção recomendada: compacte quando a contagem de revisões ultrapassar um determinado limite ou quando o overhead anexado exceder uma certa percentagem do ficheiro base, e nunca compacte um documento cujo painel de assinaturas contenha elementos ativos.

Verificar o resultado antes de o distribuir

A validação deste par de funcionalidades é muito concreta. Abra o resultado no Adobe Acrobat e confirme três aspetos: as propriedades do documento indicam a versão PDF 1.5 ou posterior quando os fluxos de objetos estão ativos; o painel de assinaturas continua a validar cada revisão assinada anteriormente após uma atualização incremental; e a contagem de páginas e os marcadores sobreviveram intactos ao ciclo de carregar, modificar e gravar. No caso de ficheiros de arquivo, submeta também o ficheiro ao veraPDF, uma vez que um xref comprimido é exatamente o tipo de estrutura que um validador rigoroso examina mais detalhadamente do que um visualizador tolerante. Se o seu trabalho envolver inputs muito grandes, os métodos de inspeção no nosso guia da API Direct File para fluxos de trabalho com PDFs grandes combinam-se naturalmente com a gravação incremental, e o mecanismo de assinaturas por trás dos intervalos de bytes acima mencionados é tratado detalhadamente no artigo sobre assinaturas digitais e PAdES com o HotPDF.

Ambas as funcionalidades são disponibilizadas como parte do HotPDF Component para Delphi e C++Builder, juntamente com as APIs de geração, formulários, encriptação e assinaturas abordadas noutros locais deste blog. A página do produto fornece o link para a referência completa da API para alinhar as chamadas acima apresentadas com o seu próprio pipeline de documentos.