anydoc README

The project's README from github.com/firecrawl/anydoc. As one markdown file: README.md.

anydoc

Crates.io npm PyPI License: MIT skills.sh

Fast Rust library that converts documents (Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF) into clean GitHub-Flavored Markdown. Includes bindings for Node.js, Python, and the browser (WebAssembly).

Built by Firecrawl to turn any office document into LLM-ready Markdown in single-digit milliseconds, with one consistent output no matter which format goes in. It powers Firecrawl Parse, so if you'd rather not run it yourself, the hosted API gives you the same conversion plus our OCR models for the scanned pages anydoc can't read on its own.

Try it in your browser: the demo page runs the library as WebAssembly, so files are converted locally and never leave your machine.

Quick start

Agent skill

anydoc ships as an Agent Skill, so your agent can read any document it runs into:

npx skills add firecrawl/anydoc

The skill teaches the agent to convert documents with the anydoc CLI. Works with Claude Code, Codex, Cursor, OpenCode, and any other compatible agent.

CLI

npx @firecrawl/anydoc report.docx               # Markdown to stdout
npx @firecrawl/anydoc slides.pptx -o slides.md  # or to a file
npx @firecrawl/anydoc - --format csv < data.csv # read stdin
npx @firecrawl/anydoc scan.pdf --ocr hosted     # scanned pages via Firecrawl Parse

npx downloads the prebuilt binary for your platform on first run. For a permanent anydoc command, install globally with npm install -g @firecrawl/anydoc. Run anydoc --help for all options.

Node.js

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

// From a file path:
const markdown = await toMarkdown('report.docx');

// To enable OCR:
const markdown = await toMarkdown('report.docx', { ocr: 'hosted' );

// From bytes, with the format detected from the content:
const fromBytes = await toMarkdownBytes(bytes);

// Or name it, which signature-less formats (CSV) need:
const fromCsv = await toMarkdownBytes(bytes, 'csv');

// Or stop at the document model, which also carries embedded assets:
const document = await toDocument(bytes);

Full API reference: node/README.md

Python

pip install firecrawl-anydoc
import anydoc

# From a file path:
markdown = anydoc.to_markdown("report.docx")

# To enable OCR:
markdown = anydoc.to_markdown("report.docx", ocr="hosted")

# From bytes, with the format detected from the content:
markdown = anydoc.to_markdown_bytes(data)

# Or name it, which signature-less formats (CSV) need:
markdown = anydoc.to_markdown_bytes(data, "csv")

# Or stop at the document model, which also carries embedded assets:
document = anydoc.to_document(data)

Full API reference: python/README.md

Browser (WebAssembly)

npm install @firecrawl/anydoc-wasm
import init, { toMarkdownBytes, toDocument } from '@firecrawl/anydoc-wasm';

await init();

// From bytes, with the format detected from the content:
const markdown = toMarkdownBytes(bytes);

// Or name it, which signature-less formats (CSV) need:
const fromCsv = toMarkdownBytes(bytes, 'csv');

// Or stop at the document model, which also carries embedded assets:
const document = toDocument(bytes);

Full API reference: wasm/README.md

Rust

cargo add anydoc
// From a file path:
let markdown = anydoc::to_markdown("report.docx")?;

// From bytes, with the format detected from the content:
let markdown = anydoc::to_markdown_bytes(&bytes, None)?;

// Or name it, which signature-less formats (CSV) need:
let markdown = anydoc::to_markdown_bytes(&bytes, anydoc::Format::Csv)?;

// Or stop at the document model, which also carries embedded assets:
let document = anydoc::to_document(&bytes, None)?;

OCR

anydoc reads text-based PDFs locally but does no OCR, so a PDF with scanned or image-only pages fails with NeedsOcr. Opt in and those documents go to Firecrawl Parse, which OCRs them and returns the same Markdown. No signup needed; set FIRECRAWL_API_KEY for higher limits.

Opt in Key, else FIRECRAWL_API_KEY
CLI anydoc scan.pdf --ocr hosted --api-key <key>
Node toMarkdown('scan.pdf', { ocr: 'hosted' }) apiKey
Python anydoc.to_markdown("scan.pdf", ocr="hosted") api_key

Only documents that need OCR leave the machine, and the whole document goes, since Parse has no page selection. If Parse cannot convert it, Node rejects with code: 'hosted' and Python raises HostedError. --api-url, apiUrl and api_url, else FIRECRAWL_API_URL, point at another Parse deployment. The Rust crate has no ocr option and never makes network calls.

Features

Supported formats

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

Benchmark

anydoc is measured against six other converters on 100 real-world documents spanning fourteen formats. Scores run from 0 to 100, higher is better; speed is the median time to convert one document.

tool formats median ms docs judged score completeness structure formatting cleanliness
anydoc 14/14 4.4 94 81 87 79 78 81
libreoffice 12/14 1129.5 87 40 59 42 40 24
unstructured 8/14 572.9 58 63 76 59 51 63
markitdown 6/14 134.8 33 65 78 66 60 52
pandoc 5/14 102.1 34 56 74 57 56 38
docling 4/14 513.6 21 57 60 60 57 51
mammoth 1/14 52.5 8 70 84 71 75 51

Per format, like for like:

format anydoc libreoffice unstructured markitdown pandoc docling mammoth
doc 87 57 67 - - - -
docm 84 48 - - - - -
docx 88 56 53 71 68 71 70
epub 77 - 72 72 52 - -
odp 86 23 - - - - -
ods 82 38 - - - - -
odt 80 51 68 - 60 - -
ppt 80 26 - - - - -
pptx 74 24 - 66 - 52 -
rtf 88 53 46 - 45 - -
xls 80 38 66 62 - - -
xlsm 76 32 - - - - -
xlsx 72 30 66 55 - 47 -

How quality was scored: an LLM judge (Claude Sonnet 5) compares two tools' outputs blind against ground truth: the document's first six pages, rendered to images by LibreOffice. Each output is scored on completeness, structure, formatting, and cleanliness. Every pair is judged twice with the outputs swapped to cancel position bias, for 482 verdicts in total. Each tool's score averages its per-format scores over the formats it supports, so a corpus heavy in one format can't skew it. It also means each row averages a different set of formats (mammoth's 69 is docx alone, while anydoc's 81 spans all fourteen), so the per-format table is the fair comparison.

Speed is one warm conversion per document on a Ryzen 9 9950X3D (Windows 11, 64 GB DDR5-6400). anydoc and the Python libraries are timed with process spawn excluded; the CLI tools include it, since that is how they are used. The harness lives in bench/; the corpus is not redistributable and is not in the repo.

Best fit: pipelines that receive a mixed bag of office documents and need one consistent, structured Markdown output. In this comparison, anydoc was the only tool to cover all fourteen formats, scored highest on every judged format, and converted documents an order of magnitude faster than the next-fastest tool.

Format detection

The format is read from the file content, using the marker its specification designates: the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype and content types. CSV has no such marker, so the extension or an explicit format names it instead.

Format::from_bytes(&bytes); // Some(Format::Docx), or None when nothing matches
Format::from_extension("pptm"); // Some(Format::Pptx)
Format::from_path(Path::new("report.odt")); // Some(Format::Odt)

The same three functions exist in Node (formatFromBytes, ...) and Python (anydoc.format_from_bytes, ...).

Errors

A conversion returns Err only when no complete Markdown could come out of the file. ConvertError names what went wrong:

match anydoc::to_markdown(path) {
    Ok(markdown) => Some(markdown),
    // No document comes out of these, so record the file and take the next one.
    Err(
        error @ (ConvertError::Encrypted
        | ConvertError::Unsupported(_)
        | ConvertError::NeedsOcr { .. }),
    ) => {
        unconverted.push((path, error));
        None
    }
    Err(error) => return Err(error),
}
Variant Meaning
Unsupported Unknown format, or one that cannot be converted
NeedsOcr Scanned or image-only pages of a PDF, listed in pages
Malformed Structurally unusable: no meaningful content could be extracted
Encrypted Encrypted or password-protected
ResourceLimit Crossed a fixed safety limit (decompression, nesting, node count)
MissingPart A part required for any meaningful output is absent
Io The file could not be read, from to_markdown only

Node and wasm publish the variant name on error.code; Python raises one anydoc.ConvertError subclass per variant, or OSError when the file cannot be read.

anydoc converts locally and does not do OCR, so a PDF with scanned pages fails with NeedsOcr. Opt in with ocr: 'hosted' in Node, ocr="hosted" in Python or --ocr hosted on the CLI to send that document to Firecrawl Parse. No signup needed. Set FIRECRAWL_API_KEY for higher limits.

How it works

document bytes
  │
  ├─► format detection      → content markers, not the extension
  │
  ├─► format parser          → one per format (doc, docx, ppt, pptx, xls,
  │                            xlsx, odt/ods/odp, rtf, epub, csv)
  │         │
  │         └─► Document     → shared model: blocks, inlines, tables,
  │                            footnotes, assets
  │               │
  │               └─► GFM serializer → Markdown
  │
  └─► PDF → pdf-inspector    → Markdown directly

Because every format funnels through the same document model and serializer, output quirks get fixed once. A table-escaping fix for docx is automatically a table-escaping fix for rtf, odt, and everything else.

Development

cargo test
cd node && npm install && npm run build && npm test
cd python && pip install maturin && maturin develop && python -m unittest discover -s tests
wasm-pack build wasm --release --target web --scope firecrawl && node --test wasm/test.mjs  # see wasm/README.md

A committed fixture corpus under tests/fixtures/ is snapshot-tested, tests/robustness.rs mutation-tests every fixture, and fuzz/ carries cargo-fuzz targets per format. The speed and quality benchmark lives in bench/.

Releases are tagged v<version>, which publishes the crate, the npm package, and the PyPI wheels from .github/workflows/release.yml. The version lives in three places, bumped together for a release:

License

MIT

anydoc (firecrawl/anydoc)

Searching Database…

22.9K ·GitHub
这是什么?这是 Firecrawl 开发的一个高性能 Rust 库,可以把办公文档——Word、PowerPoint、Excel、OpenDocument、RTF、EPUB、CSV 和 PDF——转换成干净的 GitHub-Flavored Markdown。它还附带 Node.js、Python 和浏览器(WebAssembly)绑定。
它是我在代码里调用的库,还是命令行工具?两者都是:它是一个 Rust crate,提供 Node.js、Python 和浏览器(WebAssembly)绑定,还有一个可以用 npx 运行的 CLI。它还以 Agent Skill 的形式提供,让编程智能体可以自己转换文档。
我怎么转换第一个文档?运行 npx @firecrawl/anydoc —— 预编译二进制文件会在首次运行时下载 —— 或者用 npm install -g @firecrawl/anydoc 永久安装。Node 和 Python 包也把同样的转换功能暴露为 API。
运行它还需要安装别的东西吗?不需要。它是纯 Rust 实现,没有机器学习模型,也不依赖任何外部服务;CLI 以适用于你平台的预编译二进制文件形式提供,Node 和 Python 包也是自包含的。
有没有它拒绝转换的文档?扫描件或纯图片 PDF:它本地不做 OCR,所以这类文件会报 NeedsOcr 错误而失败,除非你选择通过 Firecrawl Parse 使用托管 OCR。加密和损坏的文件也会返回明确的类型化错误,而不是输出乱码。
我的应用会收到各种随机的办公文件——这个工具合适吗?合适,这正是它官方宣称的最佳场景:接收各类办公文档的混合输入,并需要一份一致、结构化 Markdown 输出的流水线。另一个目标用户是编程智能体,通过捆绑的 Agent Skill 来使用。
我的文档会被上传到什么地方吗?默认不会——转换在本地进行,浏览器演示以 WebAssembly 运行,文件不会离开你的电脑。只有你明确发送去托管 OCR 的 PDF 会传到 Firecrawl Parse,而且因为是整份发送、无法选页,文件会完整上传。
它免费吗?商用也免费吗?它采用 MIT 许可证,所以是免费的。可选的托管 OCR 也无需注册——设置 FIRECRAWL_API_KEY 只是提高额度上限。
它支持哪些文档格式?共十四种:Word(.doc、.docx、.docm)、PowerPoint(.ppt、.pptx 等)、Excel(.xls、.xlsx、.xlsb)、OpenDocument(.odt、.ods、.odp)、RTF、EPUB、CSV 和 PDF。在项目基准测试中,它是唯一覆盖全部格式的转换器。
它和 pandoc、markitdown 比怎么样?在项目对 100 份真实文档的基准测试中,anydoc 得了 81 分,而 pandoc 是 56 分,markitdown 是 65 分;它支持全部 14 种格式(pandoc 只支持 5 种,markitdown 6 种),而且快得多:中位数耗时 4.4 毫秒对 102 毫秒。
哪些内容能保留到 Markdown 输出里?完整的结构:带锚点的标题、含合并单元格的表格、保留原始编号的嵌套列表和任务列表、脚注、演讲者备注,以及以 LaTeX 数学形式表示的公式。图片会转换为其替代文字;原始字节数据保留在文档模型上。
我怎么让我的编程智能体使用它?运行 npx skills add firecrawl/anydoc。这个 skill 会教智能体用 anydoc CLI 转换文件,并兼容 Claude Code、Codex、Cursor、OpenCode 等其他兼容的智能体。
Creator
firecrawl
Size
0.8 MB
Stars
22.9K
Age
2mo
Commits · 90 Days