Esta página é sobre o formato do arquivo, não sobre um produto. Se você está construindo qualquer coisa que recebe PDFs que não foi você quem gerou, vale saber exatamente quais estruturas registram o histórico de um documento, como elas são especificadas e como cada uma falha. Tudo abaixo é legível com um editor hexadecimal e paciência.
Dois repositórios de metadados, e por que eles discordam
Um PDF pode se descrever em dois lugares inteiramente separados, e a maioria dos arquivos modernos usa os dois.
O dicionário de informações do documento
A chave /Info do trailer aponta para um dicionário com um
vocabulário fixo e pequeno: /Title, /Author,
/Subject, /Keywords, /Creator,
/Producer, /CreationDate, /ModDate,
/Trapped. A divisão entre os dois últimos campos de ferramenta é
a parte útil e é lida errado com frequência: /Creator nomeia o
aplicativo que criou o documento original — o editor de texto, o
motor de relatórios — enquanto /Producer nomeia o que quer que o
tenha convertido em PDF, ou o que escreveu os bytes do PDF mais recentemente.
Um extrato gerado no servidor normalmente carrega a string de uma biblioteca
em /Producer; o caso interessante é quando
/Producer nomeia uma ferramenta cuja finalidade é editar.
Os valores são strings de texto PDF, o que significa PDFDocEncoding ou
UTF-16BE introduzido por uma marca de ordem de bytes FEFF. Num
arquivo sem fluxos de objetos o dicionário costuma estar ali, aberto,
encontrável com grep. A ISO 32000-2 (PDF 2.0) deprecia o dicionário de
informações em favor do XMP, mantendo as entradas de data — mas depreciação
num formato tão longevo não diz quase nada sobre o que você vai de fato
receber, e o dicionário Info continua praticamente universal no mundo real.
O pacote XMP
A chave /Metadata do catálogo do documento aponta para um fluxo
de /Type /Metadata /Subtype /XML contendo um pacote XMP da Adobe:
RDF/XML, embrulhado em instruções de processamento que começam com
<?xpacket begin= e um id de pacote fixo e — por convenção e
por recomendação da própria especificação — armazenado sem filtro,
para que um software incapaz de interpretar PDF ainda consiga encontrá-lo e
lê-lo. É por isso que o strings encontra o XMP e pouca coisa
além dele.
Um pacote XMP resumido
<rdf:Description rdf:about=""
xmlns:pdf="http://ns.adobe.com/pdf/1.3/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/">
<pdf:Producer>iText 7.2.5</pdf:Producer>
<xmp:CreatorTool>StatementRenderer</xmp:CreatorTool>
<xmp:CreateDate>2026-01-04T10:02:00+01:00</xmp:CreateDate>
<xmp:ModifyDate>2026-01-04T10:02:00+01:00</xmp:ModifyDate>
<xmpMM:DocumentID>uuid:8f2c...</xmpMM:DocumentID>
<xmpMM:InstanceID>uuid:41de...</xmpMM:InstanceID>
</rdf:Description>
Os pares que importam para o cruzamento são
pdf:Producer ↔ /Producer,
xmp:CreatorTool ↔ /Creator,
xmp:CreateDate ↔ /CreationDate e
xmp:ModifyDate ↔ /ModDate.
Duas propriedades do XMP merecem atenção por si sós. O
xmpMM:DocumentID deve permanecer estável ao longo da vida de um
documento, enquanto o xmpMM:InstanceID muda a cada instância
salva — conceitualmente o mesmo desenho do par /ID do trailer,
um nível acima. E o xmpMM:History, quando presente, é uma lista
ordenada de entradas stEvt: registrando ações, agentes de
software e carimbos de data. É opcional, é escrito por relativamente poucas
ferramentas e é trivialmente removível — mas quando um arquivo o carrega, é o
mais perto que o PDF tem de um log de edição, e ler não custa nada.
Por que os dois repositórios divergem
Não existe mecanismo no formato que os mantenha sincronizados. Se eles concordam é puramente uma propriedade das ferramentas que tocaram o arquivo. Um único gerador escrevendo uma vez preenche os dois de forma consistente. Depois disso:
- Alguns editores reescrevem o dicionário Info e deixam o pacote XMP intacto, porque reescrever XML dá mais trabalho que definir um valor de dicionário.
- Outros fazem o contrário, atualizando o XMP como repositório moderno e deixando o Info desatualizado.
- Esteiras de várias etapas — aplicativo de design, destilador, imposição, pós-processador — legitimamente deixam cada repositório refletindo uma etapa diferente.
Ou seja, divergência indica "mais de um escritor", não "um falsificador". A versão forte do achado é mais estreita: os dois repositórios discordam sobre a ferramenta e só um deles nomeia um editor de consumo. Aí a divergência é o único motivo de aquela ferramenta estar visível, o que é uma observação significativamente diferente de duas bibliotecas de servidor discordando sobre uma string de versão.
Qualquer comparação que você construir precisa de tolerâncias, ou vai afogar
você. Normalize as strings de ferramenta e trate uma como concordando com a
outra quando qualquer uma for substring da outra, para que
iText 7.2.5 e iText casem. Dê aos carimbos de data
um minuto de folga, porque os dois repositórios arredondam com precisões
diferentes.
A sintaxe de datas do PDF, e os quatro jeitos de dar errado
As datas do dicionário Info são strings num formato criado pela própria especificação:
D:YYYYMMDDHHmmSSOHH'mm'
D:20260104100200+01'00' forma do PDF 1.7, com apóstrofo final
D:20260104100200+01'00 também aparece; a ISO 32000-2 tirou a aspa final
D:20260104100200Z UTC
D:20260104 legal — tudo depois do ano pode ser truncado
Tudo depois do ano é opcional, então D:2026 é uma data PDF
válida. O indicador de fuso O é +, - ou
Z. As datas do XMP, em contraste, são ISO 8601. Qualquer
cruzamento precisa normalizar os dois em instantes antes de comparar, e
precisa lidar com uma ausência legítima de informação de fuso em qualquer um
dos lados.
Quatro modos de falha merecem teste:
- Modificação antes da criação. Compare dentro de um único repositório, não entre os dois — divergência entre repositórios é uma questão de metadados, não de cronologia. Dentro de um repositório, um gerador funcionando corretamente não consegue produzir isso.
- Carimbos no futuro. Use uma tolerância de um dia, não de um segundo. Deriva de relógio e bugs de fuso horário são reais e chatos.
- Deslocamentos UTC impossíveis. Nenhum fuso real passa de ±14:00. Um deslocamento fora dessa faixa foi digitado, não calculado.
- Strings ilegíveis. Comuns o bastante em produtores legados e de nicho para merecerem peso próprio, mais baixo. Trate "este gerador é desleixado" separadamente de "estes carimbos são inconsistentes".
Uma assimetria a internalizar: a presença de uma data ruim é evidência; a ausência de datas não é. Remover metadados é uma linha de código em qualquer biblioteca de PDF.
O par /ID do trailer
O /ID do trailer é um vetor de exatamente duas strings de bytes,
convencionalmente de 16 bytes cada e convencionalmente exibidas em
hexadecimal. A especificação lhes dá papéis distintos e úteis: o primeiro
elemento é um identificador permanente estabelecido quando o documento é
criado e não deve mudar durante a vida daquele documento; o segundo é
atualizado sempre que o arquivo é escrito de novo. É, em outras palavras, um
marcador de alteração que o formato mantém para você.
trailer
<< /Size 41 /Root 1 0 R /Info 9 0 R
/ID [<8f2c9a1b...> <41de77c0...>] >>
As implementações normalmente derivam o valor de um hash da hora atual, do caminho do arquivo, do tamanho do arquivo e do conteúdo do dicionário Info, e é por isso que os dois elementos costumam ter o comprimento de um MD5. Essa derivação é uma recomendação, não uma exigência, e nada impede uma ferramenta de escrever o que bem entender.
Três observações práticas:
-
O
/IDé obrigatório quando o trailer tem uma entrada/Encrypt, e para o manipulador de segurança padrão nas revisões mais antigas o primeiro elemento participa da derivação da chave de criptografia. Então arquivos cifrados essencialmente sempre têm um. -
Alguns geradores minimalistas omitem o
/IDpor completo. Isso remove a verificação; não indica nada sobre o documento. - Uma ferramenta que reescreve o arquivo inteiro pode definir os dois elementos com o mesmo valor novo, apagando a evidência completamente. Um par idêntico, portanto, não prova nada. O sinal é unidirecional: divergência é informativa, identidade não é.
Quando um arquivo tem várias revisões ele tem vários trailers, e cada um
carrega o seu próprio /ID. Coletar todos mostra o segundo
elemento evoluindo enquanto o primeiro fica fixo — uma pequena e satisfatória
confirmação de que você está lendo a cadeia de revisões corretamente.
Atualizações incrementais e a cadeia /Prev
Esta é a estrutura que torna a forense de PDF possível, e é um recurso
deliberado, não um descuido. Em vez de reescrever um arquivo, um escritor
conforme pode anexar: objetos novos e substitutos, depois uma
nova seção de referências cruzadas cobrindo apenas o que mudou, depois um novo
trailer, depois startxref com o deslocamento dessa nova seção,
depois %%EOF. Os bytes originais ficam intocados. Um documento
salvo três vezes pode conter fisicamente os três estados.
O trailer de cada seção anexada carrega /Prev, o deslocamento em
bytes da seção de referências cruzadas anterior. Essa é a cadeia. Você entra
nela pelo último startxref do arquivo e caminha para trás
até um trailer sem /Prev; essa seção terminal é o documento
original.
%PDF-1.7
… corpo original …
xref
0 41
0000000000 65535 f
0000000015 00000 n
…
trailer
<< /Size 41 /Root 1 0 R /Info 9 0 R /ID [<8f2c…> <8f2c…>] >>
startxref
118201
%%EOF <-- fim da revisão 1
… objetos anexados: 12 0 obj (fluxo de conteúdo substituto) …
xref
0 1
0000000000 65535 f
12 1
0000119004 00000 n
trailer
<< /Size 41 /Root 1 0 R /Info 9 0 R
/Prev 118201 /ID [<8f2c…> <41de…>] >>
startxref
183640
%%EOF <-- fim da revisão 2
Leia essa seção anexada com atenção: ela lista o objeto 12 e nada mais. O objeto 12 já existia na revisão 1. A nova entrada vence, porque a resolução sempre usa a seção mais nova que menciona um número de objeto. Se o objeto 12 for o fluxo de conteúdo de uma página, então o que o documento exibe mudou entre a revisão 1 e a revisão 2 — e a revisão 1 continua no arquivo, então você pode renderizar as duas.
Essa distinção é o jogo inteiro. "O arquivo tem duas revisões" é fraco: assinar um documento é uma atualização incremental, preencher um campo de formulário também, acrescentar uma anotação também, aplicar um comentário também. "Uma revisão posterior substitui um objeto que já existia e que carrega dados de página, de fluxo de conteúdo ou de imagem" é forte, porque significa que o documento visível mudou depois da geração.
Tabelas de referência cruzada clássicas
Uma seção xref clássica é composta de cabeçalhos de subseção
(primeiro número de objeto, quantidade) seguidos de entradas
de largura fixa de 20 bytes: um deslocamento de 10 dígitos, um número de
geração de 5 dígitos, um caractere de tipo n (em uso) ou
f (livre), e um fim de linha de dois caracteres. A largura fixa é
o motivo de as tabelas serem trivialmente legíveis por máquina e de PDFs
editados à mão as quebrarem com tanta frequência — insira um byte em qualquer
ponto anterior do arquivo e todo deslocamento depois dele está errado.
As entradas livres formam uma lista ligada cuja cabeça é o objeto 0 com geração 65535. Números de geração existem para que um número de objeto liberado possa ser reutilizado com segurança, e na prática quase nada os incrementa: uma geração diferente de zero é incomum o suficiente para merecer atenção.
Contando revisões com honestidade
Contar marcadores %%EOF é o método popular e é um piso razoável,
mas não é a contagem de revisões. Ele discorda da realidade nas duas direções:
-
Conta a mais em arquivos linearizados. Um PDF linearizado
("fast web view") legitimamente coloca uma seção de referências cruzadas da
primeira página perto do início do arquivo, com o seu próprio trailer e
%%EOF, cujo/Prevaponta para a seção principal no fim. Um salvamento, dois marcadores. -
Conta a mais em arquivos com lixo no fim — uma sequência de
bytes parecida com
%%EOFdentro de um fluxo, ou conteúdo anexado por uma transferência quebrada. - Conta a menos quando falta um marcador, o que produtores fora da norma conseguem fazer com regularidade.
- Não diz nada sobre o que mudou, que é a única parte com peso.
O método defensável é percorrer a cadeia /Prev e contar seções,
mantendo os deslocamentos de %%EOF como corroboração e como
alternativa quando a cadeia está quebrada. Proteja a caminhada: um laço em
/Prev é uma coisa real de se encontrar num arquivo hostil, então
registre os deslocamentos visitados e limite o comprimento da cadeia.
Fluxos de referência cruzada, fluxos de objetos e arquivos híbridos
O PDF 1.5 introduziu dois recursos que juntos enfraquecem bastante a forense ingênua baseada em texto.
Fluxos de referência cruzada substituem a tabela em texto
puro por um objeto de fluxo binário comprimido. O vetor /W dá as
larguras de campo de cada entrada e o /Index dá as subseções. O
tipo de entrada 0 é livre, o tipo 1 é um objeto normal com deslocamento em
bytes e geração, e o tipo 2 é um objeto que vive dentro de um fluxo de
objetos, identificado pelo número do objeto do fluxo que o contém mais um
índice dentro dele. Chaves de trailer como /Root,
/Info, /ID e /Prev migram para o
dicionário do próprio fluxo — então um arquivo pode ter uma cadeia de revisões
completa e nenhuma palavra-chave trailer literal em lugar algum.
Fluxos de objetos (/Type /ObjStm) empacotam
muitos objetos não-fluxo dentro de um único fluxo comprimido com Flate. É por
isso que o strings num PDF moderno costuma não mostrar quase
nada: o dicionário Info, a árvore de páginas e os dicionários de fonte estão
todos dentro de contêineres comprimidos. Qualquer parser forense de verdade
tem de descomprimi-los.
Arquivos de referência híbrida são a sutileza que vale
conhecer. Para continuarem legíveis por software anterior ao 1.5, um arquivo
pode carregar uma tabela clássica e um fluxo de referência cruzada
companheiro, apontado pelo /XRefStm do trailer clássico. Leitores
antigos veem uma visão do arquivo; leitores novos veem outra. As duas visões
podem legitimamente diferir em cobertura — e podem ser feitas para diferir em
substância, o que é uma técnica documentada para construir um PDF que mostra
conteúdos diferentes a leitores diferentes. Qualquer parser que pretenda ser
minucioso tem de ler as duas e fundi-las, em vez de escolher uma.
Prefixos de subconjunto de fonte
Quando um produtor incorpora apenas os glifos que um documento de fato usa, a convenção — especificada, não folclore — é prefixar o nome da fonte com seis letras ASCII maiúsculas e um sinal de mais:
/BaseFont /ABCDEF+Helvetica
/FontDescriptor 22 0 R
/FontName /ABCDEF+Helvetica
As seis letras são arbitrárias; a especificação pede apenas que sejam
escolhidas de modo que subconjuntos diferentes dificilmente colidam. Para
fontes simples, o /FontName do descritor precisa bater com o
/BaseFont, então você tem dois lugares para ler a mesma etiqueta.
O valor forense vem de uma propriedade dos geradores, não do formato: uma ferramenta escrevendo um documento numa passada reúne todos os glifos de que precisa para uma tipografia em um subconjunto com uma etiqueta. Duas etiquetas diferentes para a mesma fonte base significam que glifos daquela tipografia foram incorporados em duas ocasiões separadas — o rastro deixado quando texto é acrescentado a, ou substituído em, um PDF existente por uma ferramenta diferente.
A sua força real é a durabilidade. Etiquetas de subconjunto vivem nos recursos da página, não nos metadados, então sobrevivem à remoção de metadados, e sobrevivem a uma reescrita do arquivo inteiro que achata a cadeia de revisões. A sua fraqueza é que a montagem de documentos produz o mesmo padrão de forma inocente: junte dois PDFs que usem Helvetica e você fica com dois subconjuntos. Leia junto com o histórico de revisões, nunca sozinho.
O /ByteRange da assinatura
O /Contents de um dicionário de assinatura guarda o blob PKCS#7
como uma string hexadecimal, e o /ByteRange é um vetor de pares
deslocamento/comprimento nomeando os bytes que foram usados no hash. O arranjo
padrão são dois pares que cobrem tudo exceto o buraco onde o próprio
/Contents fica — a assinatura não consegue assinar a si mesma.
/ByteRange [0 8420 42840 15680]
^ ^ ^ ^
| | | +-- comprimento do segundo trecho coberto
| | +-------- início do segundo trecho (logo após /Contents)
| +------------- comprimento do primeiro trecho coberto
+--------------- início do primeiro trecho
coberto até o byte 42840 + 15680 = 58520
tamanho do arquivo = 61204
-------
bytes fora da assinatura = 2684
Se o último byte coberto não é o último byte do arquivo, a diferença foi anexada depois da assinatura e não é protegida por ela. Isso é aritmética com dois números inteiros. Sem certificado, sem repositório de confiança, sem criptografia — e vale independentemente de a matemática da assinatura validar.
Mantenha as duas perguntas separadas, porque os leitores costumam misturá-las:
- A assinatura confere? Exige o certificado, um repositório de confiança, verificação de revogação e uma política sobre o que conta como confiável.
- Existe algo fora do intervalo assinado? Exige o arquivo.
Um leitor pode legitimamente reportar uma assinatura válida enquanto exibe conteúdo que a assinatura nunca cobriu. É isso que "assinado, com alterações posteriores" significa na interface, e é por isso que a cobertura merece ser reportada como fato próprio.
A ressalva honesta: bytes fora do intervalo não são automaticamente
ilegítimos. Fluxos com múltiplas assinaturas os produzem por construção, e o
PDF tem um mecanismo inteiro — permissões DocMDP, expressas por uma referência
de assinatura com /TransformMethod /DocMDP e um nível
/P — para declarar quais alterações posteriores o signatário
permitiu. Reportar uma lacuna de cobertura não é o mesmo que avaliar se a
alteração era permitida.
O que sobrevive a quê
O modelo mental mais útil ao triar um arquivo: edições diferentes destroem evidências diferentes. Nada aqui é garantia — o comportamento varia por ferramenta — mas o formato geral se mantém.
| Evidência | Metadados removidos | Arquivo reescrito por inteiro | Achatado em imagem |
|---|---|---|---|
| Strings Producer / Creator | somem | substituídas | substituídas |
| Divergência Info × XMP | some | normalmente some | some |
| Anomalias de data | somem | zeradas | zeradas |
| Cadeia de revisões (/Prev) | sobrevive | some | some |
| Divergência do /ID do trailer | sobrevive | pode ser apagada | pode ser apagada |
| Subconjuntos de fonte duplicados | sobrevivem | muitas vezes sobrevivem | somem (sem texto) |
| Imagem de página inteira + texto visível | sobrevive | sobrevive | vira o sinal |
| Lacuna de cobertura da assinatura | sobrevive | assinatura destruída | assinatura destruída |
Leia as colunas, não as linhas. Limpar apenas os metadados é a evasão mais barata e deixa todo sinal estrutural intacto. Reescrever o arquivo inteiro é mais eficaz, mas destrói qualquer assinatura e tende a deixar um conjunto de metadados novo, coerente e conspicuamente recente. Achatar em imagem derrota por completo a análise em nível de texto e cria um documento cujo próprio formato — uma imagem de página inteira onde um extrato gerado por máquina deveria ter texto vetorial — é o achado.
A lição geral: nenhum sinal isolado é robusto, e as evasões são mutuamente exclusivas o bastante para que um conjunto de sinais independentes seja bem mais difícil de derrotar do que qualquer um deles. É também por isso que o enquadramento honesto é sinais, não detecção.
Lendo por conta própria
Dá para ir longe só com um terminal. Comece pelo fim do arquivo e pelos marcadores:
tail -c 500 extrato.pdf # trailer, startxref, %%EOF
grep -c '%%EOF' extrato.pdf # um piso para a contagem de revisões
grep -abo 'startxref' extrato.pdf # o ponto de entrada de cada revisão
strings extrato.pdf | grep -A40 'x:xmpmeta' # o pacote XMP, geralmente sem compressão
strings extrato.pdf | grep -o '/BaseFont *[^ /]*' | sort -u
Além disso você precisa de um parser de verdade, e a escolha da ferramenta importa mais do que parece:
-
qpdf é o honesto.
qpdf --qdf --object-streams=disablereescreve um arquivo numa forma normal, sem compressão e legível por humanos, que é a coisa mais útil que você pode fazer com um PDF que quer entender. O--show-xrefdespeja a tabela de referências cruzadas resolvida. Repare no que ele não preserva: a saída é um arquivo novo com uma revisão. Analise o original e use a cópia normalizada para leitura. -
mutool (
mutool show arquivo.pdf trailer,… grep) é excelente para cutucar objetos e dicionários individuais. -
pikepdf (Python, sobre o qpdf) é a opção agradável para
scripts e, ao contrário da maioria das bibliotecas de alto nível, deixa você
chegar ao
/Preve ao trailer bruto. - exiftool lê os dois repositórios de metadados e mostra Info e XMP lado a lado, que é exatamente a comparação que você quer.
A armadilha do carregar-e-salvar. A maioria das bibliotecas
de PDF de alto nível — aquelas que você usaria para criar um PDF —
normaliza na leitura. Carregue um arquivo, escreva-o de volta e você
tipicamente obtém: uma revisão, uma tabela de referências cruzadas
reconstruída, um par /ID novo, fontes re-subconjuntadas e
metadados novos. Todo sinal estrutural é destruído pelo próprio ato de
examinar o arquivo. Se você está construindo qualquer coisa forense,
interprete os bytes; não os faça dar uma volta completa.
Um aviso relacionado sobre manuseio de evidência: nunca analise a cópia que uma etapa de conversão, um gateway de e-mail, um antivírus ou uma camada de armazenamento com processamento no servidor tocou. Gere o hash do original ao recebê-lo e analise o original.
Ou deixe outra coisa fazer a caminhada
A Tamperlens implementa tudo o que está nesta página como um parser de bytes brutos — nenhuma biblioteca de PDF de alto nível no caminho forense, porque essas bibliotecas normalizam exatamente a evidência descrita aqui. Onze famílias de sinais, achados em linguagem clara com evidência legível por máquina, saída determinística. O verificador gratuito roda isso no seu navegador contra o seu próprio arquivo, sem armazenar nada; a API é um POST, com 25 documentos por mês de graça.
A Tamperlens reporta sinais de risco, não veredictos de autenticidade. Sinais podem ter causas benignas; combine-os com a sua própria lógica de decisão.
Leitura relacionada
- Guia das famílias de sinais em PDF As onze famílias, suas causas benignas e a lógica exata de gravidade.
- Como saber se um PDF foi editado A versão mais curta e prática desta página.
- Fraude em extratos bancários Como isto fica quando aplicado a um fluxo real de triagem.
- Guia da API Autenticação, as duas formas de requisição, o JSON do relatório anotado, cotas.