Artigo Técnico

PDF ID Determinístico em Delphi para Builds Reproduzíveis

A losLab PDF Library pode produzir saída de PDF byte a byte idêntica para entrada idêntica assim que você chama SetDeterministicDocumentID(1). Por padrão, o array /ID do trailer é um digest MD5 do relógio de parede, então duas execuções do mesmo gerador diferem em pelo menos esses bytes. O modo determinístico deriva /ID de uma semente estável, o que restaura builds reproduzíveis

O sintoma geralmente aparece na CI antes de qualquer um sair procurando por ele. O template não mudou, o registro de entrada não mudou, as fontes não mudaram, e o PDF gerado ainda calcula um hash diferente a cada execução do pipeline. Caches de build nunca acertam. O armazenamento endereçável por conteúdo acumula um blob novo a cada build noturno. Diffs de regressão em nível de byte acendem em arquivos que ninguém tocou. Rastreie o diff até os bytes reais e é quase sempre a mesma dúzia de dígitos hexadecimais sentada no trailer do arquivo

Para que serve o array de ID do trailer

O /ID do trailer é um marcador de identidade do arquivo, não um checksum do conteúdo. A ISO 32000-1 §14.4 o define como um array de duas strings de bytes: o primeiro elemento é o identificador permanente atribuído quando o documento é criado e deve sobreviver a toda edição posterior, e o segundo elemento é o identificador variável que um escritor atualiza a cada vez que o arquivo é modificado. Juntos eles permitem que um sistema decida se dois arquivos são revisões de um documento ou dois documentos não relacionados. A §7.5.5 torna a entrada efetivamente obrigatória na prática, já que o trailer deve carregar /ID sempre que também carrega /Encrypt

Nada na especificação diz como calcular o valor. A recomendação é um digest de coisas como o horário atual, o caminho do arquivo, o tamanho do arquivo e o dicionário de informações do documento, e o relógio de parede é o ingrediente que torna o resultado único. Essa é exatamente a propriedade que você quer para identidade e exatamente a propriedade que destrói a reprodutibilidade, motivo pelo qual isso precisa ser um interruptor explícito em vez de uma mudança silenciosa de comportamento

Por que o mesmo build produz um PDF diferente a cada vez?

Porque o identificador padrão é derivado do momento da geração. Historicamente a losLab PDF Library construía as strings /ID a partir de um MD5 do timestamp atual, então um documento criado duas vezes com um segundo de diferença carrega dois identificadores permanentes diferentes mesmo quando todo outro byte do arquivo é idêntico. O custo a jusante é real: um sistema de build que indexa artefatos por hash nunca consegue reutilizar uma etapa de PDF, um repositório de objetos com deduplicação mantém uma cópia por build em vez de uma cópia por documento, e um revisor olhando um diff binário precisa provar que a única mudança é ruído antes de confiar no resto do diff. A geração determinística de /ID existe para remover esse ruído, no mesmo espírito do trabalho de estabilidade de layout descrito nas notas sobre object streams e cross reference streams

Alternando para um identificador reproduzível

O modo determinístico é opcional, por documento, e desativado por padrão para que a saída existente permaneça inalterada até que você o solicite. SetDeterministicDocumentID aceita 0 ou 1 e retorna 1 quando o valor foi aceito, 0 para qualquer coisa fora do intervalo; GetDeterministicDocumentID reporta o estado atual. SetDocumentIDSeed fornece uma string de semente explícita que tem prioridade sobre tudo, e passar uma semente vazia reverte para a semente derivada. GetDocumentFileID lê de volta /ID[0] após o salvamento para que você possa registrá-lo ou testá-lo

var
  Lib: TPDFlib;
  FileID: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetDeterministicDocumentID(1);
    Lib.SetDocumentIDSeed('invoice-4471-rev3');
    Lib.SetOrigin(1);
    Lib.DrawText(100, 700, 'Invoice 4471');
    Lib.SaveToFile('invoice.pdf');
    FileID := Lib.GetDocumentFileID;   // identical on every run
  finally
    Lib.Free;
  end;
end;

A atualização acontece no momento de salvar, não quando você ativa o sinalizador, então habilitar o modo determinístico tarde na construção de um documento ainda tem efeito. Isso também significa que uma semente alterada chega ao arquivo no próximo salvamento completo: defina a semente A, salve, defina a semente B, salve, e os dois arquivos carregam identificadores diferentes, enquanto restaurar a semente A restaura o valor original. Uma semente explícita é a escolha certa sempre que seu documento tem uma chave estável natural como um número de fatura, uma revisão de registro ou um identificador de commit git, porque desacopla o identificador de metadados incidentais

De onde vem a semente quando você não fornece uma?

Sem uma semente explícita, a losLab PDF Library deriva uma a partir do estado do documento que deveria ser invariável entre regenerações idênticas: o cabeçalho de versão do PDF, a contagem de páginas, e cada entrada no dicionário de informações do documento. Valores de string e de nome são tomados literalmente, outros tipos de objeto contribuem com sua forma serializada, e tudo isso é hasheado nas strings /ID. A consequência importante é que CreationDate e ModDate fazem parte do dicionário de informações e portanto fazem parte da semente por design. Duas execuções só ganham o mesmo identificador quando genuinamente produzem os mesmos metadados de documento

Lib.SetDeterministicDocumentID(1);
// No SetDocumentIDSeed: the seed is derived from document state,
// so the timestamps in the Info dictionary have to be pinned.
Lib.SetInformation(2, 'Quarterly Report');        // Title
Lib.SetInformation(5, 'reporting-service 4.2');   // Creator
Lib.SetInformation(7, 'D:20260101000000Z');       // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z');       // ModDate
Lib.SaveToFile('report.pdf');

Fixar ModDate com a chave 8 tem uma dupla função, e essa é a parte que costuma pegar as pessoas de surpresa. Um /ID determinístico sozinho não torna o arquivo byte a byte idêntico, porque o caminho de salvamento carimba ModDate com o horário atual a menos que quem chama tenha definido explicitamente. Definir a chave 8 marca o valor como fornecido pelo chamador e suprime esse carimbo. Se você quer um arquivo reproduzível em vez de apenas um identificador reproduzível, trate timestamps de metadados como entradas de build: derive-os do registro de origem ou de uma época fixa, nunca de Now

Por que reescrever o ID quebra um PDF criptografado?

Porque /ID[0] não é apenas metadado em um documento criptografado, é material de chave. O Algoritmo 2 da ISO 32000-1 §7.6.3.3 alimenta o primeiro elemento do identificador de arquivo no cálculo da chave de criptografia para o manipulador de segurança padrão nas revisões 2 a 4, junto com a senha preenchida, o valor /O e os bits de permissão. A chave derivada então produz a string de validação /U que um leitor verifica na abertura, e a chave do arquivo é derivada e armazenada em cache quando você chama Encrypt ou quando um documento criptografado é carregado, ambos acontecendo antes do salvamento. Reescrever o identificador durante o salvamento portanto emitiria um arquivo estruturalmente válido cuja verificação de /U falha ao reabrir: não uma corrupção sutil, mas um documento que ninguém consegue abrir, inclusive você. É por isso que a atualização determinística é restrita a documentos que não carregam estado de criptografia, e por que um documento criptografado mantém qualquer /ID que já tinha, modo determinístico ou não, e a configuração simplesmente não tem efeito nesse caminho. O tratamento de revisão relacionado e a semântica de permissões estão cobertos no roteiro de auditoria de criptografia e permissões de PDF. Observe também que o caminho de restauração de criptografia atualiza apenas /ID[1], o identificador de mudança, exatamente como a §14.4 pretende

Por que salvamentos incrementais mantêm o identificador original

O segundo limite é o modo de anexação. Uma atualização incremental deixa cada byte anterior do arquivo intocado e escreve uma nova revisão depois dele, e a permanência de /ID[0] segundo a §14.4 é o que diz a um consumidor que a nova revisão pertence ao mesmo documento que a antiga. Reescrevê-lo cortaria esse vínculo, contradiria as revisões já presentes no arquivo, e interferiria na semântica de assinatura, já que uma assinatura cobre um intervalo de bytes de uma revisão específica de um documento específico. A losLab PDF Library portanto atualiza o identificador determinístico apenas em salvamentos completos e nunca durante o modo de anexação, o que mantém intacta a garantia descrita no artigo sobre atualizações incrementais de PDF e anexação a stream

Um único ponto de estrangulamento para geração de identificador

Toda geração de /ID na losLab PDF Library agora passa por uma única rotina interna, NewFileIDString, o que é o que torna o interruptor determinístico confiável em vez de um remendo em um caminho de código. A criação de documento em branco, a criação preguiçosa de um array /ID ausente sob demanda, e o caminho de restauração de impressão digital de criptografia todos a chamam, então há exatamente um lugar onde o relógio de parede poderia vazar de volta. Isso também significa que variantes futuras, como um identificador derivado do conteúdo, são uma mudança em uma função em vez de uma auditoria de todo o serializador

function BuildQuote(const Seed: WideString): AnsiString;
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetDeterministicDocumentID(1);
    Lib.SetDocumentIDSeed(Seed);
    Lib.SetInformation(7, 'D:20260101000000Z');
    Lib.SetInformation(8, 'D:20260101000000Z');
    Lib.SetOrigin(1);
    Lib.DrawText(100, 700, 'Quote 8812');
    Result := Lib.SaveToString;
  finally
    Lib.Free;
  end;
end;

// Regression guard: two independent builds, one byte sequence.
if BuildQuote('quote-8812') = BuildQuote('quote-8812') then
  WriteLn('reproducible')
else
  WriteLn('nondeterminism leaked into the output');

Conecte essa comparação à sua suíte de testes antes de confiar em saída reproduzível em qualquer outro lugar, porque ela falha ruidosamente no momento em que algum recurso novo reintroduz um timestamp. A reprodutibilidade é uma propriedade que decai silenciosamente caso contrário, e uma única asserção sobre duas gravações em memória custa quase nada para executar a cada build

A API de identificador determinístico mostrada aqui é fornecida com a losLab PDF Library para Delphi e C++Builder, junto com a referência completa de informações de documento, criptografia e salvamento incremental