* feat(anydoc): support explicit hosted OCR Closes #419 Signed-off-by: Magnus Hedemark <magnus919@pm.me> * test(anydoc): update release contract expectations Signed-off-by: Magnus Hedemark <magnus919@pm.me> * test(anydoc): align hosted OCR hint contract Signed-off-by: Magnus Hedemark <magnus919@pm.me> * chore: refresh generated marketplace Signed-off-by: Magnus Hedemark <magnus919@pm.me> * chore: refresh generated llms catalog Signed-off-by: Magnus Hedemark <magnus919@pm.me> --------- Signed-off-by: Magnus Hedemark <magnus919@pm.me>
13 KiB
Formats: what anydoc converts and what GFM you get
This reference documents every input format the pinned CLI (@firecrawl/anydoc
v0.2.4) accepts, the GitHub-Flavored Markdown each one produces, and the
fidelity caveats you must know before trusting the output. Every claim below
was verified by running the real CLI against the committed fixtures in
fixtures/ (see sources.md for provenance and the verification
procedure).
Coverage: 8 families / 21 extensions / 12 parsers
| Family | Extensions | Canonical parser |
|---|---|---|
| Word | .doc, .docx, .docm |
doc (legacy OLE) / docx (.docm aliases to docx) |
| PowerPoint | .ppt, .pps, .pot, .pptx, .pptm, .ppsx, .ppsm |
ppt (.pps, .pot alias to ppt) / pptx (.pptm, .ppsx, .ppsm alias to pptx) |
| Excel | .xls, .xlsx, .xlsm, .xlsb |
xlsx (all four; calamine reads both OLE and ZIP) |
| OpenDocument | .odt, .ods, .odp |
odt, ods, odp |
| Rich Text Format | .rtf |
rtf |
| EPUB | .epub |
epub |
| CSV | .csv |
csv |
.pdf |
pdf |
That is 8 families, 21 extensions, 12 canonical parsers: doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv. These 12 names are also the
values accepted by --format; extension aliases resolve through the same
mapping (verified: --format xls and --format docm are accepted).
Format detection reads the file bytes first (PDF header, RTF open group, OLE
stream names, ZIP mimetype/content types). CSV has no content signature, so it
falls back to the extension or to an explicit --format.
Shared output behavior
All document formats flow through one shared document model and one GFM serializer, so identical logical structure yields near-identical Markdown across formats. Behaviors you can rely on everywhere:
- Headings render as
#–######with anchors. - Inline runs preserve bold, italic,
strike,`code`, and lists (bullet, numbered, nested, roman). - GFM tables with header rows; merged cells render as empty covered spans.
- Footnotes/endnotes:
[^n]reference inline, with[^n]: ...definition lines at the end of the document. - Markdown specials in source text are escaped (
\*stars*,\| pipe). - Embedded images render as their alt text only — raw image bytes never survive into Markdown.
- Bookmarks/anchor targets render as raw
<a id="..."></a>markers.
Word (.doc, .docx, .docm)
Expected output: # title, ##/### section headings, inline emphasis,
GFM tables, [^n] footnotes. DOCX, DOC, ODT, and RTF all share this document
shape; the same fixture converted as .doc, .odt, and .rtf produced
near-identical markdown.
Real conversion of fixtures/fixture-handmade-outline.docx:
## Style heading stays a heading
### Direct level overrides the style
Direct nine turns the style heading off
# Direct outline without a style
Child style nine stops inheritance
Headings come from Word styles and direct formatting; #–###### levels map
onto heading levels. Real conversion of fixtures/text.doc shows the full
document shape:
# Fixture Document
Plain paragraph with **bold**, *italic*, and ~~struck~~ runs.
## Table
| | | |
| --- | --- | --- |
| Wide head | | End |
| Tall | B2 | C2 |
| | B3 | C3 |
## Notes and special text
Music clef 𝄞 appears before this footnote[^1] reference.
[^1]: Footnote after an astral character.
Caveats:
- Merged cells in Word tables render as empty covered spans (the covered cells are blank, not repeated or filled).
- Nested tables flatten into a single cell (GFM cannot nest tables) — a known limitation of the library.
- Legacy
.doc(OLE) converts through the same document serializer with the same shape; only the relative-link target rendering differs cosmetically between sources. - Fillable-form controls (DOCX content controls) lose their field layer; labels and underline glyphs survive.
PowerPoint (.ppt, .pps, .pot, .pptx, .pptm, .ppsx, .ppsm)
Expected output: slide titles as plain paragraphs (never markdown
headings), bullet lists, speaker notes as > blockquotes, and — for PPTX and
ODP — slide tables as proper GFM tables. Legacy .ppt flattens tables to bare
text lines (see caveat).
Real conversion of fixtures/pres.pptx:
Deck Title Slide
- Top level point
- Nested detail
- Second point with emphasis
> Speaker note for the intro slide.
Numbers Slide
| Region | Total |
| --- | --- |
| North | 42 |
Grouped shapes below.
Caveat — legacy .ppt flattens tables to bare text lines. The same deck
converted from fixtures/pres.ppt renders the Numbers Slide table as plain
lines with no | table syntax:
Numbers Slide
Region
Total
North
42
If the presentation's tables matter, use PPTX or ODP and verify the | rows
survived (see workflows.md, "Output verification").
Excel (.xls, .xlsx, .xlsm, .xlsb)
Expected output: each worksheet becomes a ## <sheet name> heading followed by
a GFM table; the first row is used as the table header when it looks
label-like.
Real conversion of fixtures/sheet.xlsx (first table):
## Values
| Kind | Value | Note |
| --- | --- | --- |
| Percent | 0.155 | fifteen and a half |
| Currency | 1234.5 | dollars |
| Thousands | 9876543 | grouped |
| Date | 2026-03-15 | ides of March |
| Duration | 26:30:15 | over a day |
| Tiny | 0.0000004 | four ten-millionths |
| Boolean | TRUE | yes |
Caveats:
-
XLS/XLSX drop number formats (issue #27). Cells carry their raw values, not the formatted display values:
Percent → 0.155(not15.5%),Currency → 1234.5(not$1,234.50), thousands9876543. A percentage reading as a raw fraction is wrong by 100x in meaning — warn consumers and sanity-check spreadsheets. Dates survive as ISO strings (2026-03-15). -
ODS is the contrast case: it keeps the formatted display values (
15.5%,$1,234.50,9,876,543) on the same logical content. If display values matter, prefer ODS or a CSV export. -
Merged cells render as empty covered spans within the populated range only. Real conversion of
fixtures/handmade-merged.xlsx:| | | | | --- | --- | --- | | Merged across | | padded | | tall | b2 | 3.5 | | | b3 | | -
Hidden rows and columns are treated as visible and appear in the output (known limitation) — check for hidden template or calculation content before feeding output to an LLM.
OpenDocument (.odt, .ods, .odp)
-
.odt: same document shape as DOCX/DOC/RTF —#/##headings, GFM tables,[^n]footnotes. Real conversion offixtures/text.odtmatches thetext.docoutput structure line-for-line (only relative-link targets differ in depth). -
.ods: same spreadsheet shape as XLSX (## Values+ GFM table) but with formatted display values preserved — the Excel number-format caveat does not apply. Real conversion offixtures/sheet.ods:## Values | Kind | Value | Note | | --- | --- | --- | | Percent | 15.5% | fifteen and a half | | Currency | $1,234.50 | dollars | | Thousands | 9,876,543 | grouped | -
.odp: same slide serializer as PPTX — slide titles as plain paragraphs, speaker notes as blockquotes, and GFM tables kept (unlike legacy.ppt). Real conversion offixtures/pres.odp:Deck Title Slide - Top level point - - Nested detail - Second point with emphasis > Speaker note for the intro slide. Numbers Slide | Region | Total | | --- | --- | | North | 42 |One cosmetic difference vs PPTX: a nested bullet renders as
- - Nested detailon one line rather than as an indented sub-list. The table, blockquote notes, and paragraph titles are identical in shape to PPTX.
Rich Text Format (.rtf)
Expected output: the same document shape as DOCX/ODT (# Fixture Document,
## sections, GFM tables, [^n] footnote definitions). Real conversion of
fixtures/text.rtf matches text.odt structure; the one notable difference is
that relative link targets render with a file:/// absolute path, e.g.
[a sibling file](file:///anydoc/tests/fixture-src/sibling.odt), instead of a
relative path — a known cosmetic quirk.
EPUB (.epub)
Expected output: # chapter headings (plus the book metadata title), GFM
tables, preserved inline emphasis/code, and internal anchor links resolved to
fragments. Real conversion of fixtures/book.epub:
# Fixture Book
# Fixture Book
anydoc tests
<a id="epub-text-ch001-xhtml-chapter-one"></a>
# Chapter One
Opening paragraph with **bold**, *italic*, and `code` runs.
See [Chapter Two](#epub-text-ch002-xhtml-chapter-two) for the table, or jump straight to [the marked paragraph](#epub-text-ch002-xhtml-markpoint).
<a id="epub-text-ch002-xhtml-chapter-two"></a>
# Chapter Two
| Name | Qty |
| --- | --- |
| Bolts | 12 |
| Nuts | 30 |
Notes: the book title may appear twice (metadata title + injected title);
internal links keep working as [text](#fragment) links; external links stay
as normal markdown links.
CSV (.csv)
Expected output: the file renders as one GFM table. The first row is promoted to the header row when it looks like labels (≥ 2 columns, non-empty, non-numeric, distinct fields) — this behavior ships in 0.2.4. Quoted fields with embedded commas and newlines are preserved.
Real conversion of fixtures/fixture-handmade-quoted.csv:
| name | desc | qty |
| --- | --- | --- |
| padded | comma, inside | 3 |
| plain | multi line | 4 |
Also verified:
-
Delimiter sniffing — a semicolon-delimited file with decimal commas splits on
;and keeps1,5intact (real output offixtures/fixture-handmade-semicolon.csv):| a | b | c | | --- | --- | --- | | 1,5 | 2,5 | x | | 3,0 | y | z | -
UTF-16 (with BOM) decodes to correct Unicode (real output of
fixtures/fixture-handmade-utf16.csv):| col1 | col2 | | --- | --- | | naïve | café | | Αθήνα | 数据 |
CSV has no content signature, so --format csv is required when reading CSV
from stdin (see cli-reference.md).
PDF (.pdf) — the lower-fidelity pipeline
Text-based PDFs convert locally through a separate pipeline (pdf-inspector)
that emits Markdown directly — PDF has no document model, so only Markdown
output exists. Real conversion of fixtures/fixture-text.pdf:
# Fixture Document
Plain paragraph with **bold**, *italic*, and struck runs. **Style-bold paragraph with a** NotBold-styled span **inside.**
## Lists
1.First numbered
2.Second numbered
a)Alpha sub one
b)Alpha sub two
i.Roman sub sub
3.Third numbered Interrupting paragraph between lists.
## Table
Wide head End Tall B2 C2 B3 C3
Fidelity caveats (verified on the real output):
- No GFM tables. Table cell text flattens into a plain paragraph run
(
Wide head End Tall B2 C2 B3 C3) — there is no|table. - No
[^n]footnotes. Footnote markers degrade to inline superscript glyphs (¹) and the note bodies drop into the flow; there is no[^1]:definition block. - Links are not emitted as markdown links. They degrade to
<u>underlined text</u>. - Numbered/bulleted list structure compresses (markers inline), and some Unicode degrades (e.g. emoji without ZWJ).
Scanned or image-only PDFs — explicit hosted OCR
A PDF with no extractable text layer fails as unsupported with this exact
message (exit code 1):
anydoc: unsupported input: PDF has no extractable text (Scanned, 1 pages): OCR is required
The local default does not perform OCR. When this message fires, report the
exact error and either route the file to local OCR tooling or, only after explicit
authorization, rerun with --ocr hosted --allow-hosted-upload. Hosted mode sends
the whole document to Firecrawl Parse because page selection is unavailable. Do
not select hosted mode implicitly or claim hosted accuracy from the upstream
announcement. See errors.md for routing guidance.
Formats anydoc does NOT support
- HTML/SingleFile (open feature request only) — not an input format.
- Images (
.png,.jpg, ...) — no image-to-text conversion. - Password-protected/encrypted documents — fail with
anydoc: document is encrypted(see errors.md). - Anything without a recognized signature and extension — fails as
unsupported input: unrecognized file content and extension: <path>.
Output-shape invariants to remember
- One serializer: the same logical structure yields near-identical Markdown across docx/odt/rtf — do not re-test each office format for the same feature.
- Spreadsheets: expect
## <sheet name>+ GFM tables; warn that xlsx/xls drop number formats (issue #27) while ODS keeps display values. - Legacy
.pptand all PDFs lose tabular structure — add a "verify the table survived" step or use PPTX/ODP and text PDFs. - Images never survive as bytes in Markdown — only alt text.