Pdf-inspector: clasifica el PDF antes de pagarle a un OCR

pdf-inspector: clasifica el PDF antes de pagarle a un OCR

La mayoría de los pipelines de documentos se apoyan en un supuesto cómodo: todo PDF va a OCR. Es el default seguro — no sabes qué hay adentro del archivo, así que lo mandas a un servicio que puede leer cualquier cosa, esperas entre dos y diez segundos, y pagas por página.

Ese supuesto es falso para la mayoría de los documentos que realmente procesas. Reportes, facturas, papers, contratos, estados de cuenta: nacieron digitales. El texto ya está dentro del archivo, sentado en el content stream como operadores planos. Pasarlos por OCR es pagarle a un modelo de visión para que lea una foto de algo que podías haber leído directamente.

pdf-inspector es la respuesta de Firecrawl a eso: una librería en Rust que abre el PDF, decide en milisegundos si necesita OCR, y —si no lo necesita— extrae el texto y te entrega Markdown limpio de forma local. Sin modelo, sin GPU, sin API key. Está en npm desde abril y en crates.io desde junio, esta semana cruzó las 8.100 estrellas en GitHub, y es la capa de clasificación que corre por debajo del endpoint comercial de parsing del propio Firecrawl.

Cómo decide, sin renderizar nada

Esta es la parte que conviene entender antes de instalarlo, porque el mecanismo explica tanto la velocidad como los límites.

pdf-inspector no rasteriza páginas ni corre un layout model. Parsea la xref table y el page tree sin cargar el documento completo, recorre los content streams, y busca dos cosas: operadores de texto (Tj, TJ) y operadores de imagen (Do). Las páginas con operadores de texto tienen texto extraíble. Las que solo tienen imágenes, no. Esa es toda la clasificación, y es la razón por la que un documento de 300 páginas se resuelve en milisegundos en vez de segundos.

Lo que devuelve no es binario sino de cuatro vías: TextBased, Scanned, ImageBased o Mixed — más un score de confianza entre 0 y 1, y una lista llamada pagesNeedingOcr.

Ese último campo es el que te cambia la arquitectura. Clasificar a nivel documento es todo o nada: un anexo escaneado y el reporte entero de 150 páginas se va a OCR. Rutear a nivel página significa que las 148 páginas nativas se extraen localmente en milisegundos y solo las dos escaneadas te cuestan algo.

Instalación y primera corrida

El paquete de Node trae prebuilt binaries para Linux x64 y ARM64 (glibc y musl, así que Alpine funciona), macOS ARM64 y Windows x64. npm baja solo el que corresponde a tu plataforma — unos 5–6 MB, con definiciones de TypeScript incluidas y sin toolchain de Rust en ninguna parte del proceso.

npm install @firecrawl/pdf-inspector
# o
bun add @firecrawl/pdf-inspector

Python y Rust reciben el mismo core:

pip install pdf-inspector
cargo add pdf-inspector

El sitio de documentación del proyecto también documenta una invocación directa por npx para probarlo sobre un archivo sin instalar nada, si quieres el vistazo más rápido posible antes de comprometerte.

Una trampa menor: los números de versión no están alineados entre registries. npm va en 1.12.0, PyPI en 0.2.6 y crates.io en 0.1.7 — mismo proyecto, tres streams de versionado independientes. Pinea por registry y no intentes razonar entre ellos.

La rama de routing

Esta es la forma que importa en un 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, sin red
  // listo
} else {
  // manda a tu servicio de OCR solo meta.pagesNeedingOcr
}

Dos llamadas, y el camino caro pasa de ser el default a ser condicional.

Si estás construyendo un pipeline híbrido donde un layout model detecta regiones sobre páginas renderizadas, existe extractTextInRegions(buffer, pageRegions): le pasas bounding boxes en puntos PDF y te devuelve el texto por región, cada una con un flag needsOcr que se dispara ante texto vacío, fuentes con encoding GID o salida basura. Cuando sospecha específicamente de una capa de texto rota, ocrReason vuelve como "suspected_garbled_text". Ese flag es la parte honesta del diseño: la librería te avisa cuando no confía en su propia extracción, en vez de entregarte basura con seguridad.

Ajustar qué tan fuerte mira

La clasificación acepta una ScanStrategy, y elegir la correcta es una decisión real:

Estrategia Comportamiento Cuándo usarla
EarlyExit (default) Se detiene en la primera página sin texto Estás ruteando documentos TextBased a un camino rápido
Full Escanea todas las páginas, sin salida temprana Necesitas distinguir bien Mixed de Scanned
Sample(n) Muestrea n páginas — primera, última, del medio PDFs muy grandes donde la velocidad importa más que la precisión
Pages(vec) Solo las páginas que le indiques Ya sabes dónde mirar

El default está optimizado para el caso de routing, no para clasificar con precisión. Si te importa distinguir Mixed de Scanned —y te importa, si estás ruteando a nivel página— usa Full.

El lado Markdown

La clasificación es la mitad de la herramienta; la otra mitad es el converter, y es más completo de lo que sugiere la descripción de “sin modelos de ML”. Infiere headings H1–H4 a partir de tiers de tamaño de fuente relativos al texto de cuerpo, detecta bold e italic por nombre de fuente, reconoce listas con viñetas, numeradas y con letras, identifica bloques de código por fuentes monoespaciadas (Courier, Consolas, Menlo, Fira Code, JetBrains Mono), reconstruye el reading order en layouts multi-columna, maneja fuentes CID/Type0 vía ToUnicode CMaps, soporta RTL, vuelve a unir palabras cortadas por guión entre líneas, fusiona drop caps, filtra números de página y colapsa los puntos suspensivos de las tablas de contenido.

La detección de tablas corre por dos vías a la vez: basada en rectángulos, leyendo las operaciones de dibujo reales del PDF, más una pasada heurística sobre la alineación del texto para tablas dibujadas sin líneas. Maneja tablas que continúan entre páginas y separa valores numéricos consolidados en tablas financieras.

Desde el crate de Rust también obtienes dos CLIs:

cargo install pdf-inspector

pdf2md document.pdf --compact          # salida eficiente en tokens
pdf2md document.pdf --select-pages 1,3,5-10
pdf2md document.pdf --pages            # inserta marcadores <!-- Page N -->
detect-pdf document.pdf --analyze --json

--compact vale la pena conocerlo si vas a alimentar esto a un LLM: colapsa los dot leaders y padding similar del documento fuente, que es desperdicio puro de tokens aguas abajo.

También hay un build WASM para browser (@firecrawl/pdf-inspector-wasm) que corre el mismo parser de Rust dentro de un Web Worker, así que un archivo que el usuario suelta en tu frontend nunca sale de su máquina.

Leer el benchmark con honestidad

Firecrawl publicó números contra el corpus opendataloader-bench (200 PDFs), actualizados el 31 de julio sobre un Apple M4 Pro, con OCR deshabilitado, comparando solo engines locales sin ML:

Engine Overall Reading order Tablas (TEDS) Headings Velocidad (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

Es un benchmark del propio vendor —lo corrió Firecrawl, sobre su propio fork del harness—, así que léelo como una afirmación, no como un veredicto. Y léelo con atención, porque el titular no sobrevive a la lectura.

Ese 0.875 contra el 0.873 de liteparse es un empate, no una victoria. Con el reading order pasa lo mismo. Donde pdf-inspector se separa de verdad es en tablas —0.814 contra 0.693 es una diferencia real, y es consistente con la estrategia de detección dual— y en velocidad, donde 0.470s contra 17.117s no es una diferencia sino otra categoría de herramienta. En headings pierde contra liteparse, algo que el README actual no dice en voz alta, aunque una versión anterior sí lo hacía.

Un número para ignorar por completo: el post de lanzamiento de Firecrawl afirma “0.002s por página”. Su propio benchmark marca 0.470 segundos para 200 documentos, o sea unos 2 ms por documento. Por página el número es otro. Usa la tabla, no el tweet.

Y una nota de justicia sobre la última fila: MarkItDown, que cubrimos aquí en abril, es un generalista que convierte Word, PowerPoint, Excel, HTML y audio además de PDFs. Medirlo sobre un corpus exclusivamente de PDFs contra un parser dedicado no es una pelea pareja, y ese 0.000 en headings dice más del test que de la herramienta. Trabajos distintos.

Dónde no encaja

No hace OCR. Eso no es una carencia, es la premisa: pdf-inspector te dice cuándo necesitas OCR y se corre del camino. Si tu corpus es mayormente escaneado, esto te ahorra un paso de clasificación y nada más.

Los headings son su eje más débil, y la razón es estructural: infiere jerarquía desde el tamaño de fuente, así que los documentos que marcan títulos con bold al mismo tamaño del cuerpo se van a aplanar. Si la estructura de headings es lo que usas para hacer chunking en RAG, prueba eso específicamente antes de comprometerte.

Y ese “~54% de los PDFs no necesita OCR” es una cifra de Firecrawl, presentada sin fuente. Es creíble en su dirección, pero mídela contra tu propio corpus en vez de presupuestar con ella — un script de dos llamadas sobre cien de tus documentos reales te da tu número real en una tarde.

Vale una tarde

La barrera acá es baja y el resultado es concreto: instalas un paquete, corres classifyPdf sobre una muestra de tus documentos de producción, y descubres qué fracción de tu gasto en OCR te está comprando texto que ya tenías. Si la respuesta se acerca a la mitad, la rama de routing se paga sola la semana que la despliegas. Y a diferencia de casi todo lo que promete ahorro, esto es MIT, corre local y se verifica con dos llamadas a función.


¿Y tú? ¿Qué porcentaje de los PDFs que procesas terminan en OCR sin necesitarlo — ya lo mediste, o todavía mandas todo por el mismo camino?