Referência: Sinais de fraude em PDF — o guia de campo
Todo corpus que eu tinha era um corpus que eu escolhi. Processos judiciais dos EUA, contratos públicos brasileiros, amostras de extrato bancário — 705 documentos, coletados com cuidado, e uma taxa de falso positivo calculada em cima de todos eles. Aí eu reparei que a taxa não era um número só. Um alvo dava 3,9% e outro dava 36,4%, para o mesmo sinal. Uma diferença de nove vezes entre fontes que você mesmo escolheu não mede o seu motor. Mede o seu gosto para escolher fontes.
Então fui atrás de duas populações que ninguém aqui montou, rodei 1.728 documentos pelo motor e descobri que ele devolvia algo diferente de baixo em 68,5% dos PDFs publicados pelo governo americano e em 82,8% de uma amostra aleatória da web brasileira. Dois em cada cinco dos brasileiros voltaram como alto.
Todos esses documentos foram publicados pela própria organização que os produziu. Não havia nada de errado com nenhum deles.
Duas populações que eu não escolhi
A primeira é o “1000 .gov PDF dataset” da Biblioteca do Congresso
americano, uma amostra aleatória em CC0 de PDFs em domínios .gov, tirada do arquivo
web deles. Não foi selecionada por emissor, nem por tipo de documento, nem por mim.
O metadata.csv publicado junto me deixou conferir exatamente o que me
interessava antes de baixar um único byte: 244 strings
/Producer distintas em 1000 arquivos, 144 delas aparecendo uma única
vez, cobrindo de PDF 1.1 a 1.7 — Acrobat PDFWriter 3.02, Distiller 4.0,
TCPDF, iText, libtiff, Word 2007. Seis fontes escolhidas a dedo não produzem essa
variedade.
A segunda eu tive que construir. Não existe equivalente brasileiro do conjunto da
LoC, então amostrei o índice do Common Crawl — blocos igualmente espaçados sob o
prefixo SURT br,, que por inverter o nome do host cobre
com.br, gov.br e edu.br juntos, em vez de uma
fatia que eu teria que escolher. Deu 900 URLs em 421 hosts brasileiros
distintos, 335 deles com.br.
As URLs vêm do Common Crawl. Os bytes não. O Common Crawl trunca os corpos de resposta que guarda em cerca de 1 MB, e um PDF truncado faria este motor relatar um dano estrutural que o arquivo do publicador não tem — eu teria medido o meu próprio pipeline de corpus e chamado isso de achado. Cada documento foi buscado no servidor de origem que o publicou.
Chegaram 990 e 744 arquivos. 985 e 743 foram analisados, com zero falhas de parsing e zero códigos de aviso. Isso precisa ser dito antes de qualquer taxa abaixo: nada do que vem a seguir é artefato de arquivos que o parser não conseguiu ler.
E a limitação, dita aqui e não numa nota de rodapé: todo documento é
presumido autêntico porque foi publicado pelo próprio emissor, então todo
sinal acima de info conta como falso positivo. Um PDF do governo que
tivesse mesmo sido alterado antes de ser publicado conta contra mim aqui. Essa
presunção é a junta mais fraca do estudo inteiro.
A diferença nunca foi entre as fontes
Minha hipótese era que a diferença de 3,9% a 36,4% entre alvos fosse na verdade um efeito de produtor — que um corpus de extratos bancários não é especial por conter extratos, e sim porque aqueles bancos todos imprimem pelas mesmas duas ou três cadeias de ferramentas. Dentro de uma população, um país, um tipo de documento:
| Família de produtor | Documentos | Acima de baixo | Sinal principal |
|---|---|---|---|
| TCPDF | 135 | 100,0% | active-content (135) |
| Adobe PDF Library | 41 | 95,1% | id-inconsistency |
| iText | 48 | 91,7% | id-inconsistency |
| Microsoft Office | 36 | 91,7% | producer-fingerprint |
| Acrobat Distiller | 347 | 74,4% | id-inconsistency |
| scanner / multifuncional | 22 | 40,9% | id-inconsistency |
| Acrobat PDFWriter | 56 | 39,3% | producer-fingerprint |
| gerador de relatório | 22 | 18,2% | metadata-mismatch |
| conversor de imagem | 18 | 0,0% | — |
De zero a cem por cento em catorze famílias. A diferença de nove
vezes que eu tinha visto entre alvos era uma diferença entre produtores vestida de
alvo. O TCPDF é o caso limpo: ele escreve um /OpenAction em todo
arquivo que produz, então um sinal disparava em 100% do que ele gera e não carregava
informação nenhuma.
Dois sinais estavam discutindo com o parser ao lado deles
O motor informa quantas revisões um PDF tem. É um fato estrutural, lido do arquivo: uma revisão significa que o documento foi escrito uma vez e nunca recebeu um acréscimo. Dois dos meus sinais estavam dizendo ao leitor que um documento tinha sido alterado depois de criado, em arquivos cuja contagem de revisões dizia que isso nunca aconteceu.
| Sinal | Severidade | Taxa EUA | em arquivo de 1 revisão | Taxa BR | em arquivo de 1 revisão |
|---|---|---|---|---|---|
id-inconsistency | média | 44,7% | 83,4% | 36,7% | 88,6% |
font-anomalies | alta | 6,0% | 76,3% | 27,1% | 92,5% |
incremental-updates | alta + média | 8,1% | 0,0% | 5,1% | 0,0% |
Essa terceira linha é o controle, e é por causa dela que isso é um bug e não um
limiar que eu calibrei mal. O incremental-updates é derivado
da contagem de revisões, então nunca discorda dela. Os outros dois estavam
inferindo uma segunda gravação a partir de um artefato que produtores comuns geram
já na primeira.
O id-inconsistency lê o vetor /ID do trailer. A
especificação diz que o primeiro elemento é fixado na criação e o segundo é
reescrito a cada salvamento, então os dois diferirem é o registro da própria
especificação de que houve um novo salvamento. É isso que produtores em conformidade
fazem. O que os produtores fazem de verdade, em 44,7% dos PDFs
publicados pelo governo americano, é gerar os dois elementos do zero já na primeira
vez.
O font-anomalies era pior, porque disparava em alto — a banda
que o meu próprio contrato de pontuação reserva para um sinal que sozinho
estabelece uma alteração. A premissa dele: um gerador reúne todos os glifos de que
precisa para uma tipografia num único subconjunto, então dois subconjuntos da mesma
tipografia significam que glifos foram incorporados em duas ocasiões separadas. Um
documento do corpus, com o achado que ele produziu:
produtor: Acrobat Distiller 8.1.0 (Windows)
revisões: 1
severidade: alta
achado: Vários subconjuntos da mesma tipografia incorporados
Calibri (FLJEPG+, FLJFAG+); Cambria-Bold (FLJEIC+, FLJEJC+);
Cambria-Italic (FLJECP+, FLJEDA+)
“…a assinatura deixada quando um texto é acrescentado ou
substituído num PDF existente com outra ferramenta”
O Distiller emite vários subconjuntos de uma mesma tipografia numa única passagem por padrão. Juntar documentos também produz isso — e juntar documentos é o que o iLovePDF, o “combinar” do Word e o botão de anexar de um scanner fazem com documentos absolutamente honestos.
Um sinal não errou o limiar. Errou a premissa.
O producer-fingerprint era o maior sinal da população brasileira —
54,5% de 743 documentos, 82 deles em alto. A premissa
dele, escrita no código: extratos, faturas e certificados são emitidos por
bibliotecas do lado do servidor, então uma ferramenta de consumo na cadeia é um
sinal.
Eis o que ele nomeava de verdade:
| “Ferramenta de edição de consumo na cadeia de produção (…)” | Documentos |
|---|---|
| Microsoft Word | 206 |
| iLovePDF | 52 |
| Microsoft: Print To PDF | 34 |
| Adobe Illustrator | 34 |
| Quartz PDFContext (macOS) | 23 |
| Canva | 15 |
| Google Docs Renderer | 14 |
Vinte e seis documentos do Microsoft Word foram pontuados como alto. O que sai do Canva voltou acima de baixo em 100% das vezes.
A premissa só se sustenta se você já sabe que o documento diz ser gerado por máquina. O meu motor não sabe o que está olhando. Ele recebe um arquivo. Numa população aberta a premissa se inverte por completo: uma ferramenta de consumo na cadeia é o que um documento comum tem, e um sinal que dispara no Word é um censo, não um achado.
A distinção que sobrevive ao contato com uma população real não é consumo versus servidor. É a origem — essa ferramenta criou o documento ou consumiu um que já existia? Word, Canva, Google Docs, Illustrator, um driver de impressão, um OCR sobre um escaneamento: a existência do arquivo começa neles, então nomeá-los não diz nada sobre o que mudou depois. iLovePDF, Sejda, PDF-XChange, o editor interativo do Acrobat: esses recebem um PDF existente como entrada e escrevem um novo. Isso é uma afirmação sobre a história do arquivo, e não sobre a autoria dele — e vale alguma coisa, embora ainda não seja prova, porque juntar, comprimir e preencher formulário são coisas que gente honesta faz com PDFs honestos todo dia.
Havia um segundo erro embaixo desse. O sinal subia para alto quando o documento também trazia imagem rasterizada de página inteira, na teoria de que isso corroborava uma edição. Na amostra brasileira essa regra sozinha foi responsável por 71 dos 82 disparos em alto — porque uma página rasterizada inteira é o que um design do Canva, uma exportação do Illustrator e uma página escaneada parecem. Ela marca um documento que é uma imagem. Não diz nada sobre a imagem ter sido editada.
E um relatava um índice de navegação como conteúdo ativo
O active-content pesava o /OpenAction igual a JavaScript
embutido. Na amostra americana ele disparou 213 vezes: 189 desses eram um
/OpenAction e mais nada, contra 9 de JavaScript de verdade.
Uma chave respondia por 19,2% daquele corpus inteiro, enquanto conteúdo de fato
executável ficava em 1,2% — o sinal real enterrado sob oito vezes o próprio volume
em ruído.
O que torna esse caso limpo de resolver é que o argumento é estrutural, e não
estatístico. O /OpenAction quase sempre carrega um destino —
abrir na página 1, ajustar à janela — que é o que a maioria das ferramentas escreve
por padrão. Quando ele carrega uma ação, esse dicionário traz
/S /JavaScript ou /S /Launch, e a varredura de presença do
parser já percorre subdicionários embutidos e marca essas flags a partir dele. Ou
seja: o caso perigoso não consegue se apresentar como
openAction sozinho, o que significa que o caso sozinho é
comprovadamente navegação.
O que mudou, e o que isso valeu
Nada foi suprimido. As quatro famílias continuam emitindo os achados delas, com a
evidência intacta — os prefixos de subconjunto conflitantes continuam listados, o
par /ID divergente continua exibido, o /OpenAction
continua relatado. Elas emitem em info, que pontua zero e fica fora da
contagem de sinais. Quem revisa um documento juntado continua vendo as fontes. O
documento é que deixa de receber uma banda forçada por uma inferência que a própria
estrutura dele contradiz.
| População | Acima de baixo | Banda alta |
|---|---|---|
| .gov dos EUA (985) | 68,5% → 18,6% | 11,8% → 7,4% |
| Web brasileira (743) | 82,8% → 30,0% | 38,2% → 10,0% |
Esses números são uma projeção, não uma nova medição, e a diferença importa o bastante para ser dita com todas as letras. Os documentos originais foram apagados depois que os achados foram escritos — isso é uma regra permanente deste corpus, não uma conveniência — então a coluna “depois” vem de reexecutar a função de pontuação sobre os relatórios guardados com as severidades novas. A reexecução reproduz a banda atual do motor em 1.728 de 1.728 documentos, que é o que a torna confiável, e ainda assim não é a mesma coisa que rodar os arquivos de novo. Os dois manifestos de URL sobreviveram, então qualquer pessoa pode fazer isso.
O que eu não corrigi, e o que ainda não posso afirmar
O redaction-exposure dispara em 5,1% da população americana e em
17,0% da brasileira — texto ainda legível sob uma tarja desenhada,
texto embaixo de uma imagem colocada por cima. Eu não consigo classificar
esses casos. Tarja mal feita em documento público é um fenômeno real e
documentado repetidas vezes, então uma fração desconhecida deles são acertos
legítimos; e uma imagem colocada sobre um texto também é simplesmente como um
timbre se assenta numa página. Resolver isso exige um humano olhando os documentos,
que a regra de exclusão fez com que eu não tenha mais. É o único número deste estudo
que não pode ser citado como taxa de falso positivo em nenhuma das duas direções.
Atualização, motor 1.19.0 (2026-08-04). Resolvido — e no fim não
precisou de humano lendo documento. Os manifestos mantiveram cada arquivo
rebaixável, então eu os recuperei (1.731 de 1.734 idênticos byte a byte) e
perguntei ao renderizador em vez de ao leitor: renderize a página e olhe os
pixels onde estaria o texto supostamente escondido. Texto sob uma tarja é um
bloco chapado; texto que não está escondido tem o contraste de glifos. O meu
próprio redaction-exposed.pdf dá 0,0 nas duas tarjas, e entre 1.693
trechos cobertos nada caiu entre 0,7 e 12,2 — o limiar é uma lacuna nos dados, não
um número que eu escolhi.
Em 1.728 documentos publicados esta família não encontrou nenhuma tarja mal feita, e cerca de nove em cada dez disparos eram errados ou vazios. Duas causas: eu ignorava caminhos de recorte, então a borda de uma célula de tabela — desenhada recortando um anel e preenchendo a célula inteira — virava uma caixa opaca sobre o texto daquela célula; e um trecho coberto contendo apenas espaços era reportado como "texto ainda legível por baixo". Os dois estão corrigidos. 5,1% → 1,9% na população americana e 17,0% → 6,6% na brasileira, com o recall inalterado nas oito operações de falsificação. O que ainda dispara errado são recortes feitos de curvas em vez de retângulos, que eu não modelo.
E a lacuna maior: tudo acima é especificidade. Todo documento foi presumido autêntico, então isso mede com que frequência o motor erra sobre arquivos inocentes e não diz absolutamente nada sobre com que frequência ele acerta sobre os culpados. Não existe número de recall aqui porque não existe corpus público de documentos rotulados como adulterados — eu procurei. Produzir um significa sintetizá-lo: pegar documentos autênticos e aplicar edições conhecidas, com verdade de referência conhecida. Até isso existir, “alta precisão, recall desconhecido” é a posição honesta — e a metade da precisão só virou uma afirmação medida nesta semana.
Por que publicar isso
68% de falso positivo não é um bom número para colocar no blog do próprio produto. Mas a alternativa era seguir citando uma taxa calculada sobre documentos que eu mesmo escolhi — e o valor inteiro de uma ferramenta forense é que os achados dela signifiquem alguma coisa quando são inconvenientes.
A lição generalizável não é sobre PDF. É que um detector medido contra um corpus que o próprio autor montou está medindo o autor. Descobrir isso custou dois conjuntos de dados públicos, uma tarde de download e uma taxa quatro vezes pior do que eu acreditava. Não descobrir teria custado um cliente descobrindo primeiro, nos documentos dele, enquanto pagava por isso.
Rode em um arquivo seu
O verificador gratuito recebe um PDF e devolve o mesmo relatório que este texto descreve, com a evidência bruta embaixo de cada achado. Sem conta, sem guardar nada.
Se quiser ver a recalibração na prática, rode algo que você fez no Word ou exportou do Canva. Uma semana atrás, boa parte desses voltava como alto. Era esse o bug.