anydoc
Convert documents (doc, docx, odt, rtf, epub, pdf, presentations, spreadsheets, csv) to GitHub-Flavored Markdown
anydoc は、Word / PowerPoint / Excel / OpenDocument / RTF / EPUB / CSV / PDF を GitHub-Flavored Markdown(GFM)へ変換する Rust ライブラリ。Firecrawl が開発し、同社のホスト型 API Firecrawl Parse の変換エンジンにもなっている。ライセンスは MIT。
設計の中心は「入力形式ごとのパーサはあるが、出力経路は 1 本だけ」という点。各形式のパーサは共通のドキュメントモデルを組み立て、そのモデルを単一の Markdown シリアライザが文字列にする。そのため docx でのテーブルエスケープ修正が、rtf・odt・その他の形式にもそのまま効く。
.doc でも最新の .pptx でも同じ体裁で出る。| 形式 | 拡張子 | パーサ(Format の値) |
|---|---|---|
| Word | .doc, .docx, .docm | Doc / Docx |
| PowerPoint | .ppt, .pps, .pot, .pptx, .pptm, .ppsx, .ppsm | Ppt / Pptx |
| Excel | .xls, .xlsx, .xlsm, .xlsb | Excel |
| OpenDocument | .odt, .ods, .odp | Odt / Ods / Odp |
| Rich Text Format | .rtf | Rtf |
| EPUB | .epub | Epub(EPUB 2 / 3) |
| CSV | .csv | Csv |
.pdf | Pdf(pdf-inspector に委譲) |
検出の 3 経路
形式判定は 3 つの関数に分かれる。to_markdown はまず内容から判定し、判定できない場合に拡張子へフォールバックする。
Format::from_bytes(&bytes); // Some(Format::Docx)、該当なしは None
Format::from_extension("pptm"); // Some(Format::Pptx)
Format::from_path(Path::new("a.odt")); // Some(Format::Odt)
同じ 3 関数は Node(formatFromBytes, formatFromExtension, formatFromPath)と Python(anydoc.format_from_bytes など)にも用意されている。
from_bytes は None を返す。拡張子か、明示的な形式指定(CLI では --format csv)で名指しする。標準入力から CSV を渡す場合は拡張子もないので明示指定が必須。
.doc でも先頭が {\rtf なら RTF パーサへ回す分岐が入っている。実運用でよくある形式偽装への対処。
PDF だけはこの流れから外れる。pdf-inspector が Markdown を直接生成するため、ドキュメントモデルを経由しない。したがって PDF に to_document を呼ぶと unsupported エラーになる。
コードの入口
src/lib.rs が公開 API を 3 本だけ出している。形式の解決は resolve_format に集約されている。
pub fn to_markdown(path: impl AsRef<Path>) -> Result<String, ConvertError>
pub fn to_markdown_bytes(bytes: &[u8], format: impl Into<Option<Format>>) -> Result<String, ConvertError>
pub fn to_document(bytes: &[u8], format: impl Into<Option<Format>>) -> Result<model::Document, ConvertError>
| ディレクトリ | 役割 |
|---|---|
src/formats/ | 形式ごとのフロントエンド(doc, docx, ppt, pptx, sheet, odf, epub, rtf, csv, pdf)と detect.rs。 |
src/model/ | 共有ドキュメントモデル。block.rs / inline.rs / table.rs / list.rs / asset.rs など。 |
src/render/markdown/ | GFM シリアライザ。escape.rs(エスケープ)、anchors.rs(見出しアンカー)、table.rs(表出力)。 |
src/package/ | ZIP / XML / OLE などパッケージ読み取りと limits.rs の安全上限。 |
src/shared/ | 形式間で共通の下位処理(番号付け、グリッド、DrawingML、URI 正規化ほか)。 |
モデルには解決済みの内容しか入らない。スタイルカスケード、番号付け、参照解決はフロントエンド側で終わらせる。埋め込みアセットはバイト列を保持するため、元アーカイブを閉じた後も Document 単体で使える。
pub struct Document {
pub blocks: Vec<Block>, // 読み順の本文
pub notes: Vec<Note>, // 脚注・文末注の本体
pub assets: Vec<Asset>, // 埋め込みアセットのバイト列
}
Block の種類
| バリアント | 保持する内容 |
|---|---|
Heading | level(1 起点、元文書のアウトライン深さ)、anchor(元文書がこの見出しを参照する場合の安定 ID)、content。Word のアウトラインは 6 を超えるため、クランプはレンダラ側の責務。 |
Paragraph | 本文のインライン列。 |
List | 番号付けを解決済みのリスト。箇条書き・番号付き・入れ子・タスクリストを表現。 |
Table | セル結合とヘッダ行を含むグリッド。フロントエンドが GridBuilder で組み、消費側は Table::grid を読む。 |
BlockQuote | 入れ子可能な引用ブロック。 |
CodeBlock | lang(元文書が言語を示す場合)と改行込みの生テキスト。 |
Rule | 水平線。 |
脚注とアセット
Noteはid/kind(FootnoteかEndnote)/blocksを持つ。本文側はInline::NoteRefで id 参照する。- Markdown はバイト列を埋め込めないため、埋め込み画像は代替テキストとして出力し、バイト列は
document.assetsにメディアタイプと出所のパート名付きで残る。 - 外部 URL を持つ画像は通常の Markdown 画像として出力される。
本文構造として保持されるのは、アンカー付き見出し、太字・斜体・打ち消し、インラインコードとコードブロック、リンクと内部相互参照、元文書の番号を引き継ぐリスト、結合セルを含む表、引用、脚注・文末注、そしてスピーカーノート。
| ターゲット | パッケージ名 | インストール | 特性 |
|---|---|---|---|
| Rust | anydoc(crates.io) | cargo add anydoc | edition 2024 / rust-version 1.88 |
| Node.js | @firecrawl/anydoc | npm install @firecrawl/anydoc | 非同期。libuv スレッドプールで実行、CLI 同梱 |
| Python | firecrawl-anydoc(import は anydoc) | pip install firecrawl-anydoc | 同期。変換中は GIL を解放 |
| ブラウザ | @firecrawl/anydoc-wasm | npm install @firecrawl/anydoc-wasm | 同期・単一スレッド。パス版 API は無い |
Rust
// ファイルパスから:
let markdown = anydoc::to_markdown("report.docx")?;
// バイト列から。形式は内容から検出:
let markdown = anydoc::to_markdown_bytes(&bytes, None)?;
// 署名を持たない形式(CSV)は名指しする:
let markdown = anydoc::to_markdown_bytes(&bytes, anydoc::Format::Csv)?;
// ドキュメントモデルで止める(埋め込みアセットも取れる):
let document = anydoc::to_document(&bytes, None)?;
Node.js
import { toDocument, toMarkdown, toMarkdownBytes } from '@firecrawl/anydoc';
const markdown = await toMarkdown('report.docx'); // パスから
const fromBytes = await toMarkdownBytes(bytes); // 内容から形式検出
const fromCsv = await toMarkdownBytes(bytes, 'csv'); // CSV は名指し
const document = await toDocument(bytes); // モデルで止める
完全な API は node/README.md、型定義は node/index.d.ts にある。
Python
import anydoc
markdown = anydoc.to_markdown("report.docx") # パスから
markdown = anydoc.to_markdown_bytes(data) # 内容から形式検出
markdown = anydoc.to_markdown_bytes(data, "csv") # CSV は名指し
document = anydoc.to_document(data) # モデルで止める
ブラウザ(WebAssembly)
import init, { formatFromBytes, toMarkdownBytes, toDocument } from '@firecrawl/anydoc-wasm';
await init();
const markdown = toMarkdownBytes(bytes); // 同期呼び出し
const fromCsv = toMarkdownBytes(bytes, 'csv');
const document = toDocument(bytes);
formatFromBytes(bytes); // 'docx'、該当なしは undefined
to_markdown は存在せず、変換は常にバイト列から始まる。呼び出しは同期で呼び出し元スレッドを占有するので、メインスレッドを空けたいならワーカーで変換する。ビルドは wasm-pack --target web。Node で使う場合は init ではなくモジュールのバイト列を initSync に渡す。
デモページ(firecrawl.github.io/anydoc)はこの wasm パッケージをそのまま読み込む静的サイトで、ソースは wasm/www/。ブラウザ内で変換するためファイルは端末外に出ない。
npm パッケージが anydoc コマンドを同梱するため、インストールなしで npx から使える。初回実行時にプラットフォーム向けのビルド済みバイナリが取得される。常設したい場合は npm install -g @firecrawl/anydoc。
npx @firecrawl/anydoc report.docx # Markdown を標準出力へ
npx @firecrawl/anydoc slides.pptx -o slides.md # ファイルへ書き出す
npx @firecrawl/anydoc - --format csv < data.csv # 標準入力から読む
| オプション | 意味 |
|---|---|
-o, --output <path> | 標準出力の代わりに指定パスへ書き出す |
-f, --format <format> | 検出に頼らず入力形式を名指しする(拡張子エイリアス xls / docm / ppsx も解決される) |
-h, --help | ヘルプを表示して終了 |
-V, --version | バージョンを表示して終了 |
終了コード
| コード | 意味 |
|---|---|
| 0 | 成功 |
| 1 | 文書を読めなかった、または変換できなかった |
| 2 | 使い方の誤り(未知のオプション、入力なし、不正な --format) |
1 回の呼び出しで 1 文書のみを変換する。失敗時は anydoc: <message> の 1 行を標準エラーへ出す。CLI は対話プロンプトを出さない。
anydoc は Agent Skill 形式でも配布されており、エージェントに文書読解手段を追加できる。スキル定義は skills/convert-documents-to-markdown/SKILL.md。
npx skills add firecrawl/anydoc
スキルがエージェントへ与える指示は次の通り。
npx -y 実行-o・標準入力の 3 パターンを使う。--format は例外時のみ-o で退避変換が失敗するのは「そのファイルから意味のある Markdown が出ない」場合だけ。プロデューサ由来の軽微な破損は回復またはスキップし(log ファサードへ debug / warn で記録)、変換自体は続行する。ログのメッセージは安定 API ではない。
Rust ConvertError | Node / wasm error.code | Python 例外 | 意味 |
|---|---|---|---|
Unsupported | unsupported | UnsupportedError | 未知の形式、または変換できない形式(画像のみの PDF) |
Malformed | malformed | MalformedError | 構造的に使えない。意味のある内容を取り出せなかった |
Encrypted | encrypted | EncryptedError | 暗号化・パスワード保護されている |
ResourceLimit | resourceLimit | ResourceLimitError | 固定の安全上限(展開・入れ子・ノード数)を超えた |
MissingPart | missingPart | MissingPartError | 出力に必須のパートが存在しない |
Io | io | OSError | ファイルを読めなかった(パス版 API のみ。wasm には無い) |
Rust 側では Malformed と MissingPart が問題のあったパート名を、ResourceLimit が超えた上限名を持つ。Python では 5 種の変換失敗がすべて anydoc.ConvertError のサブクラスなので、まとめて捕まえられる。format 引数に未対応の名前を渡すと ValueError。
失敗を記録して次へ進むパターン
match anydoc::to_markdown(path) {
Ok(markdown) => Some(markdown),
// これらからは文書が出てこないので、記録して次のファイルへ。
Err(error @ (ConvertError::Encrypted | ConvertError::Unsupported(_))) => {
unconverted.push((path, error));
None
}
Err(error) => return Err(error),
}
展開爆弾・病的な入れ子・暴走展開への対策として、すべての変換に同じ上限が適用される。上限は設定変更できない。実文書はどの値も桁違いに下回る想定で決められている。
| 上限 | 値 | 対象 |
|---|---|---|
MAX_ENTRY_BYTES | 128 MiB | アーカイブ 1 エントリの展開後サイズ |
MAX_TOTAL_BYTES | 512 MiB | 1 アーカイブから読む展開後の合計バイト数 |
MAX_ENTRY_COUNT | 100,000 | 1 アーカイブ内のエントリ数 |
MAX_XML_DEPTH | 256 | XML 要素の入れ子深さ |
MAX_XML_NODES | 2,000,000 | 1 パート内の XML ノード数(要素+テキストラン) |
MAX_EXPANSION | 4,000,000 | 表の繰り返し展開が生成できる内容ありセル数 |
MAX_EXPANSION_TEXT_BYTES | 64 MiB | 繰り返し展開で複製されるテキストの合計バイト数 |
MAX_ASSET_TOTAL_BYTES | 128 MiB | Document に保持する埋め込みアセットの合計バイト数 |
MAX_RECORD_DEPTH | 64 | バイナリレコードコンテナの入れ子深さ(レガシー PPT ストリーム) |
MAX_RECORDS | 16,000,000 | 1 レコードストリームで走査するレコード総数 |
14 形式にまたがる実文書 100 件で、他 6 種の変換ツールと速度・品質を比較した結果。スコアは 0〜100 で高いほど良く、速度は 1 文書を変換する時間の中央値。
| ツール | 対応形式 | 中央値 ms | 判定文書数 | スコア | 完全性 | 構造 | 書式 | 清潔さ |
|---|---|---|---|---|---|---|---|---|
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 |
score は、そのツールが対応する形式のスコアを平均している。mammoth の 70 は docx 単独、anydoc の 81 は 14 形式全体。したがって公平な比較は下の形式別テーブル。
形式別スコア
| 形式 | 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 | - |
ハーネスは bench/ にあり、convert.py → render_truth.py → metrics.py → judge.py → report.py の順に実行して out/report.md を作る。judge.py --dry-run で呼び出し回数を見積もれ、--limit / --formats / --opponents で範囲を絞れる。コーパスは再配布不可のためリポジトリには入っていない。
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
テストの層
tests/fixtures/にコミット済みのフィクスチャコーパスをスナップショットテストする。tests/robustness.rsが全フィクスチャをミューテーションテストする。fuzz/に形式ごとの cargo-fuzz ターゲットを置く(nightly 依存のため Cargo ワークスペースからは除外)。- 速度と品質のベンチマークは
bench/。
バージョンの置き場所
リリースタグは v<version> で、クレート・npm パッケージ・PyPI ホイールを .github/workflows/release.yml から公開する。バージョンは 3 箇所にあり、リリース時にまとめて上げる。
| ファイル | 対象 |
|---|---|
Cargo.toml | クレート |
node/package.json | npm パッケージ |
python/Cargo.toml | ホイール(python/pyproject.toml が読む) |
Cargo ワークスペースのメンバは node / python / wasm。主要な依存は calamine(Excel)、cfb(OLE)、csv、quick-xml、zip、encoding_rs、flate2、pdf-inspector。リリースプロファイルは lto = "thin" とシンボル strip。
- GitHub: firecrawl/anydoc — 本体リポジトリ(Rust、MIT)
- ブラウザデモ — wasm ビルドで端末内変換
- crates.io: anydoc — Rust クレート
- npm: @firecrawl/anydoc — Node.js バインディングと CLI
- PyPI: firecrawl-anydoc — Python バインディング
- skills.sh: firecrawl/anydoc — Agent Skill の配布ページ
- firecrawl/pdf-inspector — PDF 変換の委譲先
- Firecrawl Parse — 同じ変換に OCR を足したホスト型 API
- agentskills.io — Agent Skill の仕様とクライアント一覧