Why You Should Care About anydoc

When feeding documents to LLMs, conversion quality directly impacts RAG accuracy. Misaligned tables from Word, lost hierarchy from PPT, broken paragraphs from PDF — every formatting issue degrades vector search precision. Previously, stitching together pandoc, python-docx, LibreOffice, and more was the only way to cover mainstream office formats — costly to maintain and inconsistent in output.

anydoc is a document conversion library built from scratch in Rust by the Firecrawl team. One API call handles 14 formats with a median conversion time of just 4.4 milliseconds. It has already earned 12,000+ stars on GitHub and is becoming the new standard for document preprocessing in RAG/LLM toolchains.

The 14 Supported Formats

anydoc boasts the broadest format coverage of any open-source conversion tool:

Category Extensions
Word .doc, .docx, .docm
PowerPoint .ppt, .pps, .pot, .pptx, .pptm, .ppsx, .ppsm
Excel .xls, .xlsx, .xlsm, .xlsb
OpenDocument .odt, .ods, .odp
Rich Text .rtf
eBook .epub
Data .csv
PDF .pdf

Notably, both .doc (legacy binary) and .docx (modern XML) are supported — a rarity among similar tools. Most only handle .docx and choke on older .doc files.

Benchmark: The Secret Behind 4.4ms

The official benchmark tested 7 tools on 100 real-world documents across all 14 formats:

Tool Formats Median Time Quality Score Completeness Structure Formatting Cleanliness
anydoc 14/14 4.4ms 81 87 79 78 81
libreoffice 12/14 1129.5ms 40 59 42 40 24
unstructured 8/14 572.9ms 63 76 59 51 63
markitdown 6/14 134.8ms 65 78 66 60 52
pandoc 5/14 102.1ms 56 74 57 56 38
docling 4/14 513.6ms 57 60 60 57 51
mammoth 1/14 52.5ms 70 84 71 75 51

Key findings:

  1. Speed dominance: anydoc's 4.4ms is 23x faster than runner-up pandoc (102.1ms) and 257x faster than LibreOffice (1129.5ms)
  2. Highest quality: Top scores on every judged format
  3. Broadest coverage: The only tool supporting all 14 formats
  4. Rust advantage: Pure Rust, no ML models, no external services

Quality was scored by Claude Sonnet 5 as an LLM judge, blind-comparing outputs against ground truth (first 6 pages rendered by LibreOffice). Each pair was judged twice with swapped positions to eliminate bias — 482 verdicts total.

Installation & Quick Start

anydoc ships a Rust core with Node.js, Python, and WebAssembly bindings, plus a zero-config CLI.

CLI (Zero Install)

# Run directly with npx; prebuilt binary downloads on first run
npx @firecrawl/anydoc report.docx               # Markdown to stdout
npx @firecrawl/anydoc slides.pptx -o slides.md  # Output to file
npx @firecrawl/anydoc - --format csv < data.csv # Read from stdin

Python Integration

pip install firecrawl-anydoc
import anydoc

# Convert from file path
markdown = anydoc.to_markdown("report.docx")

# Convert from bytes (auto-detect format)
with open("report.docx", "rb") as f:
    data = f.read()
markdown = anydoc.to_markdown_bytes(data)

# Specify format (needed for signature-less formats like CSV)
markdown = anydoc.to_markdown_bytes(csv_data, "csv")

# Get document model (includes embedded assets like images)
document = anydoc.to_document(data)

Node.js Integration

npm install @firecrawl/anydoc
import { toMarkdown, toMarkdownBytes, toDocument } from '@firecrawl/anydoc';

const md = await toMarkdown('report.docx');
const mdFromBytes = await toMarkdownBytes(bytes);
const doc = await toDocument(bytes);

Rust Native

cargo add anydoc
let markdown = anydoc::to_markdown("report.docx")?;
let markdown = anydoc::to_markdown_bytes(&bytes, None)?;
let markdown = anydoc::to_markdown_bytes(&bytes, anydoc::Format::Csv)?;

Integration with LangChain / LlamaIndex

In a RAG pipeline, anydoc typically serves as the document loading step, converting various formats to Markdown before chunking and vectorization.

With LangChain

import anydoc
from langchain.text_splitter import MarkdownTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS

def load_documents(file_paths):
    docs = []
    for path in file_paths:
        md = anydoc.to_markdown(path)
        docs.append({"content": md, "source": path})
    return docs

splitter = MarkdownTextSplitter(chunk_size=1000, chunk_overlap=100)
raw_docs = load_documents(["report.docx", "slides.pptx", "data.xlsx"])

chunks = []
for doc in raw_docs:
    chunks.extend(splitter.split_text(doc["content"]))

embeddings = OpenAIEmbeddings()
db = FAISS.from_texts(chunks, embeddings)
db.save_local("./faiss_index")

With LlamaIndex

import anydoc
from llama_index.core import Document, VectorStoreIndex

file_paths = ["contract.docx", "presentation.pptx", "budget.xlsx"]
documents = []
for path in file_paths:
    md = anydoc.to_markdown(path)
    documents.append(Document(text=md, metadata={"source": path}))

index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("What are the breach clauses in the contract?")
print(response)

Best Practices for RAG

1. Use to_document() for Embedded Resources

When documents contain images, to_document() preserves image bytes and media types. You can extract alt text, send images to a multimodal model for OCR, or preserve external URL references.

2. Leverage Auto Format Detection

anydoc detects format from file content magic bytes, not extensions. Even misnamed files convert correctly:

fmt = anydoc.format_from_bytes(data)  # Returns Format enum

3. Batch Processing with Threads

Python bindings release the GIL, so multi-threaded batch conversion runs without blocking:

import concurrent.futures
import anydoc

with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
    results = list(executor.map(anydoc.to_markdown, file_paths))

4. Handling Scanned PDFs

anydoc embeds pdf-inspector to auto-detect text vs. scanned PDFs. For scanned documents, pair with Firecrawl's Parse API for OCR.

Limitations

  1. No OCR built-in: Scanned PDFs need external OCR. Firecrawl Parse API fills this gap.
  2. Complex layouts may differ: Multi-column or heavily mixed content may not match professional typesetting. anydoc targets LLM-readable structured text, not pixel-perfect reproduction.
  3. Legacy .doc format: Supported but OLE2 binary parsing is inherently more complex than .docx.
  4. No ML enhancement: Deliberately avoids ML models — fast and GPU-free, but less capable on complex layouts than docling or marker.
  5. WASM memory limits: Browser version runs locally (great for privacy), but large files may hit browser memory constraints.

Verdict

anydoc solves a real pain point: replacing the four-or-five-library juggling act with one lightweight, fast, format-complete tool for document conversion in RAG/LLM pipelines.

Strengths: - 14 formats — the only open-source tool covering all of them - 4.4ms median — 23x faster than the next fastest - Top quality scores across every judged format - Pure Rust, zero external dependencies, no GPU needed - Python, Node.js, Rust, and WASM bindings

Best for: - RAG pipeline builders handling mixed user uploads - Teams needing consistent document output - Real-time applications with latency requirements - Developers tired of maintaining multiple conversion dependencies

GitHub: firecrawl/anydoc (12,000+ stars)

FAQ

Q1: How does anydoc differ from pandoc? pandoc is a general-purpose converter supporting many input/output formats but with limited depth per format. anydoc focuses on converting office documents to LLM-friendly Markdown — higher quality on its 14 formats, 23x faster, and purpose-built for RAG.

Q2: Does anydoc require a GPU or external service? No. Pure Rust, no ML models, no external APIs. Runs locally with a 4.4ms median per document.

Q3: Can anydoc handle scanned PDFs? anydoc embeds pdf-inspector for text-based PDFs. For scanned (image-based) PDFs, pair with Firecrawl Parse API (with OCR).

Q4: What output format does anydoc produce? GitHub Flavored Markdown (GFM) — headings, tables, lists, code blocks, links. All 14 formats produce consistent output.

Q5: Can anydoc run in the browser? Yes. The WebAssembly build (@firecrawl/anydoc-wasm) converts files locally without uploading — ideal for privacy-sensitive scenarios.