pdf-inspector: classifica o PDF antes de pagar por um OCR
A maioria dos pipelines de documentos se apoiam em um pressuposto cômodo: todo PDF vai para OCR. É o padrão seguro — você não sabe o que há dentro do arquivo, então o envia para um serviço que consegue ler qualquer coisa, espera entre dois e dez segundos, e paga por página.
Esse pressuposto é falso para a maioria dos documentos que você realmente processa. Relatórios, faturas, artigos, contratos, extratos bancários: nasceram digitais. O texto já está dentro do arquivo, sentado no content stream como operadores planos. Passá-los por OCR é pagar a um modelo de visão para ler uma foto de algo que você poderia ter lido diretamente.
pdf-inspector é a resposta do Firecrawl para isso: uma biblioteca em Rust que abre o PDF, decide em milissegundos se precisa de OCR, e — se não precisar — extrai o texto e te entrega Markdown limpo localmente. Sem modelo, sem GPU, sem API key. Está no npm desde abril e no crates.io desde junho, essa semana ultrapassou as 8.100 estrelas no GitHub, e é a camada de classificação que roda por baixo do endpoint comercial de parsing do próprio Firecrawl.
Como decide, sem renderizar nada
Essa é a parte que vale a pena entender antes de instalá-la, porque o mecanismo explica tanto a velocidade quanto os limites.
pdf-inspector não rasteriza páginas nem roda um layout model. Faz parse da tabela xref e da árvore de páginas sem carregar o documento completo, percorre os content streams, e busca duas coisas: operadores de texto (Tj, TJ) e operadores de imagem (Do). As páginas com operadores de texto têm texto extraível. As que só têm imagens, não. Essa é toda a classificação, e é a razão pela qual um documento de 300 páginas se resolve em milissegundos em vez de segundos.
O que retorna não é binário mas de quatro caminhos: TextBased, Scanned, ImageBased ou Mixed — mais um score de confiança entre 0 e 1, e uma lista chamada pagesNeedingOcr.
Esse último campo é o que muda sua arquitetura. Classificar a nível de documento é tudo ou nada: um anexo escaneado e o relatório inteiro de 150 páginas vai para OCR. Rotear a nível de página significa que as 148 páginas nativas são extraídas localmente em milissegundos e apenas as duas escaneadas custam algo.
Instalação e primeira execução
O pacote Node traz binários pré-compilados para Linux x64 e ARM64 (glibc e musl, então Alpine funciona), macOS ARM64 e Windows x64. npm baixa apenas o que corresponde à sua plataforma — uns 5–6 MB, com definições de TypeScript incluídas e sem nenhuma toolchain de Rust em nenhuma parte do processo.
npm install @firecrawl/pdf-inspector
# ou
bun add @firecrawl/pdf-inspector
Python e Rust recebem o mesmo core:
pip install pdf-inspector
cargo add pdf-inspector
O site de documentação do projeto também documenta uma invocação direta por npx para testá-la em um arquivo sem instalar nada, se quiser a visualização mais rápida possível antes de se comprometer.
Uma pegadinha menor: os números de versão não estão alinhados entre registries. npm está em 1.12.0, PyPI em 0.2.6 e crates.io em 0.1.7 — mesmo projeto, três streams de versionamento independentes. Fixe por registry e não tente raciocinar entre eles.
O ramo de roteamento
Essa é a forma que importa em um pipeline real:
import { readFileSync } from 'fs';
import { classifyPdf, processPdf } from '@firecrawl/pdf-inspector';
const pdf = readFileSync('document.pdf');
const meta = classifyPdf(pdf);
console.log(meta.pdfType); // "TextBased" | "Scanned" | "Mixed" | "ImageBased"
console.log(meta.pageCount); // 42
console.log(meta.pagesNeedingOcr); // [5, 12, 15] (índice desde 0)
console.log(meta.confidence); // 0.875
if (meta.pdfType === 'TextBased' && meta.confidence > 0.8) {
const { markdown } = processPdf(pdf); // local, sem rede
// pronto
} else {
// envia para seu serviço de OCR apenas meta.pagesNeedingOcr
}
Duas chamadas, e o caminho caro passa de ser o padrão a ser condicional.
Se você está construindo um pipeline híbrido onde um layout model detecta regiões em páginas renderizadas, existe extractTextInRegions(buffer, pageRegions): você passa bounding boxes em pontos PDF e obtém o texto por região, cada uma com um flag needsOcr que se dispara ante texto vazio, fontes com encoding GID ou saída lixo. Quando suspeita especificamente de uma camada de texto corrompida, ocrReason volta como "suspected_garbled_text". Esse flag é a parte honesta do design: a biblioteca avisa quando não confia em sua própria extração, em vez de entregar lixo com segurança.
Ajustar o quão forte olha
A classificação aceita uma ScanStrategy, e escolher a correta é uma decisão real:
| Estratégia | Comportamento | Quando usar |
|---|---|---|
EarlyExit (padrão) |
Para na primeira página sem texto | Você está roteando documentos TextBased para um caminho rápido |
Full |
Escaneia todas as páginas, sem saída antecipada | Você precisa distinguir bem Mixed de Scanned |
Sample(n) |
Amostra n páginas — primeira, última, do meio | PDFs muito grandes onde a velocidade importa mais que a precisão |
Pages(vec) |
Apenas as páginas que você indicar | Você já sabe onde olhar |
O padrão está otimizado para o caso de roteamento, não para classificar com precisão. Se você se importa em distinguir Mixed de Scanned — e se importa, se está roteando a nível de página — use Full.
O lado Markdown
A classificação é metade da ferramenta; a outra metade é o converter, e é mais completo do que a descrição de “sem modelos de ML” sugere. Infere headings H1–H4 a partir de tiers de tamanho de fonte relativos ao texto de corpo, detecta bold e italic pelo nome da fonte, reconhece listas com marcadores, numeradas e com letras, identifica blocos de código por fontes monoespaciadas (Courier, Consolas, Menlo, Fira Code, JetBrains Mono), reconstrói a ordem de leitura em layouts multi-coluna, lida com fontes CID/Type0 via ToUnicode CMaps, suporta RTL, une palavras cortadas por hífen entre linhas, mescla drop caps, filtra números de página e colapsa os pontos suspensivos das tabelas de conteúdo.
A detecção de tabelas roda por duas vias ao mesmo tempo: baseada em retângulos, lendo as operações de desenho reais do PDF, mais uma passada heurística sobre o alinhamento do texto para tabelas desenhadas sem linhas. Lida com tabelas que continuam entre páginas e separa valores numéricos consolidados em tabelas financeiras.
Desde o crate de Rust você também obtém dois CLIs:
cargo install pdf-inspector
pdf2md document.pdf --compact # saída eficiente em tokens
pdf2md document.pdf --select-pages 1,3,5-10
pdf2md document.pdf --pages # insere marcadores <!-- Page N -->
detect-pdfdocument.pdf --analyze --json
--compact vale a pena conhecer se você vai alimentar isso a um LLM: colapsa os dot leaders e padding similar do documento original, que é desperdício puro de tokens para baixo.
Também há um build WASM para browser (@firecrawl/pdf-inspector-wasm) que roda o mesmo parser de Rust dentro de um Web Worker, então um arquivo que o usuário solta no seu frontend nunca sai da máquina dele.
Ler o benchmark com honestidade
Firecrawl publicou números contra o corpus opendataloader-bench (200 PDFs), atualizados em 31 de julho em um Apple M4 Pro, com OCR desabilitado, comparando apenas engines locais sem ML:
| Engine | Geral | Ordem de leitura | Tabelas (TEDS) | Títulos | Velocidade (200 docs) |
|---|---|---|---|---|---|
| pdf-inspector | 0.875 | 0.915 | 0.814 | 0.788 | 0.470s |
| liteparse | 0.873 | 0.913 | 0.693 | 0.811 | 0.750s |
| opendataloader | 0.831 | 0.902 | 0.489 | 0.739 | 2.569s |
| pymupdf4llm | 0.735 | 0.886 | 0.401 | 0.424 | 17.117s |
| markitdown | 0.589 | 0.844 | 0.273 | 0.000 | 16.165s |
É um benchmark do próprio vendor — foi rodado pelo Firecrawl, sobre seu próprio fork do harness —, então leia como uma afirmação, não como um veredicto. E leia com atenção, porque o destaque não sobrevive à leitura.
Esse 0.875 contra o 0.873 do liteparse é um empate, não uma vitória. Com a ordem de leitura acontece o mesmo. Onde pdf-inspector realmente se separa é em tabelas — 0.814 contra 0.693 é uma diferença real, e é consistente com a estratégia de detecção dual — e em velocidade, onde 0.470s contra 17.117s não é uma diferença mas outra categoria de ferramenta. Em títulos perde para liteparse, algo que o README atual não diz em voz alta, embora uma versão anterior tenha dito.
Um número para ignorar completamente: o post de lançamento do Firecrawl afirma “0.002s por página”. Seu próprio benchmark marca 0.470 segundos para 200 documentos, ou seja uns 2 ms por documento. Por página o número é outro. Use a tabela, não o tweet.
E uma nota de justiça sobre a última linha: MarkItDown, que cobrimos aqui em abril, é um generalista que converte Word, PowerPoint, Excel, HTML e áudio além de PDFs. Medi-lo sobre um corpus exclusivamente de PDFs contra um parser dedicado não é uma briga parelha, e esse 0.000 em títulos diz mais do teste do que da ferramenta. Trabalhos distintos.
Onde não encaixa
Não faz OCR. Isso não é uma carência, é a premissa: pdf-inspector te diz quando você precisa de OCR e sai do caminho. Se seu corpus é principalmente escaneado, isso te economiza um passo de classificação e nada mais.
Os títulos são seu eixo mais fraco, e a razão é estrutural: infere hierarquia desde o tamanho da fonte, então os documentos que marcam títulos com negrito no mesmo tamanho do corpo vão se achatar. Se a estrutura de títulos é o que você usa para fazer chunking em RAG, teste isso especificamente antes de se comprometer.
E esse “~54% dos PDFs não precisa de OCR” é uma cifra do Firecrawl, apresentada sem fonte. É credível em sua direção, mas meça contra seu próprio corpus em vez de fazer orçamento com ela — um script de duas chamadas sobre cem dos seus documentos reais te dá seu número real em uma tarde.
Vale uma tarde
A barreira aqui é baixa e o resultado é concreto: você instala um pacote, roda classifyPdf sobre uma amostra dos seus documentos de produção, e descobre que fração do seu gasto em OCR está comprando texto que você já tinha. Se a resposta se aproxima de metade, a ramificação de roteamento se paga sozinha na semana que você a implanta. E diferente de quase tudo que promete economia, isso é MIT, roda local e se verifica com duas chamadas de função.
E você? Qual porcentagem dos PDFs que você processa termina em OCR sem necessidade — você já mediu, ou ainda manda tudo pelo mesmo caminho?