Firecrawl closes the loop: AnyDoc adds the other 14 formats to the pipeline that pdf-inspector started
By Devy
A couple of weeks ago we covered pdf-inspector, Firecrawl’s Rust library that decides in milliseconds whether a PDF needs OCR before you pay anyone to read it. The post ended in a comfortable place: it solved PDFs and only PDFs.
On August 6th Firecrawl published the other half. AnyDoc is a library—also Rust, also MIT, also with no API key—that converts 14 document formats to clean Markdown with a single call:
let markdown = anydoc::to_markdown("informe.docx")?;
And it embeds pdf-inspector underneath, so the same call also handles text PDFs. The announcement’s phrasing is the whole thesis: “AnyDoc handles all 14 in one dependency-free library.”
If you maintain a service where users upload “whatever they have”—a contract in .docx, an .xls exported in 2009, a pitch deck, an .epub—today you probably have four libraries stitched together with an if on the file extension. This post is about how much of that tree you can delete, and about three things I discovered testing it with Spanish documents that the official comparison doesn’t cover.
What it covers, exactly
The 14: docx, doc, docm, xlsx, xls, xlsm, pptx, ppt, rtf, odt, ods, odp, epub and csv. Plus text PDFs via pdf-inspector.
A clarification on the count, because the README and the blog don’t say the same thing: the README lists quite a few more extensions than those fourteen—xlsb, pptm, ppsx, ppsm, pps, pot—and puts PDF inside the list. So “14” is a count by format family, not by recognized extension. In practice it covers more than 14 suffixes; in practice too, neither of the two lists includes native Google Docs or HTML, which is where MarkItDown still has its territory.
What matters for a real pipeline: detection is by content, not by extension. I copied a .docx to a file without an extension and to_markdown() converted it anyway. Worth noting the distinction—the format_from_path() helper does depend on the suffix and returned None on that same file. The conversion reads magic bytes; the helper reads the name. If your storage keeps blobs with opaque names, use the direct conversion and not the helper.
Installation
Three registries, one core in Rust:
cargo add anydoc
npm install @firecrawl/anydoc
pip install firecrawl-anydoc
The Python installation downloads a precompiled abi3 wheel of 3.4 MB. No Rust toolchain, no compiling anything, no system dependencies—which is the point where most parsing stacks ask you for LibreOffice or antiword in the Dockerfile.
And the same trap that pdf-inspector already had: version numbers aren’t aligned across registries. PyPI is at 0.1.7 while the crate and the npm package run their own stream. Pin by registry and don’t try to reason across them.
The API is short on purpose:
import anydoc
markdown = anydoc.to_markdown("informe.docx") # Markdown string
doc = anydoc.to_document(open("informe.docx","rb").read()) # structured AST
to_document() is what matters if you’re going to do chunking: it returns blocks, tables, notes and assets as structure instead of plain text, so you can segment by actual heading instead of by regex on #. There’s also a WASM build for browser and CLI examples for all three languages.
The vendor’s benchmark, and why it falls short
Firecrawl published a comparison against six converters on 100 real documents: AnyDoc scores 81 overall quality versus 70 for the next best, with 4.4 ms median per document, and is the only library in the set that parses all 14 formats (LibreOffice covers 12, and its median is in the range of 52 to 1,130 ms).
Read it as a vendor claim, not a verdict, for three reasons that the post itself admits or that jump out when comparing sources:
- The jury is an LLM. Quality is scored by Claude Sonnet 5 on completeness, structure, format and cleanliness. It’s a reasonable methodology and it’s reproducible; it’s not an objective metric.
- Each tool averages only the formats it supports. The post says it explicitly. AnyDoc’s 81 spans the 14; the competitor’s 70 spans the subset that competitor declares. You’re comparing averages of different populations.
- The README and the blog don’t match. The blog says 81 and 4.4 ms; the README says 80 and 4.7 ms. The difference is irrelevant to your decision, but it tells you there are at least two different runs circulating with the same headline.
The corpus, besides, is from Firecrawl. So I ran my own.
The Spanish-language test
I generated nine documents with what breaks parsers in our region and almost never shows up in an English test corpus: accents, ñ, Latin quotation marks «», opening punctuation ¿ ¡, em dash, ±, €, decimal separator with comma, and a CSV exported as Excel exports it in Spanish—UTF-8 with BOM and ; delimiter. All on the Python binding, median of 20 runs, on a shared cloud container (so: the times are from my machine, not a clean benchmark).
| Format | Median | What came out |
|---|---|---|
rtf |
0.04 ms | Text and bold correct |
odp |
0.09 ms | Title only |
ods |
0.18 ms | Complete table |
odt |
0.20 ms | Heading, paragraph and table |
csv |
0.22 ms | Markdown table, BOM and ; handled |
epub |
0.22 ms | Heading, paragraph, list + duplicated TOC |
xlsx |
0.26 ms | Table, empty formula |
pdf |
1.39 ms | Text correct, one space eaten |
pptx |
1.64 ms | Slides + presenter notes |
docx |
14.32 ms | Everything: headings, bold, italic, lists, table |
The encoding comes through clean on all ten. Zero mojibake, zero ? in place of ñ, zero Latin quotation marks turned into garbage. RTF—which stores non-ASCII as \u241? escapes—came out perfect. CSV with BOM and semicolon too, no configuration needed. That was the real risk and it didn’t materialize.
What I did find, three things:
Excel formulas don’t evaluate. I put =SUM(B2:B4) in the total cell and the Markdown brings the row with an empty cell:
| Total | | |
It’s consistent with the design—there’s no calculation engine here, and the .xlsx stores the cached value in a place the library doesn’t read—but if your spreadsheets carry calculated totals, the summary you pass to the LLM will have holes right where the numbers that matter are. Verify it before putting it in a financial pipeline.
The EPUB duplicates the title and drags the TOC. The output brings the title as #, again as ##, then the table of contents as a link list, and only then the chapter. Nothing broken, but if you do embeddings on that you’re indexing navigation noise as if it were content. A two-line filter solves it; you need to know it’s needed.
The PDF ate a space. "Informe de facturación — año fiscal 2025" came out as "Informe de facturacion-ano fiscal 2025" (the accents were lost by my test PDF, not the library; the detail is the dash). It’s the word-joining heuristic from pdf-inspector acting on an em dash surrounded by spaces. A minor artifact, right at the edge where Spanish punctuation and an English-minded heuristic cross.
And a note on speed: the .docx took 14.3 ms median, about 65 times longer than the equivalent .odt with the same content. The published median of 4.4 ms is an average across the 14 formats, and the lightweight formats—rtf, csv, odf—pull it down. If your load is 90% Word, size with the docx number, not the headline. Still two orders of magnitude better than spinning up LibreOffice, but it’s not 4 ms.
Fair note on the odp: my OpenDocument presentation had only one text box, so that “title only” is the limit of my test file and not necessarily the library’s.
What it doesn’t do
It doesn’t do OCR. A scanned PDF, or any document whose content is images of text, isn’t solved here: the documentation sends you to route those to Firecrawl’s hosted /parse endpoint. It’s the same division of labor that pdf-inspector already proposed — classify locally and cheaply, pay only for what a model really needs.
And it’s worth being clear about the split between the two repos, because they’re deliberately separate products: pdf-inspector is the PDF specialist with OCR routing per page; AnyDoc is the generalist for the other 14 formats that embeds the former so you don’t have to reimplement it. If your corpus is pure PDF, you still want pdf-inspector directly, with its ScanStrategy and its pagesNeedingOcr. If your corpus is “whatever the user uploads”, AnyDoc is the single gateway.
Worth an afternoon
The honest test here isn’t reading the benchmark, it’s running to_markdown() on fifty real documents from your own storage — the ugly ones, the ones from 2009, the ones someone exported from an Office version that no longer exists — and looking at the output. It’s five minutes of setup because there’s nothing to compile, and the result is a concrete answer: how many of your parsing libraries end up doing work that now takes a single call.
My bet, after all of the above: encoding won’t be your problem. Formulas and navigation noise will be.
And you? How many parsing libraries do you have stitched behind that if on the file extension — and which one of them always breaks?
Sources:
firecrawl/anydoc— GitHub (MIT)- Introducing AnyDoc and pdf-inspector — Firecrawl Blog (August 6, 2026)
- pdf-inspector: classify the PDF before paying an OCR — yoDEV
- MarkItDown: Microsoft’s tool that turns any document into context for your AI — yoDEV
- Own tests:
firecrawl-anydoc0.1.7, Python binding, nine documents generated in Spanish, median of 20 runs