Artigo Técnico

Atualizações Incrementais de PDF no Delphi: Guia AppendToStream

As atualizações incrementais de PDF permitem que uma aplicação Delphi modifique um documento anexando apenas os objetos alterados, deixando todos os bytes originais intocados. A losLab PDF Library implementa esta funcionalidade através do AppendToStream, que escreve apenas a secção incremental definida pela norma ISO 32000-1 §7.5.6, pelo que uma edição de um único marcador num ficheiro de 2 GB custa kilobytes de saída em vez de uma reescrita completa. O mesmo mecanismo é a razão pela qual os documentos assinados podem ser atualizados sem invalidar as suas assinaturas

A dificuldade que isto resolve é concreta. Uma gravação completa reescreve todo o ficheiro: cada objeto é novamente serializado, cada desvio de referência cruzada é recalculado e a saída não tem qualquer relação ao nível dos bytes com a entrada. Para uma fatura de 40 KB, isso não tem problema. Para um arquivo digitalizado de 2 GB onde apenas corrigiu um erro de digitação no título do documento, reescrever dois gigabytes para alterar vinte bytes é absurdo — e se o ficheiro contivesse uma assinatura digital, a reescrita acabaria de a destruir

Por que razão a gravação de um PDF corrompe a sua assinatura digital?

Uma assinatura digital de PDF não assina o conteúdo lógico do documento; assina intervalos de bytes do ficheiro físico. A entrada /ByteRange no dicionário de assinatura regista exatamente quais as partes do ficheiro que o resumo criptográfico cobre. Qualquer operação de gravação que volte a serializar esses bytes — mesmo uma que produza um documento semanticamente idêntico — altera o resumo, e todos os validadores indicarão que a assinatura está corrompida. Isto é assim por conceção: a assinatura atesta os bytes que o signatário viu, e não um modelo abstrato de documento

As atualizações incrementais são a válvula de escape que a especificação PDF fornece. Como uma gravação incremental anexa novos dados após o %%EOF original e nunca toca nos intervalos de bytes assinados, a assinatura existente continua a ser validada em relação aos bytes que cobre. Os validadores classificam então as alterações anexadas separadamente — uma segunda assinatura, um preenchimento de formulário, uma anotação — e decidem se são modificações permitidas. Todos os fluxos de trabalho com múltiplas assinaturas dependem disto: cada signatário adiciona uma secção incremental sobre a anterior. Se está a construir fluxos de assinatura, o artigo complementar sobre assinatura e validação PAdES no Delphi descreve detalhadamente como os intervalos de bytes de assinatura e as secções incrementais interagem

Como funcionam as atualizações incrementais sob a norma ISO 32000-1 §7.5.6

A norma ISO 32000-1 §7.5.6 define o modelo em três regras. Primeiro, o conteúdo original do ficheiro permanece totalmente intacto — nenhum byte se move. Segundo, os objetos alterados e recém-criados são anexados após o último %%EOF, cada um com o mesmo número de objeto que tinha anteriormente (los objetos alterados simplesmente obtêm uma definição mais recente que sobrepõe a antiga). Terceiro, uma nova secção de referência cruzada e um trailer são anexados; a entrada /Prev do trailer aponta de volta para o desvio de bytes da secção de referência cruzada anterior, formando uma cadeia que um leitor percorre da mais recente para a mais antiga para resolver cada objeto para a sua definição mais recente

Duas propriedades úteis resultam desta estrutura. As atualizações são baratas proporcionalmente ao que mudou, e não ao tamanho do documento — o custo de anexação é o tamanho dos objetos modificados mais uma pequena sobrecarga de xref/trailer. E o ficheiro torna-se o seu próprio histórico de versões: cada revisão anterior ainda está fisicamente presente, pelo que um auditor pode truncar o ficheiro em qualquer %%EOF anterior e recuperar exatamente o documento que existia nesse ponto. Para fluxos de trabalho de conformidade que devem provar como era um documento antes de cada alteração, esta pista de auditoria integrada é frequentemente o argumento decisivo para as gravações incrementais

Escrever uma atualização incremental com o AppendToStream

A losLab PDF Library expõe a saída incremental através de AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, que devolve 1 em caso de sucesso e 0 em caso de falha. O parâmetro AppendMode seleciona o que vai para o fluxo de destino. O Modo 0 escreve um ficheiro completo: os bytes originais de origem são copiados primeiro para o fluxo e, em seguida, a secção incremental é anexada. O Modo 1 escreve apenas a própria secção incremental — o delta — e ignora completamente os bytes de origem. O Modo 2 escreve primeiro um prefixo fornecido pelo chamador registado através de SetAppendInputFromString e, em seguida, anexa a secção de atualização sobre o mesmo

var
  Doc: TPDFlib;
  Delta: TMemoryStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('contract.pdf', '') <= 0 then
      Exit;

    // Small edit: the kind of change that should not
    // trigger a rewrite of the whole file
    Doc.SetInformation(3, 'Amended 2026-07-04');  // key 3 = /Subject

    Delta := TMemoryStream.Create;
    try
      // AppendMode = 1: write only the incremental section.
      // Original bytes + Delta = a complete, valid PDF.
      if Doc.AppendToStream(1, Delta) = 1 then
        Delta.SaveToFile('contract.delta.bin');
    finally
      Delta.Free;
    end;
  finally
    Doc.Free;
  end;
end;

O Modo 1 é o mais interessante para a arquitetura de sistemas. Como o delta é autónomo, pode distribuí-lo independentemente do original: armazene as revisões como blobs separados no armazenamento de objetos, replique apenas os deltas para um local remoto ou reconstrua qualquer revisão concatenando o ficheiro base com a sua cadeia de incrementos. A regra de reconstrução é uma simples concatenação de bytes — primeiro o ficheiro original e depois cada delta por ordem — porque este é exatamente o esquema que a secção §7.5.6 prescreve para um ficheiro atualizado de forma incremental

Como é que a biblioteca calcula os desvios de xref sem copiar o ficheiro original?

As entradas de referência cruzada dentro de uma secção incremental devem conter desvios de bytes absolutos — posições medidas a partir do início do ficheiro completo, e não do início do delta. Isso cria um enigma para o modo 1: o escritor nunca emite os bytes originais, mas cada desvio que ele regista tem de fingir que eles estão lá. A losLab PDF Library resolve este problema com um adaptador de fluxo interno, TPDFAppendSectionStream, que apresenta um espaço de coordenadas virtuais ao serializador. O adaptador é criado com o comprimento em bytes do ficheiro original como o seu desvio base, reporta a sua posição e tamanho como essa base mais o que tiver sido anexado até ao momento, e reencaminha apenas os bytes recém-escritos para o fluxo de destino do chamador

A consequência é que o modo 1 nunca materializa uma cópia do documento de origem — nem no disco, nem na memória. A implementação ingénua (escrever o ficheiro completo num buffer temporário e depois cortar a cauda) carregaria uma cópia transitória de todo o PDF original, o que, para entradas à escala de gigabytes, é precisamente o custo que as atualizações incrementais pretendem evitar. Esta técnica de virtualização de desvios é muito próxima do deslocamento de referências de bytes utilizado noutras partes da biblioteca; o artigo sobre fusão rápida de PDF com deslocamento de referências de bytes mostra a mesma ideia aplicada à combinação de documentos, e o guia sobre fusão e divisão de PDFs grandes com acesso direto a ficheiros cobre a arquitetura de E/S envolvente para ficheiros que não cabem confortavelmente na RAM

Streaming de gravações completas com o SaveToStream

O SaveToStream na losLab PDF Library direciona o serializador do documento diretamente contra o fluxo de destino, em vez de renderizar primeiro o documento inteiro numa AnsiString intermédia e depois escrever esse buffer numa única chamada. A abordagem mais antiga funcionava, mas significava que cada gravação completa continha temporariamente uma segunda cópia completa da saída em memória — inofensivo com 10 MB, penoso com 500 MB e uma barreira intransponível para saídas de vários gigabytes em processos de 32 bits. A serialização direta faz com que o pico de memória acompanhe as estruturas de objetos do documento em vez do seu comprimento serializado

var
  Doc: TPDFlib;
  Output: TFileStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('archive.pdf', '') <= 0 then
      Exit;

    // ... edits that justify a full rewrite ...

    Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
    try
      if Doc.SaveToStream(Output) = 0 then
        Writeln('Save failed, error ', Doc.LastErrorCode);
    finally
      Output.Free;
    end;
  finally
    Doc.Free;
  end;
end;

Uma lição de modo de partilha: quando o AppendToFile devolveu 0

Vale a pena partilhar uma regressão nesta área porque o padrão de falha aplica-se de forma geral. O AppendToFile(FileName) anexa uma atualização incremental diretamente a um PDF existente no disco — a chamada natural para um fluxo de trabalho de pista de auditoria no local: carregar um ficheiro, fazer uma alteração, anexar ao mesmo caminho. Na v3.71.2, essa sequência exata começou a devolver 0. A causa principal estava no carregador (loader), e não no escritor: para suportar a leitura a pedido de documentos grandes, o LoadFromFile mantém o identificador (handle) do ficheiro de origem aberto durante o tempo de vida do objeto do documento, e esse identificador era aberto com fmShareDenyWrite. Quando o AppendToFile tentava reabrir o mesmo ficheiro para escrita, o próprio modo de partilha do carregador negava-o e a API falhava antes de escrever um único byte

A correção relaxou o modo de partilha do carregador para fmShareDenyNone, o que é seguro precisamente devido ao que é um anexo incremental: adiciona bytes estritamente após o final do ficheiro e nunca reescreve a região que o identificador de longa duração do leitor está a servir. A lição geral para quem encapsula esta biblioteca — ou constrói carregadores de streaming semelhantes — é que leitores lentos que mantêm identificadores e escritores do mesmo ficheiro estão em conflito, e o modo de partilha que escolhe no momento de abertura é um contrato de API, não um detalhe de implementação. Se o AppendToFile devolver 0 no seu código, verifique primeiro se outro elemento no seu processo ainda mantém o ficheiro de destino com um modo de partilha restritivo

Os custos reais: quando as atualizações incrementais são a ferramenta errada

As atualizações incrementais trocam o tamanho do ficheiro pela eficiência de escrita, e a troca nem sempre é favorável. Cada revisão anexa os seus objetos alterados enquanto as definições substituídas permanecem no ficheiro, pelo que um documento editado centenas de vezes acumula objetos mortos e uma longa cadeia /Prev que cada leitor deve percorrer. Pior ainda, o conteúdo "eliminado" não desaparece: o texto removido na revisão cinco ainda está fisicamente presente nos bytes da revisão quatro, recuperável por qualquer pessoa que trunque o ficheiro. Por isso, a ocultação de informação (redaction), desinfeção ou qualquer remoção de conteúdo sensível exige uma reescrita completa — uma gravação incremental de uma remoção de conteúdo é uma fuga de dados com etapas adicionais

Uma gravação completa também é a decisão correta quando o objetivo é a compactação (eliminar incrementos acumulados e objetos não utilizados), ao alterar propriedades de todo o documento, como a encriptação — a reencriptação afeta cada string e fluxo, pelo que não há nada de "incremental" na alteração — ou ao produzir um ficheiro final limpo onde o histórico de edições não deve viajar com o ficheiro. Uma regra razoável: utilize AppendToStream ou AppendToFile enquanto um documento estiver ativo e em alteração, especialmente depois de conter assinaturas; utilize uma reescrita completa com SaveToStream nos limites do ciclo de vida, quando o documento sai do seu sistema ou o seu histórico deve ser unificado

As atualizações incrementais, a saída delta de desvio virtual e a serialização direta para fluxo fazem parte da losLab PDF Library padrão para Delphi, C# e VB.NET; a página do produto lista a interface de API completa de gravação e anexação juntamente com as funcionalidades de assinatura e ficheiros grandes discutidas acima