O PDFium Component torna a saída de PDF reprodutível uma única chamada: o TPdf.SaveAsReproducible reescreve o documento de modo que salvar o mesmo conteúdo duas vezes renda arquivos byte-idênticos. Ele remove /CreationDate e /ModDate do dicionário de informações do documento, não escreve /ID no trailer, mantém a ordem de objetos estável e reporta o que mudou em um TPdfReproducibleReport. Arquivos criptografados são recusados, e arquivos assinados precisam de consentimento explícito
A necessidade costuma aparecer em um pipeline de build. Alguém faz commit de PDFs gerados em um repositório ou em uma suíte de testes de golden-file, e toda execução produz um diff mesmo que nada em nenhuma página tenha mudado. Ou um arquivo morto deduplica por SHA-256 e guarda em silêncio cinquenta cópias da mesma fatura, porque cada salvamento carimbou um horário novo no arquivo. Um escritor de PDF que embute o relógio da parede é ok para um app desktop e hostil para qualquer coisa que trate bytes como identidade
Por que salvar o mesmo PDF duas vezes produz bytes diferentes?
Dois salvamentos de um documento inalterado diferem por exatamente dois tipos de dados: timestamps no dicionário de informações do documento e o identificador de arquivo no trailer. Todo o resto que um escritor cuidadoso emite, numeração de objetos, ordem de objetos, layout de cross-reference, pode ser tornado determinístico, e o caminho de reescrita completa do PDFium Component já faz isso: objetos são escritos em número de objeto crescente e a tabela xref segue a mesma ordem
Os timestamps são o culpado óbvio. A ISO 32000-1 §14.3.3 define /CreationDate e /ModDate no dicionário /Info, e o salvamento nativo do PDFium escreve um /ModDate novo a cada vez que serializa o documento. O identificador de arquivo é o sutil. A ISO 32000-1 §14.4 descreve o /ID como um array de dois elementos de byte strings, em que o primeiro elemento identifica o arquivo original e o segundo muda conforme o arquivo é revisado. Escritores tipicamente derivam essas strings fazendo o hash de algo sobre o arquivo, e se esse algo inclui os bytes com carimbo de tempo, o identificador herda o não determinismo
Usando TPdf.SaveAsReproducible em Delphi
O TPdf.SaveAsReproducible tem dois overloads, um escrevendo em um TStream e um em um nome de arquivo, e ambos retornam Boolean com um out Report: TPdfReproducibleReport e um parâmetro opcional AllowSignedDocument que tem padrão False. O valor de retorno espelha o Report.Succeeded; quando é False, o Report.ErrorMessage diz por quê. O overload de arquivo escreve em um arquivo temporário ao lado do alvo e substitui o alvo só depois de um salvamento bem-sucedido, então uma execução que falha não deixa um PDF pela metade onde estava a sua saída de build anterior
var
Pdf: TPdf;
Report: TPdfReproducibleReport;
begin
Pdf:= TPdf.Create(nil);
try
Pdf.FileName:= 'invoice-2026-10.pdf';
Pdf.Active:= True;
if Pdf.SaveAsReproducible('build\invoice-2026-10.pdf', Report) then
Writeln(Format('%d -> %d bytes, %d objects kept, %d pruned, ' +
'%d timestamps stripped', [Report.SourceByteCount,
Report.OutputByteCount, Report.KeptObjectCount,
Report.PrunedObjectCount, Report.StrippedTimestampCount]))
else
Writeln('Reproducible save failed: ', Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Os campos do relatório valem ser registrados em CI. O StrippedTimestampCount normalmente é 2 para um documento cujo /Info carrega as duas datas, e 0 para um sem dicionário /Info de forma alguma. O PrunedObjectCount conta objetos que estavam presentes no salvamento nativo mas não eram alcançáveis a partir do catalog ou do dicionário /Info; a reescrita reprodutível os descarta, então espere que a saída tenha o mesmo tamanho ou menos do que um salvamento comum
Por que remover os timestamps não bastou
A primeira implementação removia os dois timestamps e ainda produzia dois arquivos diferentes. As páginas batiam, o dicionário /Info batia, e os trailers diferiam no segundo elemento do /ID. A causa raiz era a ordenação: a reescrita primeiro pede ao PDFium um salvamento nativo, depois pós-processa esses bytes, e o construtor de trailer calculava o /ID fazendo o hash da saída nativa. Essa saída nativa ainda continha o /ModDate novo, então o hash, e portanto o identificador, mudava a cada execução mesmo que o timestamp em si nunca chegasse ao arquivo final
É um bom exemplo de uma regra geral para builds reprodutíveis: remover um valor não determinístico não basta se algo foi derivado dele primeiro. A correção foi parar de carregar o /ID de vez. O TPdf.SaveAsReproducible escreve um trailer contendo apenas /Size, /Root e, quando a origem tem um, /Info. Sem nada dependente de tempo restando no trailer, dois salvamentos consecutivos do mesmo documento comparam iguais byte a byte, que é exatamente como o comportamento é verificado
function SavesAreIdentical(Pdf: TPdf): Boolean;
var
A, B: TMemoryStream;
RA, RB: TPdfReproducibleReport;
begin
A:= TMemoryStream.Create;
B:= TMemoryStream.Create;
try
Result:= Pdf.SaveAsReproducible(A, RA) and
Pdf.SaveAsReproducible(B, RB) and
(A.Size = B.Size) and CompareMem(A.Memory, B.Memory, A.Size);
finally
B.Free;
A.Free;
end;
end;
Removendo chaves de dicionário sem corromper strings
Os timestamps são removidos por uma varredura consciente de tokens do corpo do objeto /Info, não por uma busca de texto. Uma substituição de bytes ingênua por /ModDate também acertaria uma string /Subject ou /Keywords que por acaso contenha esse texto, e cortar bytes de uma string literal corrompe os metadados em silêncio em vez de falhar barulhentamente. O PDFium Component primeiro neutraliza strings literais e comentários em uma cópia de varredura do corpo, depois só aceita uma correspondência onde um token de nome real começa, usa a gramática de objetos do PDF para achar onde o valor dessa chave termina, e pula o par chave-valor inteiro
A consequência prática é que toda outra entrada no /Info, título, autor, produtor, chaves customizadas, sobrevive intocada. Só as duas chaves definidas como datas são removidas, e só do dicionário para o qual a entrada /Info do trailer realmente aponta
O que omitir o /ID custa para PDFs criptografados e assinados?
Omitir o /ID é seguro para saída PDF 1.x simples, mas exclui criptografia e muda o que acontece com assinaturas. Na ISO 32000-1, o /ID do trailer é opcional a menos que uma entrada /Encrypt esteja presente, porque o security handler padrão mistura a primeira string de identificador na chave de criptografia. Um arquivo sem /ID não pode ser um arquivo corretamente criptografado, então o TPdf.SaveAsReproducible não tenta: quando o trailer carrega /Encrypt, ele retorna False e o Report.ErrorMessage diz que o documento está criptografado. Descriptografe primeiro se o seu pipeline permite, ou mantenha entregáveis criptografados fora das verificações de identidade de bytes. Se você mira o PDF 2.0, note que a ISO 32000-2 aperta a regra e exige /ID em todo arquivo, então saída reprodutível encaixa mais naturalmente com alvos PDF 1.x; o artigo sobre conformidade exata de versão de PDF em Delphi cobre como o componente trata alvos de versão
Documentos assinados são um problema diferente. O TPdf.SaveAsReproducible é uma reescrita completa, não incremental, então os offsets de bytes se movem e o /ByteRange das assinaturas existentes não cobre mais os bytes que foram assinados. Saída reprodutível de um arquivo assinado é, portanto, uma decisão deliberada de abrir mão da validade das assinaturas existentes em troca de bytes estáveis, que é o motivo de o método levantar EPdfError quando o SignatureCount é maior que zero, a menos que você passe AllowSignedDocument = True
if Pdf.SignatureCount > 0 then
// Opt-in explícito: a reescrita move offsets de bytes, então
// assinaturas devem esperar falha de verificação depois
Ok:= Pdf.SaveAsReproducible('normalized.pdf', Report, True)
else
Ok:= Pdf.SaveAsReproducible('normalized.pdf', Report);
Se o original assinado é o registro da verdade, mantenha-o como está e use saída reprodutível só para cópias derivadas, como uma rendição normalizada que você faz diff em CI. Para anexar a um arquivo protegido sem perturbar o que já está lá, veja por que um PDF criptografado não pode ser remendado em silêncio em Delphi
Limites para conhecer antes de confiar na identidade de bytes
A identidade de bytes vale para o mesmo documento salvo pelo mesmo build do componente, e cobre exatamente o que o método toca. O TPdf.SaveAsReproducible remove só as duas datas do /Info; um stream de metadados XMP com o seu próprio xmp:CreateDate ou xmp:ModifyDate é levado adiante como os bytes que o documento já guarda. Isso é estável entre salvamentos, mas significa que as datas do XMP e do /Info deixam de descrever a mesma coisa, o que importa para perfis PDF/A que exigem que os dois concordem; o artigo sobre equivalência de metadados PDF/A Info e XMP explica essas regras. Um binário PDFium diferente ou uma versão de componente diferente também podem serializar objetos de forma diferente, então fixe ambos em qualquer pipeline que compare hashes entre máquinas
Dentro desses limites, o método te dá o que diffs de CI, testes de golden-file e arquivos mortos endereçados por conteúdo precisam: mesma entrada, mesmos bytes. O TPdf.SaveAsReproducible e o TPdfReproducibleReport vêm com o PDFium Component para Delphi e C++Builder, junto com o resto da sua API de salvamento, otimização e conformidade
FAQ
O TPdf.SaveAsReproducible muda o conteúdo das páginas?
Não. Ele remove as chaves /CreationDate e /ModDate do dicionário /Info, omite o /ID do trailer e descarta objetos não alcançáveis a partir do catalog ou do /Info; o conteúdo das páginas é escrito inalterado
Consigo produzir saída reprodutível de um PDF criptografado?
Não. A criptografia depende do /ID do trailer, que a saída reprodutível omite, então o método retorna False com um ErrorMessage explicativo para origens criptografadas
Por que um PDF assinado precisa de AllowSignedDocument?
O salvamento reprodutível é uma reescrita completa que move offsets de bytes, então assinaturas existentes param de casar com os seus intervalos de bytes assinados. Passar AllowSignedDocument = True confirma que você aceita essa troca