Pular para o conteúdo
getnextpdf.com

Os mesmos bytes toda vez: PDFs reproduzíveis

Spec: ISO 32000-2, §14.4Spec: ISO 32000-2, §14.3.3

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.

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.

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 CreationDate e ModDate (Spec: ISO 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.4), armazenado no trailer dictionary (Spec: ISO 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.

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.

  1. Fixe as entradasO mesmo conteúdo, fontes e configurações que produziram o documento original.
  2. Fixe o timestampUm DateTimeImmutable alimenta CreationDate, ModDate e as datas XMP — sem relógio de parede.
  3. 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.
  4. BuildA saída agora é uma função pura do conteúdo; as duas partes móveis ficam paradas.
  5. Reconstrua e faça o diffRegenere a partir das mesmas entradas e compare os bytes — um diff vazio é a prova.
Reproducible build: identical inputs plus a pinned timestamp and a pinned /ID seed produce the same bytes, which a rebuild-and-diff step confirms.

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.

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.

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.

Deterministic byte-identical output — edition availability
EditionAvailability
Core

Suporte completo. DeterministicSettings vem no core open-source: fixe o timestamp e a seed do /ID e o mesmo conteúdo reconstrói para os mesmos bytes — sem barreira de edição.

ProNot in this edition
EnterpriseNot in this edition
  • 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 /ID importa 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 /ID fica na estrutura do arquivo.
  • 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 CreationDate e ModDate (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.