Os mesmos bytes toda vez: PDFs reproduzíveis
Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3
Visão geral
Seção intitulada “Visão geral”Construa um PDF a partir das mesmas entradas duas vezes e você esperaria o mesmo arquivo. A maioria das bibliotecas de PDF não consegue prometer isso — reconstrua e compare (diff), e os bytes derivam. O NextPDF consegue fixar as duas coisas que se movem, de modo que as mesmas entradas produzam os mesmos bytes, toda vez.
Por que isso importa
Seção intitulada “Por que isso importa”Saída idêntica byte a byte não é uma métrica de vaidade. É a fundação por baixo de três coisas que as equipes de fato querem.
A primeira é o cache. Se uma build é uma função pura de suas entradas, o hash de sua saída é uma chave de cache. Mesmas entradas, mesmo hash, pule o trabalho e sirva o arquivo armazenado. Quando os bytes vagam, o hash vaga, e o cache nunca acerta.
A segunda é a evidência de adulteração. Um pipeline que consegue regenerar o arquivo exato que enviou pode provar, depois, que um documento arquivado não foi alterado: reconstrua-o, faça o hash de ambos, compare. Se até mesmo um byte difere por causa de um relógio embutido, a prova se foi e você volta ao “confie em mim”.
A terceira é uma CI confiável. Um golden-file test registra uma saída tida como boa e falha quando uma mudança a altera. Esse sinal só tem significado se um mecanismo inalterado reproduzir um arquivo inalterado. Se cada execução difere em um timestamp, o golden file é ruído, e a equipe aprende a ignorar uma build vermelha — o hábito mais caro nos testes.
A versão resumida
Seção intitulada “A versão resumida”No perfil determinístico do NextPDF, os dois campos controlados pelo mecanismo que
de outra forma derivariam entre builds idênticas são as datas e o /ID. Isto
pressupõe que o resto do pipeline já é estável — as mesmas entradas, e uma
serialização que não varia por conta própria (mais sobre isso abaixo):
- Datas embutidas. O document information dictionary carrega
CreationDateeModDate(Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3), e os metadados XMP os espelham. Capture o “agora” no momento da build e toda reconstrução difere. - O identificador de arquivo. O array
/IDé um par de byte strings que identificam o arquivo (Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4), armazenado no trailer dictionary (Spec: ISO 32000-2, §7.5.5ISO 32000-2 §7.5.5). As bibliotecas geralmente o derivam do horário atual mais bytes aleatórios, de modo que ele é diferente a cada execução, por design.
Fixe ambos — um timestamp fixo e uma seed fixa para o /ID — e a saída se torna
uma função determinística de seu conteúdo. Deixe o conteúdo em paz e o arquivo é
idêntico byte a byte. Esta é a mesma disciplina que o projeto
Reproducible Builds estabeleceu para software compilado, aplicada à camada do
documento.
Como o NextPDF aborda isso
Seção intitulada “Como o NextPDF aborda isso”O determinismo no NextPDF é um objeto de configuração, não um truque de teste. O
mecanismo expõe um value object DeterministicSettings no namespace NextPDF\Core.
Ele é final readonly, imutável, e fixa exatamente as duas fontes de deriva
derivadas de relógio e de aleatoriedade nomeadas acima: as datas e o /ID. Fixá-las
remove as duas fontes de deriva mais comuns, mas não garante por si só uma saída
idêntica byte a byte. O restante do comportamento de serialização do mecanismo —
ordenação de objetos, subsetting de fontes e configurações de compressão — também
precisa ser determinístico para que a saída se reproduza, e o NextPDF mantém esses
estáveis por design.
Seu construtor recebe dois argumentos:
public function __construct( public DateTimeImmutable $timestamp, public string $fileIdSeed,) { // ...}$timestamp é o único instante fixo escrito em cada campo de data —
CreationDate, ModDate e seu espelho XMP. Passe um
DateTimeImmutable e o documento para de perguntar ao relógio de parede que horas
são. $fileIdSeed é a entrada que fixa o /ID do trailer: uma string hexadecimal
de 32 caracteres. Dê a mesma seed e o mecanismo deriva o mesmo identificador de
arquivo em vez de amostrar o relógio e uma fonte aleatória.
O objeto valida a própria entrada. A seed deve ter exatamente 32 caracteres
hexadecimais; qualquer outra coisa é rejeitada na construção com uma
InvalidConfigException em vez de silenciosamente produzir um /ID de aparência
diferente. Esta é a mesma postura de recusar-se a adivinhar que o resto do mecanismo
adota — uma entrada ambígua falha em alto e bom som em vez de mudar os bytes em
silêncio.
Com ambos fixados, a receita é a que o projeto Reproducible Builds tornou familiar: reconstrua, faça o diff, e o diff está vazio.
- Fixe as entradasO mesmo conteúdo, fontes e configurações que produziram o documento original.
- Fixe o timestampUm DateTimeImmutable alimenta CreationDate, ModDate e as datas XMP — sem relógio de parede.
- Fixe a seed do /IDUma seed hex de 32 caracteres deriva o /ID do trailer em vez de um valor de relógio-mais-aleatório.
- BuildA saída agora é uma função pura do conteúdo; as duas partes móveis ficam paradas.
- Reconstrua e faça o diffRegenere a partir das mesmas entradas e compare os bytes — um diff vazio é a prova.
Exemplo prático
Seção intitulada “Exemplo prático”Uma forma pequena e completa. As configurações são construídas uma vez e reutilizadas, de modo que duas execuções do mesmo programa emitem o mesmo arquivo.
<?php
declare(strict_types=1);
use NextPDF\Core\DeterministicSettings;use NextPDF\Exception\InvalidConfigException;
// One fixed instant for every date field — never the wall clock.$timestamp = new DateTimeImmutable('2026-01-01T00:00:00+00:00');
// A 32-character hex seed pins the trailer /ID. Same seed, same /ID.$fileIdSeed = '0123456789abcdef0123456789abcdef';
try { $deterministic = new DeterministicSettings( timestamp: $timestamp, fileIdSeed: $fileIdSeed, );} catch (InvalidConfigException $e) { // A malformed seed (not exactly 32 hex chars) is refused here, // before any document is built — not silently coerced. error_log($e->getMessage());
throw $e;}
// Hand $deterministic to the document configuration. With both moving// parts pinned, building the same content twice yields identical bytes://// sha256(build_one) === sha256(build_two)A seed é uma entrada de build que você controla, não um segredo. Guarde-a ao lado do resto da sua configuração de build. O ponto é que ela é fixa, de modo que o identificador de arquivo que ela produz também é fixo.
Equívoco comum
Seção intitulada “Equívoco comum”A primeira armadilha é “Removi o timestamp, então minha build agora é
reproduzível.” Em geral não é, porque o array /ID é a mais silenciosa das duas
fontes. As datas são visíveis em um painel de metadados e fáceis de lembrar; o /ID
do trailer é invisível para a maioria dos leitores e é regenerado a partir do
relógio e de uma fonte aleatória a cada execução. Uma build que fixa apenas as datas
ainda produz um arquivo diferente a cada vez. Você tem de manter ambos parados.
A segunda armadilha é tratar o determinismo como um recurso de segurança por si só.
Um /ID fixado torna um arquivo reproduzível; ele não o torna assinado, e ele
não prova por si só que duas builds são iguais. Uma comparação byte a byte ou um
hash prova que as builds são iguais; fixar o /ID apenas remove uma fonte de
diferença espúria. E nenhum desses prova que um terceiro responde pelo arquivo.
Reprodutibilidade e assinatura são camadas complementares, não substitutas.
Limites e fronteiras
Seção intitulada “Limites e fronteiras”O determinismo fixa as partes móveis do próprio mecanismo. Ele não fixa as suas
entradas. Se o seu conteúdo embute um timestamp ao vivo, puxa uma fonte que mudou em
disco, ou renderiza um valor que depende da data atual, a saída muda porque a
entrada mudou — e isso está correto. O DeterministicSettings remove o não
determinismo do mecanismo, não o seu. Uma build reproduzível ainda exige entradas
reproduzíveis.
| Edition | Availability |
|---|---|
| Core | Suporte completo. |
| Pro | Not in this edition |
| Enterprise | Not in this edition |
Documentos relacionados
Seção intitulada “Documentos relacionados”- Golden-file testing — a técnica de CI que depende de uma saída idêntica byte a byte, e por que um mecanismo determinístico é a sua pré-condição.
- Atualizações incrementais — como um PDF cresce
por anexação, onde o array
/IDimporta novamente para relacionar um arquivo às suas versões anteriores. - Metadados e o XMP packet — onde vivem as datas embutidas, e como o XMP packet espelha o document information dictionary.
- A anatomia de um arquivo PDF — o
trailer, a cross-reference table, e onde o array
/IDfica na estrutura do arquivo.
Glossário
Seção intitulada “Glossário”- Idêntico byte a byte — dois arquivos que coincidem exatamente, byte a byte. A forma mais forte de “o mesmo”, e a que um hash ou um diff consegue verificar.
/ID(identificador de arquivo) — o array de duas byte strings que identifica um PDF e suas versões (ISO 32000-2 §14.4), armazenado no trailer dictionary (§7.5.5). Em geral derivado do relógio mais bytes aleatórios, razão pela qual ele muda a cada build não fixada.- Document information dictionary — a estrutura que carrega
CreationDateeModDate(ISO 32000-2 §14.3.3). Uma das duas fontes de não determinismo que uma build determinística precisa fixar. - Golden file — uma saída tida como boa, registrada, contra a qual um teste compara; só tem significado quando um mecanismo inalterado reproduz um arquivo inalterado.
- Build reproduzível — uma build cuja saída é uma função determinística de suas entradas, de modo que reconstruir a partir das mesmas entradas produz os mesmos bytes. O termo vem do projeto Reproducible Builds para software compilado.